1. 从"能跑就行"到"跑得高效":AI芯片软硬件协同设计的核心命题
做AI芯片这行的人都有一个共识:一颗芯片流片回来,真正决定它能不能在真实业务里站住脚的,往往不是峰值算力有多高,而是软硬件配合得够不够默契。我见过太多项目,纸面参数漂亮得不行,TOPS数字一个比一个唬人,结果一上真实模型,利用率连30%都不到。问题出在哪?几乎都出在软硬件设计的割裂上——硬件团队闷头堆MAC阵列,软件团队拿到芯片才开始想怎么把算子映射上去,中间那层编译器、运行时、调度策略全是事后补丁。
这个系列写到第8篇,我想把话题收拢到一个最核心的点上:AI芯片的软硬件设计本质上是一个联合优化问题,而不是两个独立工程的简单拼接。你可以在架构阶段就为软件留好接口,也可以在指令集层面就考虑算子融合的便利性,更可以在内存层次设计时就把数据复用模式想清楚。这些决策一旦定下来,后面软件团队再怎么努力,天花板就已经锁死了。
这篇文章适合谁看?如果你正在参与AI芯片的架构定义、编译器开发、算子库优化,或者你是一个想理解"为什么同样算力的芯片实际表现差这么多"的技术管理者,那接下来的内容应该对你有用。我会从数据流、指令集、内存层次、编译映射、运行时调度这几个维度,把软硬件协同设计里那些"文档不会写但实际绕不开"的东西讲清楚。
2. 数据流决定一切:从计算模式反推硬件架构
2.1 为什么数据流是软硬件设计的第一个决策点
任何AI芯片的设计起点都不应该是"我要做多少个MAC",而应该是"我的目标负载数据怎么流动"。卷积、矩阵乘、注意力机制,它们的共同特征是什么?是数据复用。一个权重值可能被成百上千个激活值共享,一个激活值也可能参与多个输出通道的计算。硬件设计的核心任务,就是让这些复用尽可能在离计算单元最近的地方发生。
我习惯把数据流分成三类来思考:权重固定(Weight Stationary)、输出固定(Output Stationary)、行固定(Row Stationary)。这不是学术分类,而是直接决定你SRAM怎么摆、总线怎么走、PE之间怎么连的工程选择。举个例子,如果你做的是边缘推理芯片,目标模型以深度可分离卷积为主,那权重数据量小但激活值复用率高,这时候输出固定的数据流配合较大的激活缓存就更合适。反过来,如果是云端训练芯片,大矩阵乘是主力,权重和激活都需要高复用,那行固定或者混合数据流方案往往能拿到更好的能效比。
这里有个经验数据:在典型的卷积层中,如果数据流设计得当,DRAM访问量可以降到理论最小值的1.5倍以内;如果设计不当,这个数字轻松突破10倍。而DRAM访问的能耗是片上SRAM访问的100到200倍。这意味着什么?意味着数据流设计不好,你堆再多计算单元也是白搭,能耗全花在搬数据上了。
2.2 从算子特征反推PE阵列结构
我在实际项目中总结出一个方法:先列出目标负载中占比最高的5到10个算子,画出它们的数据依赖图,然后看这些图里有没有共同的"形状"。比如3x3卷积的滑动窗口、矩阵乘的二维分块、注意力的QKV计算,它们对PE阵列的要求是不一样的。
假设你的目标负载里3x3卷积占主导,那PE阵列最好能支持行卷积的滑动复用——也就是相邻输出像素共享输入行的数据。这时候PE之间的互联如果只是简单的二维网格,数据搬运开销会很大。更合理的做法是在PE阵列里加入专门的广播通路和移位通路,让一行输入数据能同时喂给多个PE,并且支持跨周期的滑动。
再比如,如果Transformer类模型占比高,那矩阵乘的维度往往很大(比如512x512甚至更大),这时候PE阵列的规模就要上去,同时要支持分块计算——因为不可能把整个矩阵都放在片上。分块的大小又反过来影响SRAM的容量设计。我见过一个项目,PE阵列做了256x256,但SRAM只够存64x64的分块,结果计算单元经常饿着,利用率上不去。这就是典型的软硬件脱节。
2.3 数据流设计中的常见误区
第一个误区是"追求通用性"。有些团队想做一个能跑所有模型的芯片,结果数据流设计得四不像,什么都能跑但什么都跑不快。我的建议是:明确你的主战场。如果80%的算力预算花在卷积上,那就把卷积的数据流做到极致,剩下的算子用可编程单元兜底。
第二个误区是忽视稀疏性。现在很多模型经过剪枝后稀疏度能达到50%以上,但硬件如果还是按稠密计算设计,那实际有效算力就浪费了一半。支持稀疏计算的数据流设计需要在PE阵列里加入零值跳过逻辑,同时编译器要能生成对应的稀疏编码格式。这部分的软硬件接口如果没提前定义好,后面补起来非常痛苦。
第三个误区是低估了数据搬运的复杂度。我经常用一个比喻:计算单元是厨房里的灶台,数据是食材。你灶台再多,如果食材都堆在楼下仓库,每次做饭都要跑上跑下,那出菜速度肯定快不了。所以片上缓存层次的设计——寄存器文件、SRAM、全局缓冲——每一层的容量、带宽、访问延迟都要跟数据流匹配。
3. 指令集与编译器:软硬件之间的"合同"
3.1 自定义指令集的粒度选择
AI芯片的指令集设计有个经典难题:粒度太粗,灵活性差,新算子支持不了;粒度太细,指令开销大,取指译码的功耗就吃掉一大块。我参与过的一个项目,最初设计了一套非常细粒度的SIMD指令,结果发现卷积层的指令密度太高,前端功耗占了总功耗的25%。后来改成粗粒度的"卷积宏指令",把循环控制、地址计算都交给硬件状态机,前端功耗直接降到8%。
那怎么选粒度?我的经验法则是:看算子的重复模式。如果一个计算模式在多个模型里反复出现(比如3x3卷积、1x1卷积、矩阵乘),那就值得做成一条宏指令。如果是一些零散的特殊算子(比如某些激活函数、自定义的归一化),那就用可编程的细粒度指令兜底。这样既保证了主路径的效率,又保留了灵活性。
指令集的另一个关键点是数据搬运指令的设计。很多团队把注意力全放在计算指令上,搬运指令随便设计,结果编译器生成代码时发现数据搬不动。搬运指令要支持什么?至少要支持:不同存储层次之间的块拷贝、带步长的访问、广播模式、转置模式。这些模式如果硬件不支持,编译器就得用多条指令拼,效率直线下降。
3.2 编译器如何"理解"硬件
编译器是软硬件之间的翻译官,但这个翻译官如果不懂硬件的脾气,翻出来的代码就没法看。我见过最离谱的案例是:硬件有一个非常高效的权重预取机制,但编译器不知道,生成的代码还是按部就班地先搬权重再计算,预取机制完全没用上。
要让编译器"理解"硬件,需要在指令集设计阶段就提供足够的元信息。比如:每条指令的延迟、吞吐量、资源占用、依赖关系。这些信息通常以调度模型的形式提供给编译器。调度模型越准确,编译器的指令调度就越高效。
还有一个容易被忽视的点:编译器的中间表示(IR)要能表达硬件特有的优化。比如张量核心的矩阵乘指令,它要求输入数据按特定格式排列。如果IR里只有通用的矩阵乘操作,编译器就很难自动生成这种指令。更好的做法是在IR里加入"硬件内建函数"(intrinsic),让上层框架能直接调用,同时编译器也能识别并优化这些模式。
3.3 算子融合的软硬件协同
算子融合是提升效率的大杀器。把卷积、批归一化、激活函数融合成一个计算块,可以减少中间结果的写回和读取,节省大量带宽。但算子融合不是软件单方面能做的,它需要硬件支持。
硬件要支持什么?首先,计算单元要能在一次数据流中完成多个操作。比如卷积计算完后,结果直接进入激活函数单元,不需要写回SRAM再读出来。其次,要有灵活的旁路通路,让数据在不同计算单元之间直接传递。最后,指令集要支持"融合算子"的表达,让编译器能生成对应的指令序列。
我在实际项目中的做法是:在硬件设计阶段就定义一组"融合模式",比如"卷积+BN+ReLU"、"矩阵乘+Softmax"等,然后在指令集里为这些模式预留编码空间。编译器识别到对应的算子组合时,直接生成融合指令。这样既保证了效率,又不会让指令集过于复杂。
4. 内存层次设计:被低估的性能杠杆
4.1 片上缓存的容量与带宽权衡
AI芯片的片上缓存设计是个多目标优化问题:容量要大,带宽要高,面积要小,功耗要低。这四个目标互相矛盾,必须做取舍。我的经验是:先确定计算单元的吞吐量,然后反推缓存带宽需求。
举个例子,假设你的PE阵列有1024个MAC,每个周期能完成1024次乘加。如果每个MAC需要两个操作数(一个权重、一个激活),那每周期需要2048个操作数。如果操作数来自SRAM,那SRAM的带宽至少要达到2048个元素/周期。假设每个元素是16位,那就是4096字节/周期的带宽。这个数字直接决定了SRAM的位宽和bank数量。
容量方面,要保证计算单元不会频繁等待数据。通常的做法是:权重缓存要能存下至少一个完整卷积层的权重(或者一个分块的权重),激活缓存要能存下至少两行输入特征图(支持滑动窗口)。如果容量不够,计算单元就会饿着。
这里有个实际数据:在28nm工艺下,1MB的SRAM面积大约是1平方毫米,功耗大约是100毫瓦。如果你的芯片面积预算是10平方毫米,那SRAM最多占3到4平方毫米,也就是3到4MB。这个容量约束会直接限制你能支持的最大模型和最大分块。
4.2 数据复用与缓存替换策略
缓存替换策略在通用CPU里是硬件自动管理的,但在AI芯片里,很多时候需要软件参与。为什么?因为AI计算的数据访问模式是可预测的。编译器知道下一步要访问哪些数据,可以提前发出预取指令,也可以显式地管理缓存的换入换出。
我比较推崇的做法是软件管理的暂存器(Scratchpad Memory),而不是硬件自动管理的Cache。Scratchpad的地址由软件直接控制,编译器可以精确地安排数据的加载和使用顺序,避免Cache带来的不确定性。当然,这增加了编译器的复杂度,但换来的效率提升是值得的。
具体到数据复用,要区分三种复用:时间复用(同一数据在不同时间被多次使用)、空间复用(同一数据被多个计算单元同时使用)、通道复用(同一数据在不同通道间共享)。不同的复用模式需要不同的缓存策略。时间复用要求缓存容量大,空间复用要求缓存带宽高,通道复用要求缓存支持多播。
4.3 片外内存接口的设计考量
片外内存(通常是DRAM)的带宽和延迟是AI芯片的另一个瓶颈。设计内存接口时,要考虑几个问题:带宽够不够、延迟能不能隐藏、访问模式是否友好。
带宽方面,一个简单的估算方法是:片外带宽至少要是片内带宽的1/10。比如片内SRAM带宽是4TB/s,那DRAM带宽至少要400GB/s。当然,这个比例取决于你的数据复用做得多好。复用做得好,片外访问就少,带宽需求就低。
延迟隐藏方面,通常用预取和流水线来掩盖DRAM的长延迟。编译器要能生成预取指令,硬件要有足够的缓冲来存放预取的数据。我见过一个设计,DRAM延迟是200个周期,但预取缓冲只有4个条目,结果预取根本来不及,计算单元经常停顿。后来把预取缓冲加到16个条目,利用率立刻上去了。
访问模式方面,DRAM最喜欢的是连续的大块访问。如果访问模式是随机的、小块的,那有效带宽会大打折扣。所以编译器和硬件要配合,尽量把访问模式整理成连续的。比如卷积的im2col操作,就是把不连续的访问变成连续的矩阵访问,这样DRAM效率高很多。
5. 运行时调度:让硬件"活"起来
5.1 任务划分与负载均衡
一颗AI芯片上通常有多个计算簇、多个内存通道。运行时调度器的任务就是把这些资源合理地分配给不同的计算任务,让它们并行起来,同时避免资源冲突。
任务划分的粒度很关键。粒度太粗,并行度不够;粒度太细,调度开销大。我的经验是:以计算簇为单位划分任务,每个任务的计算量至少能覆盖调度开销的10倍以上。比如调度一个任务的开销是1000个周期,那这个任务的计算量至少要10000个周期,否则调度本身就成了瓶颈。
负载均衡方面,静态划分简单但容易不均,动态划分灵活但有开销。实际项目中,我通常采用静态为主、动态为辅的策略:编译时做一次静态划分,运行时根据实际执行情况做微调。比如某个簇执行慢了,运行时可以把下一个任务优先分给它。
5.2 多核协同与数据一致性
多核AI芯片的一个难点是数据一致性。多个计算簇可能同时访问同一块内存,如果缺乏同步机制,就会出现数据竞争。硬件上通常用屏障(Barrier)和原子操作来支持同步,但软件上怎么用这些机制,直接影响性能。
我见过一个设计,每个计算簇都有自己的私有缓存,但缓存之间没有硬件一致性协议。结果软件必须显式地做缓存刷新和失效,代码复杂不说,性能还差。后来改成共享缓存加硬件一致性,软件简单了,性能也上去了。所以我的建议是:如果面积和功耗允许,尽量做硬件一致性。软件管理一致性的代价太高了。
5.3 动态电压频率调整与功耗管理
AI芯片的功耗管理是个系统工程。动态电压频率调整(DVFS)是最常用的手段,但怎么调、什么时候调,需要软硬件配合。
硬件要提供什么?频率和电压的调节接口、功耗和温度的传感器、以及调节的粒度。软件要做什么?根据当前负载和热状况,决定调节策略。比如推理任务对延迟不敏感,可以降频省电;训练任务对延迟敏感,要保持高频。
这里有个实际经验:DVFS的调节频率不要太高。我见过一个设计,每1000个周期调一次频率,结果调节开销比省下来的功耗还大。通常来说,调节周期在毫秒级比较合适,这样既能响应负载变化,又不会引入太多开销。
6. 从纸面到硅片:那些只有流片后才明白的事
6.1 仿真与实测的差距来源
流片前,我们通常用仿真器评估性能。但仿真器和真实芯片之间往往有差距,有时候差距还很大。差距从哪来?我总结了几点:
第一,时钟树和布线延迟。仿真时通常假设时钟是理想的,但真实芯片里时钟树有偏斜,布线有延迟。这些在先进工艺下尤其明显。我见过一个设计,仿真时能跑1GHz,流片后只能跑800MHz,问题就出在时钟树上。
第二,内存的实际带宽。仿真时通常假设内存带宽是理论峰值,但真实访问模式下,有效带宽可能只有峰值的60%到70%。特别是当访问模式不连续时,差距更大。
第三,功耗和热的影响。仿真时通常不考虑热,但真实芯片跑起来会发热,发热会导致漏电增加,进而影响频率和功耗。如果散热设计不好,芯片可能被迫降频。
6.2 软件栈的适配与调优
芯片流片回来,软件栈的适配是个大工程。编译器要重新调优,算子库要重新实现,运行时要做性能剖析。这个过程通常需要几个月甚至更长时间。
我的经验是:在流片前就要准备好软件栈的框架。编译器可以先用功能仿真器验证,算子库可以先用指令集模拟器测试。等芯片回来,只需要把后端替换成真实硬件,大部分软件逻辑可以复用。这样能大大缩短适配周期。
调优方面,要重点关注瓶颈在哪里。用性能计数器收集数据,看计算单元利用率、内存带宽利用率、指令发射率等指标。哪个指标低,就重点优化哪里。不要盲目地改代码,要有数据支撑。
6.3 常见流片后问题与应对
流片后最常见的问题包括:时序不满足、功耗超标、功能bug、性能不达标。时序问题通常靠降频解决,但降频会影响性能。功耗问题要靠DVFS和软件优化。功能bug要靠软件绕过或者重新流片(代价很大)。性能不达标要靠软件调优,但如果差距太大,可能就要考虑改版了。
我的建议是:在架构阶段就留足余量。时序上留10%到15%的余量,功耗上留20%的余量,性能上留30%的余量。这样即使流片后有偏差,也不至于无法挽回。
7. 一些个人体会
做AI芯片软硬件设计这些年,我最大的体会是:不要试图把软硬件分开设计再拼起来。最好的芯片,往往是软硬件团队坐在一起,从算法特征出发,一步步推导出架构、指令集、编译策略和运行时方案。这个过程很痛苦,因为要反复迭代,要互相妥协,但最终的结果会好很多。
另一个体会是:不要迷信峰值指标。TOPS、带宽、能效比,这些数字只有在真实负载下才有意义。我见过太多芯片,峰值指标很好看,但实际跑模型时利用率很低。所以评估一颗芯片,一定要看它在目标负载上的端到端表现,而不是纸面参数。
最后,软件栈的投入不要省。一颗芯片的硬件成本是固定的,但软件栈的质量决定了这颗芯片能用多久、能跑多少模型。我见过硬件不错但软件稀烂的芯片,最后没人用;也见过硬件一般但软件极其好用的芯片,生态做起来了。所以如果预算允许,软件团队的投入至少要和硬件团队相当。
这个系列写到第8篇,基本上把AI芯片软硬件设计的主要方面都覆盖了。后面如果还有想聊的,可能会写写具体案例的拆解,或者某些专项技术的深入分析。有什么想讨论的,欢迎交流。