第一次把一块 AI 芯片的 RTL 代码和配套编译器一起跑通的时候,我才真正明白“软硬件设计”这几个字的分量。外面很多文章习惯把 AI 芯片拆成硬件架构和软件栈两个黑盒分别讲,但实际项目里,架构师和编译器工程师为了一条总线带宽的分配吵上三天三夜,是再正常不过的事。这篇是整个系列的开篇,目标不是教你写 Verilog,也不是带你刷算子,而是先把 AI 芯片的软硬件设计这套底层博弈框架立起来:算力从哪里来、存储墙有多痛、数据流怎么决定架构上限、编译器到底在忙什么、精度和量化为什么是联调的头号天坑。把这个框架理顺之后,你再去看任何具体加速器方案(不管是云端 NPU 还是手机里的 ISP 级 AI 单元),都能快速定位它到底在哪个环节做了取舍、为什么这样做。
系列编号里的“1”也不是随便写的。这个主题内容量很大,第一篇我只会把“骨架”讲清楚,后面的章节再逐个深入:指令集设计、编译器后端、量化算法、性能建模、验证与 bring-up,都会有对应的展开。这一篇适合谁?做算法和上层框架的工程师想了解硬件视角,做嵌入式开发的人想弄明白 NPU 为什么这么难伺候,还有刚转行做 AI 芯片的硬件工程师,我基本默认你们都会来读一读。读完你不需要立刻能设计芯片,但至少能看懂架构文档里那些关键决策背后的原因——这比记住一堆术语有用得多。
1. 为什么我执意把“软件”放在“硬件”前面讨论
1.1 先划定边界:什么样的芯片能叫“AI 芯片”
聊设计之前,必须先把概念范围划清楚。广义上的 AI 芯片,指的是任何面向深度学习或其他机器学习负载做了专门硬件加速的处理器。它不只有 GPU 和 TPU 这两种形态,手机 SoC 里的独立 NPU、车规芯片里的深度学习加速器核、甚至 FPGA 上为某个算法定制的加速逻辑,都算。
不同形态的 AI 芯片,软硬件耦合的深浅完全不同。我用一张表来总结它们的差异,这张表之后整个系列都会反复用到:
| 芯片类型 | 典型场景 | 软件栈形态 | 软硬件耦合程度 |
|---|---|---|---|
| 通用 GPU | 数据中心训练、大规模推理 | 通用指令集 + 运行时库 | 中等,硬件相对透明 |
| 定制云端 NPU/TPU | 超大模型训练、云原生推理 | 专用编译器 + 运行时,深度绑定 | 极高,芯片与编译器共同进化 |
| 嵌入式 NPU | 手机、摄像头、车机 | 推理引擎 + 量化工具链 | 高,runtime 直接控制硬件 |
| FPGA 加速器 | 实验室原型、快速迭代验证 | HLS 或定制 RTL + 部分软件栈 | 中高,但重构成本高 |
这张表里最有意思的是云端 NPU 那行。它和 GPU 最大的区别在于,GPU 有几十年的软件生态积累,硬件再复杂,编译器也有充足的历史经验去适配;而定制 NPU 往往是一颗芯片和一套编译器同时从零开始,任何一边出了问题,另一边寸步难行。所以定制 NPU 的软硬件设计,本质上不是在“设计两个组件”,而是在“设计一个系统”——这个认知是整个系列最底层的地基。
1.2 算力提升的三条腿,只有一条真正需要软件栈
很多人觉得 AI 芯片算力强是因为硬件拼得凶。这个说法只对了一半。芯片的有效算力可以用一个很朴素的公式来表达:
有效算力 = 峰值 MAC 数量 × 实际利用率
峰值 MAC 数量,也就是芯片每秒能做多少次乘法累加操作,由硬件规模决定。这个数字可以通过两条腿提升:一是更先进的工艺,让同样面积塞下更多处理单元;二是堆并行,在架构里放更多计算单元。但这两条腿都有天花板——工艺越往先进节点走,设计和流片成本呈指数级上升;无脑堆并行也不行,因为功耗墙和内存墙会同时压过来。
第三条腿才是软硬件协同真正发挥价值的地方:让已有的计算单元尽量不空闲、不空转、不因等待数据而浪费时钟周期。这也就是实际利用率。利用率靠什么提上去?靠编译器把算法负载映射得足够聪明,靠数据搬运调度足够精准,靠存储层级设计配合得当。这一环里,硬件架构必须被软件看得懂、控得住、用得好,这就是为什么我得把“软件”放在“硬件”前面来讨论。
1.3 软硬件协同的本质:一起制定接口,而不是硬件完工后再等软件适配
我在不少项目里见过一种惯性思维:先定架构、出 RTL、流片回来,然后把编译器团队叫过来,“你们开始适配吧”。这是典型的串行开发,几乎必然导致芯片可用率低下。因为硬件一旦定型,指令集、存储布局、数据流机制、同步模型就全部锁死了,编译器只能在硬件给的小框框里“戴着镣铐跳舞”。有些算法映射不到合理的效率,不是软件工程师能力不行,而是硬件压根没有给软件留口子。
真正的软硬件协同设计,是在架构定义阶段就让两边坐在一起,共同确定三件事:第一,指令集抽象层,硬件提供哪些粗粒度的加速指令,这些指令背后暴露多少数据移动细节给编译器;第二,存储模型,片上各级缓存的容量、带宽、分配策略怎么定,才能让编译器做出可预期的调度;第三,精度处理约定,量化格式、舍入模式、溢出行为由谁负责,硬件的计算单元和软件的量化器必须对同一种行为有完全一致的理解。
这三件事里任何一件没对齐,后面就是无休止的返工。我在实际项目里见过最典型的一个例子:硬件团队用了很激进的低比特压缩来省带宽,但没有把压缩格式的具体规约提前同步给编译器团队,结果编译器生成的调度序列完全没利用压缩数据通道,带宽瓶颈纹丝不动。两边各花了两周才发现问题出在“接口约定”上——这不是某一个是代码写错了,是整个协同流程缺了共同设计这一环。
2. 所有 AI 芯片的“算数心脏”:MAC 阵列与数据流
2.1 一个 MAC 能做什么,以及为什么把它排成阵列
深度学习计算的基础是矩阵乘法和卷积,这两种运算最底层的操作就是乘累加,英文叫 MAC(Multiply-Accumulate)。一个 MAC 单元做一件事:读进两个数(比如一个输入像素和一个权重),相乘,然后累加到一个输出变量上。单个 MAC 每秒做不了多少事,但把几百上千个 MAC 单元排列成二维阵列,在同一个时钟周期里并行执行成百上千次乘加,就是 AI 芯片算力的物理来源。
以矩阵乘法为例:如果芯片有 16×16 个 MAC 单元组成的阵列,那么理论上每个时钟周期能完成 256 次乘加。实际能不能做到,取决于数据能不能及时够喂到每个 MAC 单元嘴边。这里的关键就不在 MAC 本身了,而在排列方式和数据流动方向。设计 AI 芯片的人花最多精力的地方,往往不是怎么让 MAC 算得更快,而是怎么让 MAC 旁边的数据永不间断。
MAC 单元的物理实现本身也有讲究,特别是对于整数和浮点两种算术。整数 MAC 逻辑简单、面积小、功耗低,适合量化的端侧推理芯片;浮点 MAC(尤其是 BF16、FP16)逻辑复杂、面积大,但动态范围好,适合训练场景。硬件团队在流片前就要和软件团队确认清楚:推出的产品到底是面向训练还是推理,目标精度是多少,这直接决定了 MAC 单元的数据位宽和算术实现。架构师如果拍脑袋选了高精度计算单元,可能性能能力受影响不大,但芯片面积和功耗会成倍上涨,软硬件协同从一开始就输在了成本线上。
2.2 内存墙:算一个数只要一纳秒,拿一个数可能上百纳秒
AI 芯片领域有一句名言:计算已经不是瓶颈,数据搬运才是。这句话具体到芯片物理实现,用一组量级对比就能说清楚。在典型的老工艺条件下,一次乘加运算消耗的能量可能只相当于从寄存器读一次数据,而去 SRAM 里读数据要贵一个数量级,到外部 DRAM 里去拿数据又要贵一个数量级。不同工艺数值不同,但“访存远比计算贵”“离计算单元越远越贵”这个结论非常稳定。
这就是所谓的“内存墙”。芯片的算力可以靠堆 MAC 阵列线性增长,但内存带宽和访存延迟没法同步线性增长。冯诺依曼架构天然让数据处理单元(CPU/加速器)和存储单元之间那条通道成为瓶颈。AI 芯片的任务,本质上就是尽量绕开这条狭窄的外部通道,让计算尽可能发生在数据“家门口”。
绕开的手段总结下来就两个:一个是复用,同一个权重或特征图数据在片上多算几次,减少从外部内存读取的次数;另一个是预取,软件栈提前把即将用到的数据搬到片上,别让计算单元干等着。复用怎么做由硬件的数据流模式决定,预取怎么做由编译器调度和 DMA 传输决定。这两件事必须配合,而且必须在设计阶段就配合——这就是为什么数据流设计是软硬件协同的交叉点。
2.3 数据流模式:权重固定、输入固定、还是输出固定
谈到 AI 芯片架构,你一定会听到几个术语:Weight Stationary(权重固定)、Input Stationary(输入固定)、Output Stationary(输出固定)。它们是数据流模式,直接描述 MAC 阵列里的数据以谁为主“驻留”在片上。
- Weight Stationary(WS):权重数据先被搬到片上,然后尽量驻留在本地寄存器或缓存里,特征图数据流经阵列,在多个 MAC 单元之间共享同一份权重。卷积操作非常吃这种模式,因为同一份卷积核权重要被所有输入位置复用,权重固定的时间越长,外部访存次数越少。
- Input Stationary(IS):特征图数据驻留在片上,权重数据流式通过。适合那些输入特征图单个体量大、复用度也高的网络结构,能减少输入数据的反复搬运。
- Output Stationary(OS):最看重的是部分和(partial sum)的累积。矩阵乘法或者全连接层在计算过程中会产生大量中间累加值,如果这些累加值动不动就往外部内存写,带宽会瞬间被打爆。输出固定模式让部分和尽量待在 PE 内部,攒够了再统一写回。
实际芯片几乎不会只采用单一数据流,而是在不同计算阶段切换不同模式。为什么这么设计?因为智能模型的结构差别太大——卷积层权重复用强、全连接层部分和累积压力大、某些算子输入数据体量大。架构师在设计硬件时如果不给编译器提供切换数据流模式的能力,编译器就只能在单一固定模式下做调度,效率损失非常大。
我之前参与过一个云端推理加速器项目,最初硬件只支持 Weight Stationary,编译器怎么优化,跑 Transformer 类模型时利用率最多也就 40%。后来架构加了 Output Stationary 的旁路支持,让全连接层和注意力矩阵乘的部分和能留在片上累积,Transformer 模型的利用率直接翻了一倍。这不是硬件性能变好了,而是硬件给了软件更合适的“姿势”,软件才能把活干顺。
2.4 一个直观的映射例子:把大矩阵乘法切到小阵列上
光说概念不落地会显得空,我们来看一个具体映射问题。假设 MAC 阵列是 16×16,现在要计算一个 256×256×256 的矩阵乘。直觉上这就是把 256 维循环三次展开到 16×16 阵列里,但实际远没这么简单。256 维的数据必须切成多个 16×16 的块,分时送入阵列计算;每块算完产生的部分和要么留本地做累加,要么流到下一级缓冲;切块的形状还受片上 SRAM 容量的限制。
编译器在这个过程里做的事情,本质上是一个三维拼图:最大化数据复用、最小化数据搬运、同时满足硬件存储容量的约束。拼图能力强不强,直接决定芯片利用率。硬件给编译器提供的“拼图块”是什么?是存储层级的分块大小、是 DMA 通道的数量、是寄存器文件的端口数。这些硬件参数必须和编译器的分块算法联合调优,而不能各定各的。
换句话说,MAC 阵列的规模决定芯片理论算力上限,数据流模式决定它能发挥出几成,而编译器映射决定了最终落地的性能。这三层环环相扣,任何一层掉链子,用户拿到手里的“AI 算力”都会大打折扣。
3. 存储系统与数据搬运:算得再快,也得端水端平
3.1 三级存储体系:寄存器、片上 SRAM、外部 DRAM
AI 芯片内部几乎都有三级存储体系。最靠近 MAC 阵列的是寄存器文件,容量最小、速度最快,每个时钟周期都能被运算单元取数;中间是片上 SRAM 缓冲区,容量在几百 KB 到几十 MB 之间,用于驻留权重块、特征图像块和中间结果;最外面是外部 DRAM,容量大但带宽有限,访存延迟比片上高出几个量级。
很多初学者误以为有了大容量 SRAM 就万事大吉,实际恰恰相反。SRAM 占的面积并不便宜,设计者必须在容量和面积之间找平衡。而且 SRAM 不是容量大就好用,它还得切分成多个 bank,让不同处理单元能并行访问不同 bank,避免访问冲突。编译器做数据调度时,最头疼的就是 bank 冲突——两个处理单元恰好同时访问同一个 bank,其中一个就必须等一拍,这一拍就是有效算力的流失。
所以我常跟硬件团队说,存储系统的设计目标不是“有多少缓存”,而是“缓存能不能被软件以可预期的方式利用”。为了达到这个目标,存储分块的几何尺寸、bank 的数量、总线宽度这些参数提前就得和编译器的 tile 尺寸对齐。就拿输出通道数来说,如果片上 SRAM 分块设计时刚好不会是输出通道分块大小的整数倍,编译器每次切数据都会残留一段碎片,带宽利用率明显下滑。
3.2 双缓冲与多级流水:用空间换时间的老手艺
数据搬运和计算之间最常见的“堵车”是:计算单元算完当前数据块,需要等下一块数据从外部搬进来。解决办法是双缓冲(double buffering)——设置两个缓冲区,一个给当前计算用,一个给 DMA 预取下一块数据用。计算和搬运在时间上重叠起来,计算单元几乎不用停下来等数据。
多级流水更进一步,把“从 DRAM 搬到 L2”“从 L2 搬到缓冲区”“从缓冲区搬到寄存器”这几段数据路径全部流水化。每一级都有各自的缓冲和同步机制,软件栈负责向每一级发出预取指令,让整个链路就像一条装配线,数据从最外侧一步步被送到 MAC 嘴边。
双缓冲听起来简单,落地时却容易出问题。最典型的是同步逻辑没处理干净:计算单元已经在消费缓冲区 A,DMA 引擎却以为 A 已经算完,开始往 A 里覆写新数据,结果算出来结果全是错的。这种 bug 属于软硬件边界问题,往往要在芯片和编译器两边同时查才能定位——硬件测说 DMA 时序没问题,软件测说调度逻辑没问题,最后发现是两边的“缓冲区状态标记”协议不一致。
3.3 DMA 与描述符:软件搬运数据的“合同文件”
谁来控制这些数据搬运?AMD/NVIDIA 那种大芯片有复杂的存储管理单元,但很多 NPU 用的是更轻量的 DMA 引擎。软件侧通过一组数据结构——通常叫描述符(descriptor)——向 DMA 引擎描述一次数据搬移任务的详细信息:源地址、目标地址、搬运长度、源和目标的数据步长、是否需要在搬运完成后发起中断。一批描述符串起来形成一个链表,DMA 引擎按顺序执行,软件每填一次描述符就是在签一份“搬运合同”。
这份合同里的细节比想象中多。比如源地址和目标地址的对齐规则、burst 长度的选择、是在传输层做转置还是搬完再转置、以及多路 DMA 通道之间的优先级仲裁。这些细节如果设计师没有在架构定义阶段和软件确定下来,编译器生成的描述符要么吞吐不够,要么格式对不上,只能在验证阶段打得头破血流。
我在这里必须强调一点:AI 芯片里的“可编程性”不只是指通用计算单元的能力,更指这些数据搬运路径能不能被软件精确控制。往往一个芯片计算单元没什么特别,但 DMA 通道多、描述符结构灵活、调度逻辑清晰,软件优化空间就很大。反过来,堆了一堆 MAC 阵列但 DMA 能力很抠门,那这芯片基本就是纸面算力巨人、实际吞吐侏儒。
4. AI 芯片的编译器:软硬件协同设计的主战场
4.1 编译器不是“翻译官”,而是“调度总监”
很多人把编译器理解成把高级语言翻译成机器码的翻译官,但在 AI 芯片领域,这个理解远远不够。神经网络编译器的工作更像一个调度总监——它读入模型的计算图(比如卷积、全连接、激活、归一化这些算子组成的图),然后做一系列规划:哪些算子可以融合成一个内核、数据切成多大块、按什么顺序在计算单元上执行、哪些数据应该预先搬到哪一层存储、哪些可以复用不重复搬。
算子融合是最经典也最有效的一招。举例来说,卷积后面经常跟着批量归一化(BatchNorm)和 ReLU 激活。如果不融合,卷积输出要完整写回外部内存,BatchNorm 再读出来跑一遍,算完写回,ReLU 又要读一遍。一份数据三趟访存,带宽压力直接拉满。如果编译器把这三级融合成一个内核——卷积核内部做完乘累加,紧接着做归一化和激活,原始数据在片上走完所有流程——外部数据搬运量瞬间缩小好几倍。
算子融合涉及硬件能不能提供这种“连续作战”的能力。硬件如果每个算子之间都强制要求数据落内存,编译器再聪明也白搭。我见过某款芯片专门为算子融合设计了内部数据旁路,让一个 PE 的输出直接交给下一个 PE 做后处理,不用绕道 SRAM。软硬件在这个点上对齐,整个推理引擎的端到端性能肉眼可见地提升。
4.2 指令集设计决定编译器能不能“看懂”芯片
指令集是硬件向编译器承诺的“接口契约”。通用 CPU 的指令集大家都熟悉,什么 load、store、add、branch。AI 芯片的指令集则往往分成两类:一类是标量控制指令(控制流、循环计数、同步等待),另一类是粗粒度的张量指令(一次执行一堆矩阵运算、卷积运算、数据搬运指令)。这两类指令各有各的职责,编译器需要把它们编排成高度重叠的执行流。标量指令负责“剧情推进”,张量指令负责“干粗活”,数据移动指令负责“调运粮草”。
设计指令集时最容易犯的错误是过于复杂、过于专用。有的硬件团队恨不得为每一个后端算法定制一条指令,结果编译器代码里堆满了补丁,每条新指令都要配套特殊处理逻辑;模型一变,旧指令又用不上了。好的 AI 芯片指令集应该像乐高积木:基础指令有清晰的语义边界,组合起来能覆盖大范围模型结构,编译器能从少数原语中生成出各种需要的执行序列。
另外,指令集层面一定不要忽略同步和依赖语义。NPU 里有多个引擎(计算引擎、搬运引擎、后处理引擎)并行工作,指令之间什么时候能重叠、什么时候必须等待,编译器必须非常清楚。硬件在指令集里如果不同步机制表达得含糊,编译器只能做保守处理——大量插入等待指令,并行度大打折扣。
4.3 运行时、驱动与推理引擎的边界到底在哪
一个完整的 AI 芯片软件栈除了编译器,还有三块:驱动、运行时、推理引擎。它们的职责必须划分清楚,否则软件栈就是一锅粥。
驱动其实是整个芯片团队的“老黄牛”角色。它管理设备初始化、内存分配、电源状态、硬件状态配置。驱动足够出色时,上层运行时甚至不关心底层是 NPU、GPU 还是别的加速器。很多项目把驱动该干的活和运行时该干的活混在一起,结果哪天换了芯片版本,一行驱动代码的改动影响到了上层推理框架的接口,版本兼容性立刻变成噩梦。
运行时(Runtime)负责任务级别的调度:加载编译产物,管理输入输出缓冲、执行队列、同步事件。推理引擎则是面向用户的那一层,负责模型的解析、前处理、执行和后处理,接口通常比较稳定。我们在设计时坚持一条原则:推理引擎只是编译器和运行时的“调度员”,它不应该关心底层硬件的字节对齐、bank 冲突、量化尺度之类的细节。
真正成熟的 AI 芯片软件栈,会有清晰的层次划分和接口规范。编译器输出某种硬件无关的中间表示(比如经过优化的计算图和指令序列),运行时把中间表示翻译成硬件特定的调度命令,驱动再把它落到寄存器级操作。每一层各管一段,出了问题就能快速定位到具体层,而不是整个团队陷入“底层 bug 和上层 bug 互相甩锅”的死循环。
4.4 硬件无关抽象与硬件相关映射的张力
这里值得单独拿出来讲一个哲学问题:编译器应该在多大程度上暴露出硬件特性给上层。完全隐藏硬件细节,上层框架很好写,但性能一定上不去;完全透明,性能有可能做到极致,但任何上层改动都可能触发硬件特性相关的坑,软件栈非常难维护。
折中方案是分层:图优化层做到硬件无关,不管你后端是什么加速器,算子融合、算子消除、常数折叠这些优化都是通用的;到具体的硬件映射层,再结合目标芯片的存储层级、数据流能力和指令集细节做深度优化。这种分层让 AI 芯片软件栈具备了基础的可移植性:换一颗新的 NPU,编译器的前端和图优化不用重写,只需为新的后端编写一个可映射的接口层。
硬件团队必须明白,给编译器暴露的“开关”越多,软件栈的适配工作量越大。比较稳妥的做法是只暴露少量高杠杆的控制点:存储层级容量和带宽、计算阵列规模、数据流模式选择、内存地址空间布局。这些控制点能覆盖绝大多数性能优化空间,又不至于让软件团队陷入硬件的每一个微末细节。
5. 精度与量化:软硬件协同中最容易翻车的环节
5.1 数据精度到底选多少位:FP32、FP16、BF16 还是 INT8
深度学习模型在训练时通常用 FP32,因为梯度下山的每一步都需要较高的数值精度;推理阶段则普遍追求更低比特的表示,以换取带宽下降、吞吐上升。对 AI 芯片来说,选择精度不只是算法的事,直接决定硬件 MAC 单元的实现复杂度、缓存容量和带宽需求。
FP16 有较高的尾数位;BF16 和 FP32 有相同的动态范围,但被压缩到更少的尾数,更新宽范围的数值不太会溢出;INT8 则是定点表示,只有整数语义和固定的缩放因子,动态范围比浮点小得多。硬件实现里,浮点 MAC 比整数 MAC 复杂很多;但在很多推理场景,合理的 INT8 量化能保持精度损失在可接受水平,同时获得“单位功耗下的更高吞吐”。
训练用的芯片几乎必须支持 FP16/BF16,推理芯片则越来越多主打 INT8 甚至 INT4。软硬件协同在这里的命题是什么?硬件如果只支持 INT8,那软件量化就绝对不能把模型数值范围压爆;硬件如果支持了 Flex 精度,软件栈就要有能力对量化尺度做精确建模。两边必须对“一个数是怎么从 FP32 变成 INT8 又变回来”的整个机制有一致的理解。
5.2 训练后量化(PTQ)和量化感知训练(QAT):各吃各的苦
量化和浮点推理之间,最常见的是两条落地路径。第一条是训练后量化(Post-Training Quantization,PTQ),拿一个已经训练好的 FP32 模型,直接统计权重和激活的数值分布,算出合适的缩放因子,然后转成 INT8 版本。PTQ 最大的优势是不用重新训练模型,成本低、上线快。但碰上数值分布畸形的层(比如某些激活数值有很长尾),PTQ 容易出现严重的精度回退,这时就要做一些针对性处理:分通道量化、KL 散度校准、逐层验证等。
第二条路是量化感知训练(Quantization-Aware Training,QAT),在训练阶段就模拟量化误差,让网络“适应”低比特表示。QAT 精度通常比 PTQ 高,但代价是需要重新训练模型,迭代周期长,业务方不一定愿意配合。现实中大量端侧推理芯片的主推路径是 PTQ 先行,遇到精度敏感的模型再用 QAT 兜底。
硬件设计者对这两种路径的影响比大多数软件工程师想的大。如果硬件的 MAC 单元对舍入行为、溢出行为和缩放因子计算支持得不够透明,PTQ 校准出来的量化参数在硬件上会产生额外误差;反过来,硬件把这些细节定义得越干净,PTQ 的成功率就越高,项目也就越不需要折腾昂贵的 QAT 重训练。
5.3 硬件与量化器的“一致性”问题
我见过的软硬件协同最大坑,不在架构设计,而在量化的“一致性”。量化本来就是一个损失信息的过程,如果硬件在运行中对同一份 INT8 数据产生的舍入、截断行为跟软件端模拟器预期的不完全一致,那所有在模拟器上验证过的精度数字都变成了一张废纸。这个问题通常要等到芯片回来、上板实测时才会暴露,而那时候任何硬件改动都已经来不及了。
正确的做法是在设计阶段就让量化规范成为硬件的“契约”。硬件团队要明确:加法累加中间结果的位宽是多少、每层累加结束后的舍入模式是什么、溢出是饱和还是回绕、缩放因子是每个张量统一还是每个通道独立。软件量化器要严格按照这套契约做校准和模拟,保证“软件模拟的硬件行为”与“真实硬件行为”逐比特一致。没有这份契约,量化精度验证就无从谈起。
6. 跑通“纸上架构”到“真实芯片”之间的那几道坎
6.1 性能模型:第一批软硬件决策的试金石
芯片设计周期动辄两三年,不可能等到 RTL 写完了才去看运行效能。业界通用做法是提前搭一套性能模型(Performance Model),在架构还没定稿时就估算各种工作负载(模型结构、算子组合、数据规模)在候选架构上的表现,作为软硬件共同决策的依据。
性能模型要做到能评估的不仅是理论峰值算力,更重要的是实际执行流水线。它要模拟多个引擎并行工作时的冲突、DMA 的搬运时延、存储 bank 的访问冲突等,这样软件团队才能提前知道某类调度策略的效果,硬件团队才能提前知道哪个模块会成为瓶颈。我见过太多乙方项目,性能模型做得太粗,直接假设数据搬运永远不跟计算冲突,估算性能翻了一倍,实际硅片回来差一大截,项目直接被动。
6.2 接口版本化:让软件不被硬件迭代拖死
软硬件协同还有一个常被忽视的工程化管理问题:版本的演进。AI 模型一年一个样,芯片不可能两年完全不变。硬件每次迭代,指令集和接口规范必然有变化,但软件栈要尽量保持兼容。业界成熟的 NPU 团队都会给自己制定接口版本策略:把对外的指令集和通用接口定为“稳定基线”,把新增能力做成可选的扩展接口;编译器针对新接口生成高性能代码,同时保留对老接口的兼容。
这个动作看着像是软件过程管理,根子上偏偏是硬件设计的学问。硬件在定义寄存器地址空间和指令编码时,就得预留一部分扩展位,不能一出新版本就把老指令格式推倒重来。架构师如果只管当前一代芯片,不做版本演进规划,那从第二款产品开始,软件团队就等着还债吧。
6.3 端到端验证:别只盯着合成基准测试
最后聊聊验证。很多 AI 芯片流片回来,硬件测试团队喜欢跑一个标准矩阵乘,看着数字很兴奋;但一颗芯片要能真正“上车”,必须跑完整模型端到端验证,包括混合精度、多个算子交替、动态输入尺寸、各种内存碎片场景。端到端验证阶段往往才是软硬件协同真正露馅的时候——合成负载的测试覆盖不到真实模型里的长尾问题,模型一变,隐藏的瓶颈全冒出来。
所以我现在更推荐尽早把软硬件联调的环境搭起来:芯片开发时就把整个软件栈框架、编译器、运行时全部对齐,甚至在性能模型阶段就选择真实模型中的代表性子集做“软硬件联合仿真”,而不是只做合成 kernel。验证越靠前,返工的代价越小;真等硅片回来的那天再开始软硬件磨合,时间成本和心理压力都很大。
6.4 踩坑后的几点掏心窝建议
按惯例,最后给几条基于实际项目的建议,希望后面做同类项目时能少走一点弯路。第一,软硬件两边在架构定义阶段就共用一份“接口契约”文档,任何改动都要走评审,不允许任何一方单方面修改数据流、指令语义和精度行为。第二,性能模型不要做得敷衍,哪怕先只模拟关键算子,也要把数据搬运和计算重叠的建模函数做出来,这是所有优化讨论的基础。第三,量化规范不要到流片前才开始定,它跟指令集一样属于核心接口——一份藏在供应商驱动文档里的量化细节,往往是许多联调事故的精神源头。
我自己做软硬件协同设计的体会是,真正优秀的 AI 芯片工程师往往不是纯硬件天才,也不是纯系统天才,而是能同时看懂 RTL 和编译器代码、能陪着软件团队一起调算子性能、能为一行访存优化去改架构侧内存映射的人。AI 芯片的软硬件设计,不是两个团队的结合,而是同一种能力在不同抽象层上的贯通的落地实践。希望这个系列的第一篇能帮你在心里搭起这个框架,剩下的细节,我们下一篇挑一场硬仗来打。