1. 从一次流片失败聊起:AI芯片软硬件协同到底难在哪
前两年跟一个做推理加速卡的朋友吃饭,他跟我吐槽说他们第一版芯片回来之后,实测性能只有仿真数据的六成,功耗还超了预算的三成。问题出在哪?不是RTL写错了,也不是后端布局布线崩了,而是算法团队在模型侧做的一次算子融合优化,硬件端根本不知道——软件以为硬件会按某个顺序执行,硬件按自己的流水线节奏跑,两边对不上,DMA搬运次数比预期多了一倍,带宽直接成为瓶颈。
这件事特别典型。AI芯片跟传统SoC最大的区别就在于:它的价值几乎完全取决于软硬件配合的紧密度。你做一个通用CPU,指令集定好了,编译器生态跟上就行,硬件微架构对上层基本透明。但AI芯片不一样,它的指令集、数据流、存储层次、甚至片上网络的拓扑结构,都是为特定类型的计算模式(卷积、矩阵乘、注意力机制)量身定制的。一旦软件栈没有把硬件的特性吃透,或者硬件设计时没有充分考虑软件调度的灵活性,性能就会断崖式下跌。
所以这篇内容我想聊的是:AI芯片的软硬件设计到底应该怎么协同考虑。不是泛泛地讲“软硬件协同设计很重要”这种废话,而是从架构选型、数据流设计、编译器配合、算子映射、存储层次规划这几个具体维度,把我在实际项目和跟同行交流中积累的经验和踩过的坑,尽量讲透。适合正在做AI加速器架构设计的工程师、做推理框架优化的算法工程师,以及想理解AI芯片全栈设计逻辑的技术管理者。不管你是刚入行还是已经做过几颗芯片,应该都能从中找到一些可以直接参考的思路。
2. 架构选型:为什么你的第一版AI芯片不该追求通用性
2.1 从应用场景倒推架构,而不是反过来
我见过太多团队在立项时的第一反应是:“我们要做一个能支持所有主流模型的AI加速器。”这个想法听起来很合理,但实际上是灾难的开始。为什么?因为“支持所有模型”意味着你的硬件必须足够灵活,而灵活性在芯片设计里是用面积、功耗和频率换来的。你为了支持各种奇怪的算子,加了一堆可配置的通路和缓冲区,结果就是核心计算单元的利用率上不去,PPA(Power, Performance, Area)全面崩盘。
正确的做法是:先锁定一到两个核心应用场景,把这类场景的计算模式吃透,然后针对性地设计数据流和计算阵列。比如你做的是边缘侧的视觉推理,那主要负载就是卷积和少量的全连接,卷积核尺寸集中在3x3和1x1,输入分辨率不会太大。这种情况下,你可以放心地做一个固定尺寸的脉动阵列,把权重缓存和特征图缓存的比例调到一个经过验证的黄金值,不需要考虑太多动态重构的问题。
反过来说,如果你做的是云端训练芯片,那矩阵乘的尺寸变化范围极大,从几十到几千都有可能,这时候你就必须考虑分块策略的动态可配置性,以及片间互联的带宽扩展能力。这两种场景对硬件的要求几乎是正交的,想用一颗芯片通吃,结果往往是谁都吃不好。
2.2 数据流的选择决定了整个芯片的骨架
AI芯片设计里有一个核心决策:数据流(Dataflow)怎么选。常见的有权重固定(Weight Stationary)、输出固定(Output Stationary)、行固定(Row Stationary)这几大类。这个选择不是学术问题,它直接决定了你的片上存储怎么划分、PE阵列怎么连接、甚至编译器要怎么写。
我拿一个实际案例来说明。假设你要做一个16x16的脉动阵列,做3x3卷积。如果选权重固定,那每个PE里都要存一份权重,权重从片外加载一次之后就不动了,特征图在阵列里流动。这种方案的好处是权重复用率极高,适合batch size比较大的推理场景。但缺点是,如果模型换了,权重全变了,你得重新加载整个阵列的权重,这个加载时间在切换模型时是不可忽略的。
如果选输出固定,那每个PE负责计算一个输出像素的部分和,特征图和权重都在阵列里流动。这种方案对卷积核尺寸变化比较友好,但部分和需要在PE之间传递,对互联网络的压力更大。
实操心得:如果你不确定选哪种数据流,可以先在Excel里做一个简单的建模,把你要支持的主要算子的数据复用次数算出来,然后对比不同数据流下的片外访存量。这个建模不需要多精确,但能帮你快速排除明显不合理的方案。
2.3 指令集设计:别让编译器替你擦屁股
很多AI芯片团队在定义指令集时,喜欢把指令做得特别“语义化”,比如一条指令直接完成一个卷积层。这种CISC风格的指令看起来很美,编译器简单,但实际落地时会发现两个致命问题:一是硬件解码逻辑极其复杂,时序很难收敛;二是灵活性极差,遇到一个没预料到的算子组合就得改硬件。
我的建议是:指令集尽量RISC化,把复杂的控制逻辑交给编译器。硬件只提供最基础的原子操作:加载、存储、矩阵乘、激活、池化。然后通过编译器的调度把这些原子操作组合成完整的算子。这样做的好处是硬件设计简单、频率容易做高,而且新算子可以通过软件快速支持,不需要重新流片。
当然,这要求你的编译器团队足够强。但话说回来,做AI芯片本来就是一个软硬件深度耦合的事情,编译器弱的话,硬件再强也发挥不出来。
3. 存储层次设计:AI芯片真正的性能瓶颈所在
3.1 为什么计算单元不是瓶颈,访存才是
我经常跟人说,AI芯片的设计里,计算单元的设计是最简单的部分。你做一个256x256的MAC阵列,只要面积和功耗允许,堆上去就行了。真正难的是:怎么让数据及时地送到这些计算单元面前。
有一个经验公式:在典型的AI推理负载中,每做一次MAC运算,需要从片外读取大约0.1到1字节的数据。这意味着如果你的芯片算力是100 TOPS,那访存带宽需求就是10到100 TB/s。这个数字是什么概念?目前最先进的HBM3e,单堆栈带宽大约是1.2 TB/s,你需要几十个堆栈才能满足。这显然不现实。
所以AI芯片设计的核心矛盾就是:算力增长的速度远远快于访存带宽增长的速度。解决这个矛盾的唯一办法就是提高数据复用率,让数据在片内多停留、多复用,减少对片外带宽的依赖。
3.2 片上存储的分层策略
一个典型的AI芯片存储层次是这样的:
| 层级 | 典型容量 | 访问延迟 | 带宽 | 主要用途 |
|---|---|---|---|---|
| 寄存器文件 | 几十KB | 1 cycle | 极高 | PE内部操作数 |
| 片上SRAM | 几MB到几十MB | 几cycle | 很高 | 特征图/权重缓存 |
| 片外DRAM | GB级别 | 几百cycle | 有限 | 模型参数/中间结果 |
设计的关键在于:怎么划分SRAM的用途。是全部给权重?还是全部给特征图?还是按比例分?这个比例不是拍脑袋定的,要根据你的目标算子的算术强度(Arithmetic Intensity)来算。
算术强度 = 计算量 / 访存量。以3x3卷积为例,假设输入通道64,输出通道64,特征图尺寸56x56。计算量是56x56x64x64x3x3 ≈ 1.16亿次MAC。访存量呢?输入特征图是56x56x64 ≈ 20万字节,权重是64x64x3x3 ≈ 3.7万字节。算术强度大约是1.16亿 / 24万 ≈ 483 MAC/Byte。这个强度很高,意味着数据复用空间很大,你可以把权重和部分特征图放在SRAM里,大大减少片外访问。
但如果是1x1卷积,算术强度会低很多,因为输入通道和输出通道之间的复用减少了。这时候你就需要更大的SRAM来缓存特征图,或者接受更高的片外带宽需求。
3.3 双缓冲和预取:隐藏访存延迟的必备技巧
即使你有了足够的SRAM,如果访存和计算是串行的,那计算单元还是会有大量空闲时间。解决办法就是双缓冲(Double Buffering):把SRAM分成两块,一块用于当前计算,另一块用于预取下一批数据。当计算单元在处理当前数据时,DMA引擎在后台把下一批数据搬进来。
这个技术听起来简单,但实操中有几个坑:
- 缓冲区大小要匹配计算时间。如果预取的数据量太大,DMA搬不完,计算单元就得等;如果太小,DMA频繁启动,开销又上去了。一般建议预取缓冲区的大小等于计算单元处理一个完整tile所需的数据量。
- 同步机制要设计好。双缓冲需要硬件信号量或者中断来协调DMA和计算单元的节奏。如果同步没做好,很容易出现数据覆盖或者死锁。
- 边界情况要处理。当特征图尺寸不能被tile大小整除时,最后一个tile的数据量会少一些,预取逻辑要能正确处理这种不规则的边界。
注意事项:双缓冲的SRAM面积开销是单缓冲的两倍。如果你的SRAM本来就紧张,可以考虑用更细粒度的流水线来替代,比如把一次大搬运拆成多次小搬运,用更小的缓冲区实现类似的效果。
4. 编译器与硬件的握手:算子映射和调度策略
4.1 算子映射不是简单的“一对一”
编译器团队和硬件团队最容易出现分歧的地方就是算子映射。硬件团队觉得“我提供了这么灵活的PE阵列,你编译器应该能自动映射所有算子”,编译器团队觉得“你的硬件限制太多,很多算子根本映射不上去”。
这个矛盾的根源在于:硬件设计时没有充分考虑算子的多样性。比如,你设计了一个专门优化3x3卷积的脉动阵列,但模型里突然出现了一个5x5的深度可分离卷积,你的阵列尺寸对不上,怎么办?要么硬件支持动态重构,要么编译器得把5x5拆成多个3x3来算,但这样会引入额外的数据搬运和部分和累加开销。
我的经验是:在硬件设计早期,就让编译器团队介入,把目标模型集合里的所有算子列出来,逐个分析映射方案。如果某个算子映射效率特别低,要么改硬件,要么在模型侧做等价变换(比如把5x5卷积用两个3x3卷积近似,精度损失在可接受范围内)。
4.2 调度策略:先分块,再流水
编译器的调度策略直接决定了硬件的利用率。一个典型的调度流程是这样的:
- 算子融合:把连续的卷积、BN、激活融合成一个计算图节点,减少中间结果的写回。
- 分块(Tiling):把大的特征图和权重切分成适合SRAM容量的小块。
- 流水线编排:确定每个块的加载、计算、写回顺序,尽量让DMA和计算单元并行工作。
- 指令生成:把编排好的操作序列翻译成硬件指令。
这里面最考验功力的是分块策略。块太大,SRAM放不下;块太小,数据复用率低,片外访存多。我一般会建议用一个简单的搜索算法:以SRAM容量为约束,以片外访存量最小为目标,遍历可能的块尺寸组合,找到一个近似最优解。这个搜索不需要很精确,因为实际运行时还有双缓冲和预取来兜底。
4.3 量化与编译器的配合
现在做AI芯片,不支持量化基本没法玩。但量化不只是硬件加个INT8乘法器那么简单,它需要编译器和硬件的紧密配合。
举个例子:模型量化后,激活值的范围是动态变化的,每一层都不一样。如果硬件只支持固定的缩放因子,那编译器就得在每层计算前插入一个缩放操作,这会增加额外的指令开销。更好的做法是硬件支持动态缩放,编译器把每层的缩放因子作为参数传给硬件,硬件在累加完成后自动做缩放。这样既减少了指令数,又提高了精度。
实操心得:量化的精度损失往往不是均匀的,某些层对量化特别敏感。我一般会建议在编译器里加一个敏感度分析的pass,自动识别出哪些层需要保留更高精度(比如INT16),哪些层可以用INT8甚至INT4。这个分析可以基于权重的分布和激活值的动态范围来做,不需要跑完整的训练。
5. 实操案例:一个边缘推理芯片的软硬件协同设计过程
5.1 需求定义和约束分析
假设我们要设计一颗用于智能摄像头边缘推理的AI芯片,目标场景是实时目标检测,模型是YOLO系列的一个轻量级变体。约束条件如下:
- 功耗:不超过2W
- 帧率:30fps @ 1080p
- 模型大小:不超过10MB
- 精度:mAP下降不超过2%
从这些约束出发,我们可以倒推出算力需求。YOLO轻量级变体大概需要2-3 GOPS每帧,30fps就是60-90 GOPS。考虑到实际利用率(通常50%-70%),峰值算力需要做到150 GOPS左右。这个算力水平用INT8精度实现,功耗控制在2W以内是可行的。
5.2 硬件架构的确定
基于上面的分析,我们确定以下架构参数:
- 计算阵列:128个INT8 MAC单元,分成8x16的脉动阵列
- 片上SRAM:总共2MB,其中1.5MB用于特征图缓存,0.5MB用于权重缓存
- 数据流:权重固定,因为YOLO的权重在推理过程中不变,可以一次性加载
- 指令集:RISC风格,基础指令包括LOAD、STORE、MAC、ACT、POOL
- 量化:支持INT8和INT16混合精度,敏感层用INT16
这个配置下,理论峰值算力是128 MAC x 2(乘加各算一次)x 1GHz = 256 GOPS。实际利用率按60%算,大约150 GOPS,满足需求。
5.3 编译器栈的实现
编译器这边,我们基于TVM做了一些定制化的工作:
- 前端:支持ONNX和TFLite模型导入
- 图优化:算子融合、常量折叠、死代码消除
- 量化:训练后量化(PTQ),支持逐层敏感度分析
- 调度:基于SRAM容量的自动分块和流水线编排
- 后端:生成我们自定义的指令序列
这里面的关键点是调度器。我们实现了一个基于动态规划的调度算法,以片外访存量最小为目标,自动搜索最优的分块和流水线方案。实测下来,相比手工调优,自动调度在大多数算子上能达到90%以上的性能,只有少数特别复杂的算子需要人工干预。
5.4 实测结果和调优过程
第一版芯片回来之后,实测性能是120 GOPS,功耗1.8W,基本达标。但帧率只有25fps,离30fps还差一点。排查后发现瓶颈在DMA搬运上:权重加载的时间比预期长了20%,因为权重缓存的bank冲突比较严重。
解决办法是调整权重缓存的bank映射策略,把经常同时访问的权重块映射到不同的bank。这个改动在RTL层面只改了几行代码,但效果立竿见影,权重加载时间降到了预期水平,帧率也上到了32fps。
这个案例说明了一个道理:AI芯片的性能调优往往不是计算单元的问题,而是存储和搬运的问题。你在架构设计阶段就要把存储层次和DMA通路考虑清楚,否则后期调优会非常痛苦。
6. 常见问题与排查技巧实录
6.1 仿真性能很好,实测性能腰斩
这是最常见的问题,原因通常有以下几个:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 计算单元利用率低 | DMA带宽不足或调度不合理 | 用性能计数器看DMA和计算单元的活跃周期比 |
| 功耗超标 | 时钟树功耗过高或SRAM漏电 | 分别测量动态功耗和静态功耗 |
| 精度不达标 | 量化误差累积或累加器位宽不够 | 逐层对比浮点和定点输出 |
| 帧率不稳定 | 双缓冲同步有问题或DMA冲突 | 抓取DMA和计算单元的时序波形 |
6.2 编译器生成的代码效率低
编译器生成的代码效率低,通常是因为调度器没有考虑到硬件的微架构细节。比如,硬件支持双缓冲,但编译器没有把加载和计算编排成流水线,导致两者串行执行。
解决办法是在编译器里加入硬件感知的调度pass,把硬件的双缓冲、预取、bank冲突等特性作为约束条件纳入调度算法。这个工作比较繁琐,但一旦做好,性能提升非常明显。
6.3 模型换了之后性能波动大
这个问题在支持多模型的芯片上特别常见。原因是硬件的数据流和存储层次是针对某个特定模型优化的,换了一个计算模式不同的模型,硬件利用率就会下降。
缓解办法有两个:一是硬件设计时留一定的灵活性,比如支持多种数据流模式;二是编译器侧做更激进的算子变换,把新模型的计算模式尽量往硬件擅长的方向靠。但根本上,这个问题没有完美的解决方案,只能通过明确目标场景来规避。
避坑技巧:在芯片立项时,一定要明确“不做什么”。与其做一个什么都能跑但什么都跑不快的芯片,不如做一个在特定场景下性能极致的芯片。AI芯片的竞争已经过了拼通用性的阶段,现在拼的是场景适配度。
7. 一些关于软硬件协同的个人体会
做了几年AI芯片之后,我最大的体会是:硬件团队和软件团队必须坐在一起办公。这不是开玩笑,物理距离真的会影响协作效率。硬件工程师在定义寄存器接口时,编译器工程师在旁边就能立刻反馈“这个接口对调度不友好”;编译器工程师在写调度算法时,硬件工程师能马上指出“这个操作硬件不支持”。
另一个体会是:不要过早优化。我见过一些团队在架构设计阶段就纠结于某个算子的映射效率,花了几个月时间反复修改,结果芯片回来发现真正的瓶颈在别的地方。正确的做法是:先做一个端到端的原型,把整个软硬件流程跑通,找到真正的瓶颈,然后再针对性地优化。
最后,仿真环境一定要尽可能接近真实硬件。很多团队用理想化的仿真环境做性能评估,忽略了DMA延迟、bank冲突、时钟树偏差这些实际因素,结果流片回来发现性能对不上。我的建议是,在仿真环境里加入这些非理想因素的模型,哪怕精度不高,也能帮你提前发现大问题。
这个领域变化很快,新的模型架构、新的存储技术、新的封装方式都在不断涌现。但软硬件协同设计的基本原则是不变的:让数据流动得尽可能少,让计算单元尽可能忙。抓住这个核心,剩下的就是工程上的不断迭代和打磨了。