AI芯片软硬件协同设计:从指令集到编译器的全栈优化 前阵子有位做软件的朋友问我AI芯片是不是就是把矩阵乘法那层皮扒掉里面堆一堆乘加单元就够了。我跟他说如果真是这样我做的那款模拟加速器就不会在实验室里改了三版架构、编译器的代码量几乎是硬件的三倍。做AI芯片时间久了会有一个很深的体会真正决定一颗芯片能不能在真实场景里跑出漂亮指标的从来不是某个乘法器快了多少而是算法、编译器、硬件这三层能不能作为一个整体咬合住。这篇文章接着前面的系列往下写重点聊聊软硬件协同设计这条主线——从指令集契约、数据流选择到编译器映射、性能预估再到最终上板验证把我在几个模拟项目里踩过的坑和沉淀下来的方法一并说清楚。1. 做加速器几年我最大的认知变化瓶颈从来不在算力本身1.1 从数MAC阵列到数数据片上流动的距离入行初期我觉得AI芯片设计最难的部分是把MAC阵列做大、把主频拉高。后来真正接手一个边缘端加速器的前端设计才发现这个想法有多天真。一颗3TOPS的INT8加速器芯片片内MAC阵列面积可能只占die的不到1/4剩下大片面积是SRAM、互联、控制逻辑和对外接口。为什么因为芯片每执行一次矩阵乘法消耗的能量和延迟大头往往不在计算本身而在数据搬运——把权重和激活值从DRAM搬进片上SRAM、从SRAM搬进寄存器、往下一层MAC送。业内有个粗略经验一次DRAM访问消耗的能量大约可以支持几十次到上百次MAC运算。所以在真实场景里你的算力再高如果数据喂不进去计算单元照样空转。这就像一间餐厅灶台MAC阵列火力再猛切菜、传菜、摆盘数据搬运跟不上出餐速度还是上不去。很多做AI芯片的新人最容易犯的错就是盯着TOPS这个数字却被跑模型时的实际吞吐泼了一盆冷水。所以现在团队里讨论新架构第一句话很少问算力多少而是问数据片从DRAM到片上要流几遍能不能只流一遍1.2 算法、编译器、硬件必须当成一个系统来设计软件工程师习惯把硬件当成一个可以随时调用的黑盒子硬件工程师则容易把模型当成一组可以反复重新运行的负载。这两种视角在通用CPU时代问题不大但在AI加速器上会直接变成灾难。因为AI芯片不像通用CPU那样有一个几十年稳定不变的指令集做缓冲而是芯片的每个周期行为、每个buffer的容量、每个MAC阵列的组织方式都会直接影响某类模型能不能跑得快。我在模拟项目里见过最典型的场景是算法同学把网络里的Depthwise卷积改成普通卷积精度没怎么变但推理时延翻了一倍。原因很简单硬件对DW卷积有专门的数据通路优化改回去之后编译器只能走通用矩阵乘的老路数据流被打断中间结果频繁落DRAM。这个案例让我意识到所谓软硬件协同设计不是硬件设计完了软件去适配而是从需求拆解阶段开始算法、编译器、硬件三方就要坐在一起把这个模型到底会以什么数据流形态出现在芯片上这件事定义清楚。也就是说你得先把算法里的算子集合、张量形状、精度要求摸一遍再来决定片上存储分几级、MAC阵列什么拓扑、指令集用什么粒度。反过来硬件拓扑一旦确定编译器就得用特定的分块和数据复用策略去喂满它。这种双向约束关系才是AI芯片设计的真正核心。2. 指令集就是软硬件签下的那份契约2.1 先定数据流再谈指令集很多人以为指令集设计是芯片设计最早期的工作实际在我参与的项目里指令集的决策反而是架构中期的一次收敛。它前面必须有一件事先定下来计算阵列采用什么数据流。数据流这个概念通俗讲就是数据在MAC阵列里的流动方式是权重待在原地不动、激活数据流过去还是激活待在原地、权重流过去。不同选择会直接决定片上存储的容量比例、互联的宽度、甚至编译器要怎么切tile。拿我参与的那个边缘端加速器项目来说最初版本用的是Output Stationary数据流——每个PE先把部分和留在自己的局部寄存器里等累加完成为止。这种流方式对卷积很友好因为输出通道之间的累加操作不需要频繁写回SRAM能省不少功耗。但它的代价是PE内部需要一组额外的寄存器保存累加器状态对编译器来说分块方案也被限制得很死。后来为了跑更宽的Transformer类模型我们又引入了Weight Stationary的辅助模式权重一次性广播到整行PE激活数据逐列流过。这样在同一颗芯片上卷积和全连接都能找到相对合适的执行形态。所以指令集不是凭空设计出来的它其实就是把上面这些数据流选择翻译成硬件能够理解的操作原语一条指令告诉硬件从哪个buffer读激活、从哪个buffer读权重、往哪个buffer写累加结果以及累加多少轮。这个翻译要做得足够干净编译器才不至于在ISA层去掩盖硬件的数据流限制。2.2 三种经典数据流到底在争什么圈内经常讨论三种数据流Weight Stationary、Input Stationary、Output Stationary。它们争的本质只有一件事数据复用的发生在哪一级谁牺牲带宽、谁节省功耗。数据流保持不动的数据流动的数据典型优势主要代价Weight Stationary权重驻留PE内激活数据流经PE权重复用率高适合卷积共享权重场景PE内寄存器压力大激活广播需要高带宽Input Stationary激活驻留PE内权重流经PE激活复用好适合激活数据放不下但权重可流式的场景权重重复读取多对DRAM带宽要求高Output Stationary部分和驻留PE内权重和激活都流入累加量小功耗低适合卷积层需要额外累加寄存器PE利用率受输出tile形状影响我当时做的一个很直观的实验是在同一个16×16的MAC阵列上分别用WS和OS两种数据流去跑一个3×3卷积层。同样是256KB的片上SRAM切片WS版本能把权重搬运量降到OS版本的大约六分之一但代价是激活需要被额外广播互联布线面积明显增加。这说明没有绝对最好的数据流只有和你的目标负载最匹配的数据流。如果你主要跑卷积权重复用很有优势如果主要跑全连接或Attention输出拼接和激活复用可能更关键。2.3 指令集里最容易被低估的一环同步与内存屏障硬件工程师画RTL的时候一般不太会去细想同步因为硬件天然是并行的组合逻辑一跑数据自然到。但编译器工程师拿到指令集之后第一件头疼的事就来了多条指令之间怎么保证访存顺序是对的一条指令写入SRAM的数据下一条指令能不能保证读得到PE阵列里每个PE算完了自己的部分和什么时候可以触发全局累加我在模拟项目里设计ISA时早期版本把同步设计得特别隐式——只有一条WaitAll指令让所有PE都停下来对齐一次。结果编译器给出的调度经常因为过度同步导致流水线气泡实测性能比理论上限低了将近40%。后来参考了一些公开的加速器设计思路改成显式的barrier和fence机制允许一个PE组在完成本地累加后先行同步其他PE组继续算这样就能把同步点从整阵列粒度细化到行粒度或bank粒度。这个看起来很小的改动最后让编译器调度器的自由度大了非常多很多原本凑不齐的流水也可以被填满。所以指令集设计有一条很实际的经验尽可能把同步、内存可见性、buffer切换这类语义在ISA层暴露清楚让软件能精确控制硬件并行度。不要好心地把它们全部隐藏起来这会变成编译器优化的一道隐形墙。3. 编译器才是决定AI芯片上限的那只手3.1 模型进编译器之后发生了什么说到AI芯片大家习惯先看硬件架构但真正拉开性能差距的往往是编译器。我常打一个比方硬件是运动场编译器是教练。场地再好教练不会排兵布阵运动员照样乱成一团。模型从框架端进来编译器要依次完成算子解析、数据格式转换、算子融合、循环分块、存储规划、指令调度最后生成一串能在你这颗芯片上跑的指令序列。每一步都直接在跟硬件的物理限制打交道。以我们团队自研的编译器为例前端接的是ONNX格式的模型中间表示层会把每个算子的计算循环显式展开成五重甚至七重循环N、C、H、W、K、R、S。然后编译器要决定这七重循环里哪些维度在片上SRAM完成、哪些维度流向DRAM、哪些维度塞进PE阵列的局部寄存器。这个决策过程直接决定了一次推理过程中DRAM的访问总量。跑同一个ResNet类模型不同的分块策略可以让DRAM访问量差出三四倍这不是夸张。3.2 分块把一次放不下的计算切成刚刚好的小块片上SRAM永远不够装下一整张特征图和全部权重所以编译器必须做tiling。一个常用的判断逻辑是先用Roofline模型算出理论计算强度再根据片上buffer的容量反推每个tile的尺寸。比如某个卷积层输入特征图是112×112、64通道INT8下就是800多KB。4MB的片上SRAM还要给权重和输出留位置所以就得把H和W方向切成多块比如一次只算64×64的空间范围让输入块尽量多地留在片上。这个看似简单的分块过程实际是整体性能的放大器。我记得有一版调度器因为切分的H维度过小导致每个tile边界都要重新读一遍相邻像素特征图边缘的冗余读取一下多出30%。后来我们把分块尺寸和硬件里的邻域缓冲对齐让tile之间的halo卷积的边界重叠区正好落在片上DRAM流量立刻降下来。有些优化甚至还要考虑复用顺序同一份权重要被多个输出通道复用编译器可以把它先广播到整列PE再依次切换输入这样权重只需要加载一次。用术语说就是让一个数据变量在尽可能高的存储层级上被复用避免数据从DRAM重复加载。这个原则几乎适用于任何AI芯片编译器。3.3 算子融合与访存优化编译器为什么能带来几倍差距另一个大头是算子融合。框架里常见的ConvBNReLU如果不融合每一步都要把中间张量完整写回DRAM。在有限的片上带宽下这种中间结果落盘简直就是性能杀手。编译器要做的就是识别这些算子的计算模式把BN的缩放因子折算进卷积权重把ReLU变成激活函数级别的截断操作让三个算子在PE阵列里一趟完成。我做过一个很小的对比实验同样一个MobileNet类网络在同一个模拟器上不融合版本和全融合版本相比端到端时延差了2.1倍。各位注意2.1倍不是算力差不是架构差纯粹是编译器少做了一组优化。这给了我们团队一个很重要的价值观不要轻易说某颗AI芯片不行先看一眼它的编译器栈到底把硬件潜力榨出来几成。编译器这部分还有一个特别容易被忽视的点指令缓存和取指带宽。有些加速器的指令很宽一条长指令能同时控制8个buffer的读写地址、MAC阵列的开关、累加器的复位。如果指令调度生成的代码不够紧凑取指阶段就会成为新的瓶颈。我们后来给编译器加了一个指令打包的pass把多条短操作合并成一条VLIW风格的长指令取指带宽占用一下子降了一半。3.4 编程模型设计让算法工程师愿意用、用得顺这一点是我后来才强烈意识到的。编译器再先进如果编程模型对算法工程师不友好项目照样推进不下去。我们早期给编译器提供的是类似CUDA风格的显式buffer管理接口结果算法同事每次调用都要自己手动搬数据、申请临时buffer、管理同步光调试就花了一周。后来改成了张量原语风格的上层接口算法工程师只需要描述这是一个3×3卷积输入张量形状如此输出张量形状如此剩下的分块、buffer分配、掩码全部交回编译器自动推导。这个改动表面看只是API风格差异实际上改变了整个项目的工作方式。算法端可以用高级描述快速迭代新模型编译器端可以用统一的中间表示做性能优化硬件端的buffer结构也稳定下来不用反复改。所以说软硬件协同设计的最后一公里其实是和人协同——让每个角色的表达成本都低才愿意配合你。4. 性能预估与实际芯片之间隔着一堆合理的意外4.1 用Roofline模型给自己泼冷水在跑仿真之前团队常用Roofline模型做一次粗略的天花板预估。这个模型很简单横轴是计算强度每字节数据对应多少次运算纵轴是可达性能斜线是带宽上限平台区是算力上限。一张图就能回答这颗芯片跑这个模型到底是算力不够还是带宽不够。我举个例子某卷积层计算量是1.2GOPS对应的激活和权重数据合计约8MB。如果芯片的DRAM带宽是16GB/s理论计算强度就是150 Ops/Byte。而我那片加速器在750MHz、2048 MAC并行时算力大概3TOPS除以16GB/s带宽临界计算强度约187 Ops/Byte。也就是说这个卷积层刚好处在Roofline的转折区附近——稍微增加一点数据复用就能从带宽受限转到算力受限性能提升会很明显。这个结论指导了我们的优化方向优先减少DRAM访问量而不是再堆主频。很多团队上板前不做这个测算结果一上板性能达不到指标就开始怀疑硬件有bug实际上多半是数据搬运强度超过了带宽上限。Roofline不能给出精确数字但能帮你把性能瓶颈快速定位到该改存储结构还是该改编译器这个判断价值很大。4.2 上板实测最容易翻车的三个环节仿真阶段的性能模型往往把所有存储资源都当成理想的、可随机访问的SRAM。真正放到FPGA原型平台或者流片回来之后第一个翻车点通常是bank冲突。片上SRAM被分成多个bank如果两条指令同时访问同一个bank的不同地址硬件只能串行处理延迟瞬间翻倍。编译器在调度时如果不感知bank布局这种冲突会频繁发生。第二个翻车点是动态电压频率调整DVFS带来的降频。性能模型默认芯片一直跑在标称频率但实际推理任务会在几分钟内把芯片温度拉上去电源管理逻辑自动把频率从750MHz降到650MHz算力直接损失13%。特别是一些跑Transformer的模型功耗尖峰非常高降频几乎是必然发生的。第三个是部分算子的固定开销远超想象。比如LayerNorm虽然计算量很小但它要跨通道做归约需要多次访问整行数据而且很难流水化。在模型里这种算子可能只占5%的计算量却消耗了20%的执行时间。这类算子会成为端到端性能的隐形短板架构设计时如果只盯着矩阵乘法优化最后跑模型时就会发现Arm核或DSP核被这些杂活拖住了。4.3 端到端评估不能只盯着算子的峰值算力我见过不少团队在做架构对比时习惯只统计每个算子在硬件上的计算时间然后简单加总。这个做法有他一定的合理性但会漏掉三笔隐性开销一是数据搬移的时间二是指令发射和同步的等待时间三是内存分配和释放时候的固定开销。尤其在小batch推理场景每层都只算几百微秒而这些固定开销可能就有几十微秒比例相当可观。正确的做法是做一次端到端的指令级仿真把编译器生成的完整指令序列喂给周期精确模型统计从第一张图进来到第一张图出去的全部周期数。这个数字才算是真实可交付性能。在我们的项目里端到端仿真跑出来的性能通常是理想算子时间累加的55%到70%你如果只看累加很容易过于乐观。端到端评估里还有一个很反直觉的经验有时候算法模型加大batch算力利用率会涨但端到端时延和功耗也一起涨。边缘端推理用户往往更在意单帧时延这时候宁可牺牲一点算力利用率也要保证响应时间因为消费者的体验是看单张图等了多久不是看那段时间芯片忙不忙。5. 一个边缘端AI加速器案例的完整复盘5.1 需求拆解为什么选择3TOPS算力加4MB片上SRAM为了把这套方法串起来我复盘一个模拟的边缘端AI加速器项目。需求方最初给的条件很粗线条在5W整板功耗以内跑一个目标检测模型输入尺寸640×640跑在INT8精度上帧率不低于30FPS。算一下这个模型单帧大概需要90GOPS左右的推理量30FPS就是2.7TOPS有效计算量。再算上算子开销和实际利用率理论算力至少要3TOPS。这决定了MAC阵列规模——投到2048 MAC并行配合750MHz主频理论峰值为3.07TOPS。片上SRAM容量定4MB也是从数据流反推出来的。为了在跑检测头通常是一些1×1卷积和小尺寸特征图操作时不需要频繁访问DRAM我们希望把整帧的特征图中间结果尽量留在片上。640×640输入经骨干网络下采样后最小特征图约20×20通道数256一张也就100多KB。但各层中间结果叠起来峰值需求会到3MB左右。4MB是留了余量也刚好满足LPDDR4带宽约束下的数据缓冲需求。有时候多一点SRAM编译器能做的优化空间大很多这也是百万级晶体管换性能的划算买卖。5.2 编译器与硬件的三轮迭代过程第一版架构是最传统的设计一个统一的MAC阵列一套单一的数据流编译器用固定策略分块。跑简单分类模型没问题但一旦跑检测模型头部那些小卷积和逐通道操作的低效率就暴露了。虽然理论算力3TOPS实际跑YOLO类结构的端到端算力利用率只有12%。这个数字简直不能看。第二轮做了两个关键修改一是把MAC阵列拆成两个可独立调度的子阵列一个处理大卷积一个处理小算子避免大卷积霸占阵列时小算子排队二是编译器加入了自动平衡数据流的开关让它在不同算子之间自由选择WS或OS。利用率从12%提到了24%。改的是硬件但触发原因来自编译器的profiling反馈。第三轮则彻底调整了存储层次把原来的两级buffer改成三级在PE阵列和主SRAM之间加了一小层distributed register file专门缓存邻域数据让3×3卷积的halo读取不再占用主SRAM带宽。同时编译器新增了一个针对检测头的形状推断优化把小张量操作尽量打包成一条复合指令减少同步点。最终端到端算力利用率到了35%左右——听起来不高但已经比第一版翻了三倍。在边缘端这个利用率水平已经算可用状态。三轮迭代下来最大的体会是性能和架构的关系不是单向的编译器发现的问题必须反馈回硬件硬件改完又给编译器释放新的空间这个循环走两三次才能得到一个真正合格的设计。5.3 新设计里的几个关键选择稀疏、量化、多芯粒完成三轮迭代后团队开始看下一版的方向。第一个是稀疏化。检测模型里ReLU之后大约40%的激活是0如果能跳过这些无效计算等效算力可以再提升一截。但做稀疏化要谨慎它需要硬件支持一定的跳零逻辑还需要编译器在运行时动态判断哪些块可以跳过。如果跳过粒度太细控制逻辑开销会超过节省下来的算力。目前可行的方案是块状稀疏比如16×16的块内如果都是0就整体跳过控制逻辑简单收益也稳定。第二个是量化。INT8已经普及但很多模型在敏感层需要更高精度。我们测试了混合精度方案大部分卷积保持INT8个别敏感的检测头用INT16精度几乎不损失而推理速度只下降约8%。关键是编译器要支持混合精度的自动插入。这又是一个软硬件绑定很深的点。第三个是多个芯粒之间的互联。边缘端可能不急着做多die但服务器端的大模型推理单die的面积和功耗都撑不住。芯粒间互联的带宽和延迟会直接决定模型并行切分方式。这部分我们还在早期探索目前的结论是先别追求片上网络的花哨拓扑扎实把点到点带宽和低延迟流控做好编译器切分的自由度就够用了。从软件端看还有一件不算技术、但比技术更影响成败的事给算法和编译器团队留出足够的协同时间。硬件流片周期长软件栈的适配往往被压在最后两个月到时候才发现某个指令集的字段安排不合理再想改已经不可能。我现在的做法是硬件RTL冻结之前先把指令集模拟器、编译器后端、周期模型全部跑通用真实的模型负载做一次全栈演练把所有软件想用但硬件不支持硬件支持但编译器不会用的摩擦提前暴露出来而不是等到MPW回来再擦屁股。附我可以分享的一点实际操作体会如果你正准备开始一个AI芯片相关的项目不管你是做硬件、做编译器还是做算法我建议你先找一个十几行代码的经典卷积亲手把从算法描述到指令生成再到周期仿真的链路至少完整跑通一遍。很多人一上来就搭大框架结果在打通流程上花的时间比写核心逻辑还多。还有做架构决策时一定要把编译器能不能用出来当成硬性评审项一个再漂亮的片上网络或数据流方案如果编译器要绕很大一圈才能生成指令那它在真实负载面前就很难落地。我做的这几个模拟项目里真正让团队从会做芯片变成能把芯片做好的分水岭就是所有人都开始承认软件不是硬件的附属品编译器不是收尾时才插进来的模块。把这句话想通了再回头看那些密密麻麻的SRAM bank、PE互联和指令调度规则突然就觉得它们不是一个一个孤立的电路而以一颗芯片为中心、三层软件栈共同编织成的一个整体。这颗芯片性能好不好先别急着甩锅给某一个环节把链路从头到尾顺一遍答案往往就自己浮出来了。