群里的朋友又在问第七篇的预告什么时候出,说来也巧,这一篇恰恰是整个系列里最“磨人”的一篇。前几篇聊架构微架构、聊内存子系统、聊工具链基础,多少都能从公开资料里找到影子,唯独“AI芯片的软硬件设计”这个主题,必须同时站在芯片和编译器两边,用一套心智模型把两边串起来。这篇文章我尽量不端着,把做项目中真实踩过的坑、想通的关键点、反复权衡的取舍讲清楚。适合正在入门AI芯片设计、刚接触软硬件协同开发、或者准备做异构计算平台选型的工程师参考。
1. 软硬件协同设计的全局观
1.1 为什么软硬件必须一起设计
很多刚接触这个领域的朋友会先入为主,觉得AI芯片嘛,无非是把一堆乘加器排在一起,配上大缓存,再把编译器弄出来,能跑模型就行。真上手做就会发现,硬件一旦流片回来,软件栈还停留在“能点灯”的阶段,整个项目就废了一半。AI芯片的性能最终由软件映射效率决定,这句话听起来像口号,实际上每一条都对应着硬件的真实约束。
我举一个最普通的例子。很多AI加速器在设计时片上SRAM容量定得很大,以为这样就能减少DDR访问。可编译器如果缺乏对整个计算图的全局分析能力,算子粒度就切得碎,中间结果频繁写回DDR,片上缓存利用率很低。这种情况下,硬件上的“豪华配置”根本转化不成实际性能。反过来说,如果编译器足够聪明,可以把连续多个算子的中间张量融合在片上,哪怕SRAM只有几兆,也能跑出比“大缓存但是编译器稀烂”的硬件更好的性能。
所以软硬件协同设计的本质是:硬件架构为软件映射提供“可能性边界”,软件栈则决定在这条边界上能逼近多少。这个边界包括计算阵列的排布方式、数据通路宽度、存储层次结构、同步与流水机制,也包括指令集或者配置寄存器的设计。软件栈必须从硬件寄存器级抽象开始设计,而不是在API层面套一个通用编译框架就万事大吉。
1.2 算力需求如何推导硬件规格
做软硬件协同设计,第一步不是选工艺、定频率,而是先算清楚“目标模型家族”的算力和访存需求。业界常用的是一个非常朴素的公式:
[ \text{所需MAC数} = \text{模型FLOPs} \times 2 / \text{目标功耗时的时钟数} ]
单位时间内的MAC吞吐,直接决定了加速器要放多少个MAC单元;而模型中间张量的尺寸和复用特性,则决定了片上存储的容量和带宽下限。我记得做一个8比特推理加速器时,目标是在10瓦功耗内跑一个主流检测网络。计算下来需要约2000个MAC并行工作,频率目标尽量压到500MHz以下以保证功耗达标。可提到“能不能让编译器把算子融合好、把数据搬移降到最低”时,发现软件团队对硬件的存储结构还不清楚,沟通了整整两周。
这里我强烈建议,在做软硬件协同设计时,先不要急着写RTL,更不要急着做芯片验证。先做一轮“架构-编译器”纸面对齐:把目标模型的每层特征图大小、卷积核大小、通道数资料化,然后推演每一层在硬件上的执行方式、中间数据会出现在哪里、片上存储需不需要回写DDR。这个推演过程消耗的时间通常是两周,但节省的返工时间可能是两个月。
2. 核心计算单元与存储层次的协同设计
2.1 脉动阵列 vs 单指令多数据流的取舍
计算阵列的微架构选择,往往是AI芯片设计中争论最激烈的地方。脉动阵列思路,数据在阵列中像流水线一样相邻处理单元传递,每个处理单元都只和相邻单元通信,好处是数据复用度高、布线压力小、能效比出色;坏处是编译器压力极大,因为必须严格控制数据流入流出的时序,稍微映射不好,整个阵列就大量空转。单指令多数据流风格则更灵活,每个处理单元可以独立执行不同操作,编译器只需把计算任务切分到不同单元上,但代价是每个单元附近都要有自己的寄存器或者小块缓存,指令路由和数据进行交叉互连,面积效率和功耗都不如脉动阵列。
在实际产品里,纯脉动或者纯单指令多数据流的架构都很少,更多采用的是“两者融合”的方案。核心计算块用脉动或者近脉动阵列,保证卷积类算子的高能效;同时在阵列旁边配了少量可以随机访问的向量处理单元,用来处理归一化、池化、激活函数这类对数据流不那么规律的算子。这么设计的目的很明确——编译器只需要对主力算子做严格的数据流规划,其余算子可以在向量单元上“随手”处理,软件复杂度大大降低。
我个人的体会是:如果你所在团队软件编译器力量很强,可以大胆一点用脉动阵列,因为性能上限更高;如果软件团队人力有限,还是选择带灵活性的架构更稳,不要为了追求纸面峰值算力,给后续的工具链带来无底洞。
2.2 片上缓冲区与数据复用策略
计算阵列之外,最核心的资源就是片上缓冲区。几乎每一篇AI芯片的论文都在强调数据复用,可实际设计时,这个“复用”是分层次的。卷积计算有输入特征图的通道复用(同一个像素被多个卷积核使用)和输出特征图的累积复用(多次部分和累加);全连接层则主要是权重复用,一个权重要乘以多个输入向量。
硬件设计时,你需要在SRAM容量和编译器能力之间做一次明确分工。这里我推荐的做法是:在架构层定义好三种数据复用模式——输入驻留、权重驻留、输出驻留,每种模式对应不同的数据流排布方式。编译器根据算子的特征自动选择把哪个数据留在片上,其余数据按画好的流水线搬运。这个设计思想源自经典的数据流分析理论,搬到AI芯片上同样适用。
举个例子,一个常见的3x3卷积算子,输入通道数256,输出通道数256,输出特征图尺寸112x112。如果权重驻留在片上,那么权重占用的SRAM约为3x3x256x256字节,用8比特量化就是576KB;输入特征图每次搬运一个滑动窗口,输出部分和持续累加。此时计算阵列可以保持很高的利用率,但编译器必须保证输入数据流入的速度刚好配得上计算节奏,否则脉动阵列必然出现气泡。这种流水级的规划,必须在硬件寄存器设计阶段就为编译器预留好可编程的控制字段,而不是等RTL写完了才回头商量。
2.3 存储墙问题在片上的体现
CPU时代大家就谈存储墙,到了AI芯片时代,这个问题更加尖锐。计算阵列动辄几千个乘加器同时工作,每个时钟周期消耗的数据量是几千字节,即便SRAM能提供几百GHz的带宽,依然很容易被计算阵列“抽空”。实际上,仅仅增加SRAM容量往往治标不治本,因为大容量SRAM物理上通常集中在一个固定位置,访问延迟和布线面积都会变大,所有处理单元都去访问这一个块,也会造成拥塞。
更务实的思路是构建分布式的多级存储层次。每个处理单元附近放一小块私有存储,用来保存需要在该单元局部复用的数据;然后一层中等容量的共享SRAM用于多计算单元之间的协调;最后才是DDR这类外部存储。这种结构和软件线程映射强相关,编译器需要感知每一级存储的容量和延迟,才能做出现实的调度决策。设计时我会给编译器团队提供一份存储层次带宽图,包括理论上每个端口每周期能读写多少字节、冲突会发生什么行为,让编译器在生成指令时尽量避开冲突,这也是“软硬件接口文档”里最关键的章节。
3. 软件工具链的落地实现
3.1 编译器中间表示与算子映射
AI芯片的编译器,中间表示层决定了后续优化能走多远。这里我不建议直接复用通用编译器的中间表示,因为AI编译器优化的核心对象是张量的数据流和布局变换,而不只是标量运算和循环变换。针对硬件特性设计专门的AI编译器中间表示,能保留张量的维度和数据类型信息,同时能显式描述数据在存储层次之间的搬移操作。
中间表示往下需对接硬件抽象层,这里最容易出的问题就是“硬件抽象层过厚”。有些团队为了方便,把加速器的底层寄存器封装成一套复杂的驱动接口,编译器只能看到API看不到硬件行为特征。这会导致编译器优化时完全凭猜测,性能惨不忍睹。正确做法是让编译器后端能够直接操作加速器的配置寄存器,映射时能精确控制数据流的启动和停止。即使牺牲一些通用性,这也是值得的。
算子映射这件事,本质是把一个计算图中的每个算子,翻译成加速器上可执行的基本指令序列。这些指令可以不是传统的CPU指令,而是一组配置字,告诉计算阵列的数据流模式、输入数据的起始地址和步长、以及循环的层数和边界。我见过很多团队一开始粗暴地把算子切成一小块一小块执行,导致数据搬到片上就为了算一小块卷积,效率极低。要解决这个问题,编译器必须做一个大的算子融合过程,把连续的卷积、激活、归一化等算子直接融合成一张大的执行计划。
3.2 运行时调度与多任务切分的细节
实际部署场景中,芯片很少只跑一个模型,更常见的是视频流输入,多个任务抢占加速器资源。这时候运行时的调度策略就变得很重要。硬件设计师常常忽略运行时层面的设计,总觉得编译器把计算图排好就没问题了。可一旦多任务并发,中断处理、上下文切换、内存隔离,这些问题都需要软硬件共同定义好接口。
我的建议是,运行时直接和硬件控制层协作,不要像操作系统那样采取抢占式调度,因为AI计算任务一旦在阵列上流动起来,贸然打断会导致流水线排空,损失非常大。更高效的是协作式调度:每个计算任务在启动前显式申报硬件资源,运行时统一裁决,按照某种优先级分配时间片或空间资源。协作式调度对硬件的要求也简单很多,不需要复杂的现场保护,只需要在任务边界上设置同步屏障。
实践证明,空间复用比时间复用对AI芯片友好得多。如果计算阵列足够大,可以把整张阵列切成几个独立的子阵列,每个子阵列跑一个任务,这样多个任务并行,互相之间不受干扰。硬件设计时就要预先留下一组“子阵列划分寄存器”,供运行时动态配置,而不是把所有计算单元绑死在一个任务上。
3.3 性能分析工具如何设计
没有性能分析工具,AI芯片的优化基本等于盲人摸象。性能分析工具要回答的核心问题有三个:计算阵列的利用率是多少?片上存储的读写带宽是否饱和?数据在外存和片上之间的搬运量比理论最低值高了多少?
这三个数据看起来简单,但实现难度都在硬件调试用的探针上。我建议在RTL设计阶段就把性能计数器的位点规划清楚。计算阵列控制器里记录空闲周期的计数器、存储端口记录占用率的计数器,这些几乎不需要额外硬件开销,却能在后续测试中帮大忙。很多团队流片回来才发现完全没有观测手段,只能靠猜,这种教训并不少见。
有了性能工具,就能形成一个优化闭环:运行模型→看到性能瓶颈→调整算子映射策略→修改编译参数→再运行。这个闭环跑顺了,软硬件协同设计才算真正转起来。而优化闭环的第一步,往往是先找到一个足够小的基准样例反复测,比如一层卷积加一层激活,而不是直接上完整模型。
4. 验证与物理实现中的关键细节
4.1 功能验证要覆盖编译器生成的边界情况
AI芯片验证的难点,不在于功能点太多,而在于编译器生成的指令序列组合空间太大。手写的测试用例能覆盖常规路径,但编译器总会生成一些你没想到的边界情况,比如某些维度不为8对齐的算子,或者超大特征图导致地址空间越界。这些边界组合在验证阶段缺乏关注,流片后美国队长也救不回来。
我建议在验证计划中单独列一个“编译器随机压力测试”类别:通过随机修改算子参数,生成大量合法但罕见的指令序列,在仿真平台上回归测试。这个方法听起来简单,实际做起来效率极高,能在早期发现大量RTL逻辑漏洞。
功能验证的另外一个重点是数据精度。AI芯片普遍支持8比特甚至更低的量化精度,RTL里乘累加的顺序变化会导致精度表现微妙差异。验证阶段要专门比较编译后的硬件执行结果和参考模型的计算结果,容差设定不能太宽松,也不能严到完全不切实际。这里需要软硬件团队共同定一个标准,一般是逐层比较输出的余弦相似度或者误差百分比。
4.2 物理设计与低功耗的博弈
物理实现阶段最棘手的问题,倒不是电路能不能收敛,而是高频率和大面积带来的功耗和发热。AI芯片里面计算阵列的翻转率通常很高,动态功耗巨大,如果不做精细的时钟门控,静态功耗和动态功耗叠加起来,芯片温度很快逼近上限。
一个非常有效的做法是,在阵列级做细粒度的时钟门控:当某块子阵列没有任务时,硬件控制逻辑直接把对应区域的时钟关闭。但这要求编译器能够在指令流里显式标注任务的开始和结束位置,硬件在结束位置自动触发时钟门控。这个配合我在两个项目里实践过,效果立竿见影,功耗最多能省下30%以上。
低功耗的另一个层次是数据搬移的功耗。从DDR读数据的能耗,比从SRAM读数据高一个数量级,更远高于计算本身的能耗。所以“数据搬移尽量少”不仅是性能优化的目标,也是功耗优化的核心手段。软硬件协同设计到最后,会发现性能优化和功耗优化的策略常常是一致的,那就是尽量把计算留在片上,把数据复用做到极致。
4.3 后仿真与性能预评估
物理设计完成后,拿到带寄生参数的网表,跑后仿真,这个时候芯片的实际情况已经和图样很接近了。我建议后仿真阶段一定要用真实的软件工具链生成的实际应用场景负载,不要只用简单的测试向量。因为只有真实的负载才能模拟出存储带宽冲突、多任务并发的效果。
这一步能提前暴露出几个问题:内存接口的时序裕量是否足够、片上网络在多端口同时访问时是否出现死锁、时钟树偏差对高速路径的实际影响。在流片前尽早发现这些风险,哪怕需要微调布局布线,也比之后芯片回来再补救要便宜得多。
后仿的性能预评估数据,也是软件团队调优的依据。因为此时还能修改编译器调度策略,如果预评估发现某个算子频繁因为存储端口冲突而停顿,编译器就可以提前改变数据布局,避免访问竞争,这个优化窗口在流片后就永久关闭了。
5. 常见问题与项目复盘心得
5.1 编译器掉队导致硬件空转
我见过不止一个项目死在这种情况:芯片算力纸面数据很好看,编译器迟迟达不到预期映射效率,最终整体性能只有峰值的二三成。这种问题的根源普遍在于软件的启动时间晚于硬件的设计周期,等硬件快完工了,软件才发现架构上某个关键限制让映射束手束脚。
要避免这个问题,唯一可行的方案是把“最小软件栈”的启动节点前移,甚至在架构定义阶段就有编译器人员在旁参与。不必等完整工具链,只需要一个能映射少量算子、走过完整软硬件数据通路的原型,哪怕性能很低,也能暴露软硬件接口的核心矛盾。这个实践看起来拖慢了前期进度,实际上是为整个项目买保险。
5.2 中间张量如何调度最经济
推理和训练场景中,中间张量的调度都很有讲究。我见过很多编译器默认把中间张量全部留在DDR,简单但性能很差;另一些编译器反过来,试图把所有中间张量都留在片上,结果容量爆了频繁换入换出,性能同样拉垮。
折中的方案是给每个张量标注一个预期生命周期,然后做容量规划,让生命周期最短的张量优先使用片上存储,生命周期长的流式数据直接走DDR。这和操作系统里的页面置换算法有点类似,但区别在于编译器可以全局地预知所有张量的生命周期,所以可以做出比运行时算法更优的静态分配。
5.3 文档即接口
软硬件协同设计团队,最容易被低估的交付物不是代码,不是架构文档,而是“程序员参考手册”。这份文档需要精确到每个控制寄存器的位域含义、每个状态机的跳转条件、每条同步指令的时序开销。没有这份文档,编译器后端开发人员和硬件设计人员日常沟通就会变成一场冗长的马拉松。
我现在的习惯是,硬件RTL第一版冻结的同时,强制要求硬件团队同步完成寄存器手册的初稿,后续每改一次RTL,手册必须在同一天更新。这个约定执行起来并不容易,但坚持三周后,你会发现软件团队的返工请求大幅减少。文档即接口,这不是一句口号,是无数项目经验换来的血泪教训。
回到这篇“AI芯片的软硬件设计 7”,我个人最想强调的还是那个看起来有点老生常谈的结论:软硬件协同不是两个团队各自做接口对接,而是从一开始就为一个共同目标设计。硬件多留一点可编程性,软件多理解一点硬件约束,最终的性能和功耗收益都是乘法级别的。项目做到后期,你会发现最珍贵的不是某个灵光一现的架构创新,而是这一套从编译器到寄存器的完整默契。这套默契建立起来之后,后面再做下一代芯片的时候,前期磨合的时间能省下一大半。