做了好几年AI芯片的软硬件设计,我最深的体会是:真正难的不是把算力堆上去,而是让软件真的把硬件吃满。很多团队芯片流片回来,跑几个经典模型,性能只能到标称值的百分之三四十,问题往往不在RTL,而在软硬件之间那层没人愿意说清楚的边界上。这是系列的第二篇,上一篇我们梳理了AI芯片的整体形态和基本流程,包括什么是NPU、主流的芯片形态、以及一个芯片项目大概要经历哪些阶段。这篇想换个更硬核的视角,把硬件侧的架构决策、软件侧的编译栈、以及两者怎么协同设计这条线完整串一遍,再把我实际项目里踩过的坑和排查方法整理出来。适合正在做或打算做AI芯片的工程师、做AI框架底层优化的同学,也适合想搞清楚"芯片和软件到底怎么配合"的产品和技术管理者。
1. 重新理解"软硬件设计":AI芯片的两条腿
1.1 芯片不是堆料游戏,软件才是"翻译官"
刚接触AI芯片的人,最容易陷入一个误区:看算力。某某芯片标称20 TOPS,某某芯片标称100 TOPS,就觉得后者一定更强。但真实项目里,标称算力只是硬件在最理想条件下、以某个固定数据格式跑某个特定算子时的峰值,和用户实际能拿到的吞吐量之间,隔着十万八千里。
可以把芯片想象成一座高度自动化的工厂。硬件决定的是"工厂里有哪些机器、流水线怎么布置、物料传输通道有多宽",软件决定的是"今天要生产什么、每个工序怎么排期、物料怎么流转才能不堵车"。工厂再先进,排产计划一团糟,照样产能低下;排产计划再聪明,机器本身只能干某一种活,遇到新产品也白搭。
AI芯片的软件栈,本质上就是这个"排产计划"和"翻译官"的组合。它要做的,是把PyTorch、ONNX这些高层框架里的模型,一层层翻译成芯片能执行的底层指令,同时把计算任务排到合适的硬件单元上,让数据搬运和计算尽量重叠,让每个时钟周期都不浪费。硬件负责定义"能做哪些事",软件负责决定"怎么把事做好",两者缺一不可。
我在项目里见过太多"重硬轻软"的团队,架构师花一年时间打磨微架构,编译器团队只有两三个人,等芯片回来才发现软件跑不动,模型延迟惨不忍睹,整个项目延期。反过来,也见过硬件设计平庸但软件调优极好的芯片,在特定场景里反而跑出更优的效果。软硬件设计从来不是两个阶段,而是一件事。
1.2 从大模型到指令流:一条完整的翻译链路
理解AI芯片,绕不开那条从模型到指令的完整链路。我习惯把它分成五层:
第一层是模型层,也就是PyTorch、TensorFlow或者ONNX表达的计算图,这层描述的是"要算什么",和芯片无关。第二层是图优化层,常见的如算子融合、常量折叠、算子改写,这层开始对计算图做第一次"瘦身"。第三层是算子层,把融合后的图拆解成一个个可在硬件上执行的算子实现,比如GEMM、Convolution、Softmax的某种切分版本。第四层是内核层,也就是每个算子如何在硬件的计算阵列、存储、流水线上落地,包括循环切分、数据排布、向量化。第五层才是真正打到芯片的指令流。
这条链路每往下一层,约束条件就多一层。硬件规定了计算阵列的尺寸、片上SRAM的大小、可用的向量宽度、存储的bank数量、甚至访存的burst长度,软件每下降一层,都必须在这些约束里做选择。举例来说,一个3x3的卷积,在模型层只是一个节点,到了算子层,可能要拆成im2col + GEMM,也可能走winograd变换;到了内核层,又要决定是沿着H维度切块还是沿着C维度切块,切多大才能把输入输出完整塞进片上SRAM;最后到指令层,还可能因为硬件指令集不支持某些寻址模式,再多搬几次数据。
任何一个项目,如果团队成员心里没有这条链路的完整图景,就会在某个环节脱节。硬件工程师觉得"指令集我已经定好了",编译器工程师觉得"算子库已经够用了",框架集成工程师觉得"推理引擎已经接好了",但没人对着整条链路看数据格式、内存分配、同步机制对不对得上,最后出问题就是必然的。
2. 硬件侧的核心架构决策:算力、存储与数据流
2.1 算力单元的三条主流技术路线
先聊硬件侧最核心的决策:算力单元选型。业界主流路线基本可以分三类:脉动阵列、SIMD/SIMT向量阵列、以及近年来不少NPU采用的"向量单元+矩阵单元"混合架构。
脉动阵列的代表是Google的TPU,核心思路是把大量乘加单元排成一个二维阵列,数据像水在管道里流动一样,一个周期往阵列里塞一遍输入,所有单元同时做乘加,结果接力传递。优点是单位面积能效比极高,数据复用好,非常适合矩阵乘法和卷积这类规则计算;缺点是灵活性差,遇到非规则算子、动态形状、稀疏计算,阵列利用率会大幅下降。编译器要把算子映射得漂亮,需要做大量循环变换,难度不小。
SIMD/SIMT路线是GPU的老本行,大量并行线程共享一套控制逻辑,每个线程在同一时刻执行同一指令,适合通用并行计算。灵活性强,编程模型成熟,CUDA生态就是它的护城河,但代价是控制开销和缓存开销大,能效比不如脉动阵列。
第三条路线,典型的比如华为昇腾、寒武纪这些AI芯片走的路子:矩阵单元负责GEMM和卷积这类密集计算,向量单元负责激活、归一化、逐元素运算,标量单元负责控制流和地址计算。这种分工思路更贴近真实模型的计算需求,因为Transformer里除了大矩阵乘,还有大量LayerNorm、Softmax、逐元素乘加这类又碎又小的算子。三种路线各有优劣,我做过一个简单的对照,供选型时参考:
| 维度 | 脉动阵列 | SIMD/SIMT向量阵列 | 矩阵+向量混合架构 |
|---|---|---|---|
| 能效比 | 高 | 中 | 中高 |
| 灵活性 | 低 | 高 | 中 |
| 编译器难度 | 高 | 中 | 高 |
| 典型代表 | TPU | GPU | 昇腾、寒武纪系列 |
| 擅长场景 | 规则密集矩阵运算 | 通用并行计算 | 大模型混合负载 |
我的建议是,先把目标工作负载算清楚再选型。如果产品只做推理、模型相对固定、矩阵乘占比极高,脉动阵列的性价比最好;如果要做训练、要兼容各种框架和动态模型,混合架构更稳。最怕的是拍脑袋定了一个架构,后面编译器团队跪着也填不满算力。
2.2 存储层级与数据复用:算得再快也怕搬数据
芯片设计里有个铁律:数据的搬运成本远高于计算成本。单位能量的算术运算要比单位数据的片外搬运便宜一到两个数量级,所以AI芯片性能上不去,很多时候不是算力不够,而是数据喂不进来。这就是所谓的访存墙问题。
为了解决这个墙,硬件设计要搭一个金字塔形的存储层级:寄存器堆在最顶层,容量最小但带宽最大;中间是片上SRAM,一般几百KB到几MB,带宽可以达到每周期几十到几百字节;再往下是片外DDR或HBM,容量大但带宽和延迟都差几个量级。
关键的设计变量是片上SRAM的大小和它的数据通路。SRAM越大,能缓存的数据越多,片外访存次数越少,但面积和功耗也上去,缓存命中率还有边际递减效应。工程上有一个经典的目标:对目标模型里占比最高的算子,能把一个完整的计算Tile所需的输入、权重、输出全部装进SRAM里,让计算在片上循环,只把最终结果写回片外。
数据复用是另一个重点。卷积运算天然有数据复用:同一个输入像素会被多个输出通道的卷积核用到,同一个权重会被多个空间位置的输入用到。好的数据流设计会把这种复用显式地表达在硬件上,比如让权重在阵列里横向广播、让输入在纵向流动,最大程度减少从SRAM重复取数。之前做设计空间探索时,我见过同一份卷积代码,因为数据流方向不同,SRAM访问带宽消耗能差3倍以上。这个环节的优化需要硬件架构师和编译器工程师坐在一起,反复对着数据流图核对,而不是各写各的文档。
2.3 片外带宽、片上互联与SoC集成
算力单元和存储层级定了之后,还要面对片外带宽和片上互联。片外带宽的选择基本决定了芯片的"天花板"。以HBM为例,一颗HBM2E能提供约460GB/s的带宽,而一颗LPDDR4X只有30-50GB/s,差距一个数量级。但HBM贵、封装复杂、功耗也高,不是所有产品都适合。
做边缘侧芯片,比如瑞芯微RK3588、地平线征程系列这类SoC,一般选LPDDR,带宽有限,所以架构设计更强调片上缓存和数据复用,编译器也要把访存调度优化放到首位。做云端训练芯片,比如寒武纪思元、壁仞这样的,HBM几乎是标配,因为模型规模摆在那,没有足够带宽,计算阵列只能空转。
片上互联负责把计算单元、SRAM、DMA、外部接口连接起来。复杂SoC一般用NoC或者层次化AXI总线,设计时要注意带宽的对称性和路径的一致性。我踩过一个坑:一套总线协议在各模块的地址映射上有一处重叠,导致DMA搬运到错误地址,数据静默损坏,跑模型时精度忽高忽低,查了两周才定位到。这种问题在验证阶段如果没做全地址空间的交叉检查,大概率会漏到芯片回来之后。
SoC集成层面,还有启动、电源、时钟这些"不性感但致命"的环节。启动流程一般是BootROM读片外存储上的引导代码,逐步加载固件和驱动;这里涉及QSPI Flash、eMMC、看门狗芯片、升压电源芯片这些外围器件。很多工程师盯着NPU架构,忽略了这些细节,结果芯片回来点不亮或者经常死机复位。Jetson Orin Nano这类开发板如果QSPI Flash损坏,系统只能通过专用工具重新烧写,可见这个环节的可靠性设计非常重要。
3. 软件侧的编译器设计与运行时机制
3.1 图优化与算子融合:先把计算图瘦身
硬件架构定了,软件侧的重头戏开始。编译器第一道工序是图优化,宗旨只有一个:减少不必要的数据搬运和计算。最经典的优化是算子融合,典型如Conv+BatchNorm+ReLU融合成一个算子。不融合时,卷积的输出要写回片外内存,BatchNorm再读出来算一遍,ReLU再读出来又写一遍,一个算子三次访存,数据量大了之后片外带宽直接被浪费掉。融合之后,中间数据留在片上SRAM里,一次流水线走完,访存开销大幅下降。
我实测过一个MobileNetV3的优化,单纯做算子融合这一项,端到端推理延迟能降低20%到35%,具体取决于芯片的SRAM大小和片外带宽。图优化还包括布局转换,比如把NCHW布局转成NHWC或者NC1HWCO。不同硬件对数据排布的要求差异很大,向量单元喜欢通道在连续维度,矩阵单元可能要求特定的分块排布,编译器如果只做一次全局布局决策,性能往往不好。
真正让图优化变得复杂的原因是精度和动态性。比如推理引擎里常见的动态形状——输入图片分辨率不固定、batch size变化——很多优化在静态形状下很有效,形状一动态就失效。所以现代AI编译器普遍采用"静态形状优先,动态形状兜底"的策略,对热门模型做静态特化,对冷门输入走通用路径。
3.2 算子映射与指令生成:编译器最难啃的骨头
图优化之后,编译器要把每个算子变成能在硬件上高效执行的指令序列。这里最核心的工作是两层:循环切分和指令调度。
循环切分解决的是"数据放不下"的问题。一个矩阵乘,假设输出是4096x4096,片上SRAM只有2MB,不可能一次性算完,必须把计算切成一个个Tile,保证每个Tile的输入、权重、输出都能装进SRAM,并且Tile之间的数据用DMA预取来重叠。切分粒度太大,SRAM溢出,频繁访存;切分粒度太小,DMA启动开销占比上升,计算单元饿肚子。这个平衡没有常数解,要看硬件参数,基本要靠编译器里的代价模型自动搜索。
指令调度解决的是"硬件怎么动起来"的问题。矩阵单元、向量单元、DMA、标量单元,哪个先动、哪个后动、怎么用同步信号保证数据依赖正确,都体现在这一层。好的调度能让DMA搬运和矩阵计算重叠,让矩阵计算和向量激活流水起来,做到"算着算着数据就位了"。
以GEMM映射到脉动阵列为例子,业界有个经典优化思路,分解成六个维度(M、N、K方向的切分,以及阵列映射、数据复用的方向选择等)逐步求解。第一步确定K方向的切分,保证权重分块能放进阵列;第二步确定M、N方向的Tile,兼顾SRAM容量和写回效率;第三步决定数据在阵列里的流动方向,比如输入从上方流入、权重在阵列中广播,还是反过来;最后再做指令级流水排布。这一套操作,纸上写起来简单,真正实现起来要考虑边界条件、对齐约束、位宽差异,工作量相当可观。
3.3 运行时、驱动与推理引擎:让模型真正跑起来
编译器和内核只是把"能算的东西"准备好了,模型真正要跑起来,还差运行时和驱动。
运行时负责三件事:内存管理、任务调度、生命周期管理。内存管理要解决的是频繁的分配释放带来的碎片化问题,所以推理引擎一般用内存池,一次性向驱动申请大块连续内存,内部自己管理。任务调度要把编译好的多个算子按依赖关系排队,分配给不同的硬件单元,同时处理多线程、多进程的并发访问。大模型场景下,还要考虑KV Cache的预分配和动态扩缩容。
驱动层是软硬件最后的接缝。寄存器读写、消息邮箱、中断处理、DMA描述符的填充和回收,都在这层完成。我最担心的是这一层的代码质量和文档维护。芯片的寄存器定义经常在验证阶段变动,同一个功能地址改来改去,驱动如果没同步更新,非常容易出现"硬件明明改了,软件还按旧地址写"的静默错误。这种问题用仿真很难复现,因为行为仿真里地址错位往往直接挂死,但真实芯片上可能只是行为异常,很难定位。
推理引擎层把上面所有能力封装成上层框架能调用的接口,比如兼容ONNX Runtime或者自定义一套C API。对开发者友好度的评估,要看你从PyTorch导出一个模型之后,几步能跑起来。这个过程涉及算子支持清单、量化格式对齐、输入输出的内存布局约定,哪一环不透明,用户都会在集成时踩坑。
4. 软硬件协同设计与全流程落地
4.1 先定工作负载,再谈架构
软硬件协同设计的第一步,也是最容易被跳过的:定义清楚目标工作负载。不是一个宽泛的"我们要支持主流模型",而是具体到模型的集合、各算子的时间占比、输入形状的分布、batch size的范围、精度要求(FP16还是INT8)、延迟还是吞吐优先。
我习惯先从公开模型库里选一个代表性子集,跑一遍算子级的profiling,把时间占比拉出来看。比如一个典型的BERT类模型,矩阵乘可能占60%以上,但LayerNorm、Softmax、GELU这些逐元素算子加在一起也有20%-30%。如果硬件只优化GEMM,不关心向量单元,那再好的矩阵阵列也会被这些"碎算子"拖住。而一个基于MobileNet的视觉模型,卷积的形态又完全不同,深度可分离卷积在硬件上利用率天然偏低,需要专门的映射策略。
工作负载分析的产出是一张算子时间分布表和一个访存强度分布图,这两样东西直接决定硬件参数的初值。算力要多大、SRAM要多大、片外带宽要多高,都应该从这张表倒推,而不是先定一个"看起来好看"的TOPS数字。
4.2 性能建模与架构探索:不是拍脑袋定参数
工作负载定了之后,架构探索要用模型而不是感觉。业界最常用的工具是Roofline模型,画一条"机器天花板"曲线,横轴是运算强度(每字节数据承载多少次运算),纵轴是可达性能。任何计算内核在这张图上的位置,要么被计算上限限制,要么被带宽上限限制,一眼就能看出瓶颈在哪。
举个例子,一个10 TOPS的芯片,假设片外带宽只有30GB/s,那它的Roofline转折点在运算强度约333 FLOPs/Byte。一个计算强度只有50的算子,理论上限就是1.5 TOPS,无论计算阵列多强都跑不满。这类分析的价值,是在花几百万美元流片前,把最致命的性能问题暴露出来。
更精细的架构探索需要周期级仿真器或者性能原型。高精度仿真的精度好但速度慢,跑一个大模型要几天;分析模型快但误差大。工程实践里一般两级配合:初期用分析模型快速扫设计空间,锁定几个候选配置;再用周期级仿真对热点算子精仿,修正关键参数。需要注意的是,SRAM容量、MAC阵列规模、频率、带宽这四个变量是强耦合的,改一个往往要连带看其他三个的连锁效应,不能只看单一变量。
4.3 从RTL验证到芯片后端再到片上调试的完整流程
架构探索收口之后,硬件进入实现阶段。RTL设计、验证、逻辑综合、后端物理设计、DFT、流片,这是硬件团队的主战场。但软硬件协同在这个阶段就已经开始了,最常见的做法是FPGA原型验证和硬件仿真加速器(emulator)双轨并行。
FPGA原型提前把RTL跑起来,软件团队可以在流片前完成驱动开发、系统启动验证、基础算子调试。一个典型的AI芯片设计,FPGA原型通常比真实芯片慢几十倍,但对于验证功能正确性足够用。另一个路径是用仿真加速器,比如Cadence的Palladium或Synopsys的Zebu,速度快、可调试性好,但成本高,主要用来跑操作系统的启动流程和复杂并发场景。
芯片后端流程和芯片测试也是协同设计的一部分。后端要做时序收敛,芯片测试要做ATE测试和硬件功能测试,软件团队需要提供一套自检测试程序(通常叫D0测试),在拿到首批芯片时快速验证基本功能。经验是:这套测试程序最好在流片前就跑过仿真和FPGA,不然芯片回来你都不知道是芯片坏了还是测试程序写错了。
5. 实测踩坑与典型案例复盘
5.1 软硬件接口不一致导致的"静默错误"
先分享一个我印象深刻的案例。某款NPU的硬件团队在实现时,把DMA描述符里的地址字段从32位扩展到了36位,但寄存器地址偏移没变。驱动这边一直按旧文档用32位读写,功能仿真没发现异常,因为验证平台里所有地址都落在低32位。流片回来后,跑大模型时偶尔出现计算错误,而且错误位置完全不固定。排查了两周,最后是用printk把DMA描述符逐个字节打出来,才看到高4位地址丢失。
这种问题本质是软硬件接口的版本管理失控。解决思路是三条:第一,把寄存器描述文件(通常叫RDL或者IP-XACT)作为唯一事实源,驱动代码和验证环境都用它自动生成,不允许手写;第二,每次寄存器改动都强制触发全量回归,包括FPGA上的驱动测试;第三,在硬件里加校验机制,比如关键描述符的CRC校验,软件写错能立刻被发现,而不是静默算错。
5.2 性能不达标的排查路径:先分清楚瓶颈再动手
芯片性能跑不到预期,排查路径要按部就班:先看是不是计算瓶颈,再看是不是访存瓶颈,最后看是不是调度开销。用Roofline做第一层判断最简单,把一个算子的运算强度算出来,够不够转折点一测便知。
有个实际例子:一个卷积在阵列利用率只有40%,但整算子性能还是低的。一开始大家以为是矩阵映射有问题,各种优化做了半天没效果。后来用性能计数器一看,发现DMA的排队等待时间占了整个算子耗时的50%,根因是输入Tensor的对齐导致DMA传输分成了大量碎片请求,带宽利用率极低。解决方案是改编译器里的对齐策略,让DMA每次搬运都是完整的burst长度。一个很小的代码改动,算子性能直接翻倍。
类似的还有启动开销问题。推理引擎如果拆出大量细粒度算子,每个算子的启动延迟就会叠加。之前优化一个小型检测模型,端到端延迟总是压不下去,原因是图优化没做到位,400多个算子提交到硬件,每个算子的任务调度和同步开销占了大量时间。合并掉一部分碎片算子,并让多个独立算子形成流水线后,延迟降了将近一半。性能问题永远要带着计数器找证据,不要凭感觉猜。
5.3 启动流程与固件设计:最容易翻车的地方
芯片上电后能不能板利跑起来,启动流程是关键。常见启动链路是:BootROM → 片外启动设备(QSPI Flash或eMMC) → 引导加载程序 → 固件 → 操作系统 → 驱动。链路里任何一环出问题,表现都是"上电没反应"或者"反复复位"。
看门狗芯片在这里的作用很容易被忽视。如果固件启动时间超出看门狗超时周期,芯片会被强制复位,形成启动循环。之前调一块板子,系统总是启动到一半就重启,排查到最后,是看门狗芯片的超时配置太短,而新固件因为初始化了很多外设,启动时间刚好卡在临界点。把看门狗超时调大,并且在固件里增加了"心跳喂狗"逻辑后才稳定。
电源时序是另一个高频问题。AI芯片一般有多路电源域,核心电压、IO电压、内存接口电压,上电顺序有严格要求。很多外围的升压电源芯片配置不对,会导致某些模块上电不完整,芯片在进入某个深度计算时突然掉电重启。排查这种问题,最实用的工具是示波器抓各路电源的上电时序,和芯片手册上的要求逐一核对。别小看这些"不智能"的环节,它们恰恰是芯片项目延期的主要原因之一。
6. 几个个人实践中的习惯,分享给你
做了几个项目之后,我逐渐养成了一些未必写在教科书里的习惯,分享出来供参考。
第一个习惯是"软件团队从第一天就进场"。硬件团队做架构探索时,编译器团队的负责人就应该坐在旁边,一起看工作负载分析,一起定指令集和寄存器接口,而不是等RTL freeze了再介入。成本最低的修改时机永远是架构探索阶段,而不是流片之后。
第二个习惯是"永远有一个可跑通的最小系统"。不管进展到哪一步,都保持一个最小的软件栈可以从模型编译到片上执行,哪怕是只支持一个算子、跑一个最简单的网络。这个最小系统是团队的基准线,任何改动导致它坏了,都能第一时间发现,避免问题越积越多最后无从查起。
第三个习惯是"性能计数器从设计第一版就定义好"。很多团队芯片快流片了才开始想性能分析怎么做,导致计数器布局不合理,调试时发现关键信号根本没引出来。性能计数器是软件调优的眼睛,应该在架构定义阶段就列出需要监测的事件清单,包括计算单元利用率、SRAM读写带宽、DMA等待周期、指令发射停顿周期这些。没有这些数据,优化就是盲人摸象。
最后再强调一句我在多个项目里反复验证过的判断:AI芯片的软硬件设计,本质上是在算力、带宽、存储、编译器复杂度这个四维空间里找平衡点。每一项单独做到极致都不难,难的是让四项在同一个产品里同时成立。培养这种感觉没有捷径,就是多跑数据、多拆模型、多对着性能计数器看真实行为。希望这篇文章能帮你少踩几个我已经踩过的坑。