做AI芯片这几年,我越来越有一个体会:很多人一听说“AI芯片”,第一反应就是看算力、看制程、看TOPS,但真正决定一颗芯片能不能用的,往往是软件和硬件之间那层薄薄的“契约”。你硬件堆了再多的MAC阵列,编译器映射不上去、算子跑不出效率、内存搬移把时间全吃掉,那这颗芯片就只是一块昂贵的砖头。AI芯片的软硬件设计,本质上是在做一件事:让计算架构和算法表达彼此妥协、彼此成就。这篇文章我会从整体设计思路、硬件微架构、软件栈协同、以及实测调优四个维度,把AI芯片从定义到落地过程中真正值得抠的细节讲透,内容偏工程实践,适合芯片工程师、AI框架开发者、以及准备入行AI基础设施方向的同学参考。
1. AI芯片软硬件设计,真正要权衡的几件事
1.1 动手写Verilog之前,先回答三个问题
我见过不少团队做AI芯片,开局就是看对手方案、抄计算阵列尺寸、拼工艺节点,结果做出来的东西要么编译器改到吐血,要么在某些模型上性能好看、换个模型就直接“翻车”。如果你问我设计AI芯片第一步该干什么,我会说:先不要谈核数、不要谈频率,先回答三个问题。
第一,这颗芯片主要服务什么场景?云端训推一体、边端低功耗推理、还是自动驾驶这种时延敏感的实时任务?不同场景直接决定你计算精度的选择、片上存储的大小、以及数据流架构的方向。第二个问题,目标模型家族是什么?CNN、Transformer还是混合架构?这决定了你计算阵列的形态——比如Transformer的矩阵乘法和CNN的卷积在数据复用模式上差异很大,我见过在阵列设计时只按卷积优化、结果跑Attention时MAC利用率掉到三成以下的例子。第三,谁在使用这颗芯片?是给内部的软件团队还是开放给外部开发者?如果是外部开发者,你不仅要有完善的指令集文档,还要有足够友好的编程接口。
这三个问题看起来简单,实际上是把“软硬件设计”从概念变成具体约束的过程。计算精度是FP16、BF16还是混合精度?激活函数是放硬件还是走软件模拟?内存子系统是纯SRAM还是带LPDDR?每一条都牵扯到软件栈怎么写。这些问题如果靠拍拍脑袋决定,后面大概率要用十倍的时间去填坑。
1.2 算力、带宽与能效的三角拉扯
很多人会把TOPS当成AI芯片最重要的指标,但从系统角度看,TOPS只是上限,真正决定实际性能的是“算力/带宽”的比值。你的MAC阵列每秒钟能算4096次乘加,但你片外内存带宽每秒只够喂512个数,那计算阵列会一直处于饥饿状态。这里有一个常用的估算方式:假设一个矩阵乘法算子,MAC阵列需要从片外读取A矩阵的一个块和B矩阵的一个块,为了不浪费计算能力,必须保证读取这些数据的时间小于或等于计算时间。
举个例子,假设你的AI核心以1GHz运行,每次周期能完成1024次MAC计算,那么每秒就是1024 G MACs即2048 GOPS。要喂满这个阵列,假设每次读取一个32KB的数据块用于计算,那么你能做到的带宽需求约等于算力除以数据复用倍数。如果是卷积算子,输入像素被多个输出通道复用,数据复用倍率可能很高;而如果是逐元素算子或者Embedding查找这类低复用场景,带宽需求会陡然增大。这也是为什么AI芯片必须围绕算子特征来做软硬件协同,单纯堆算力没有任何意义。
能效则是另一个硬约束。很多端侧AI芯片的功耗预算只有几瓦到十几瓦,你提高主频、增加阵列宽度,都可能让功耗直接超预算。工程上常用“能效比”(TOPS/W)来衡量,但你要注意,这个数字在不同精度、不同稀疏度条件下差异极大。硬件架构师最怕看到“峰值TOPS/W”很高的设计——因为那通常意味着只有在极度理想的情况下才能达到,真实负载下能打到五六成就算不错了。
1.3 软硬件边界怎么划:哪些固话、哪些可编程
接着上面说,软硬件边界划分是整个项目中最需要克制力的环节。业内普遍的做法是:把最成熟、最稳定、最频繁被调用的底层算子交给专用硬件单元,比如矩阵乘法单元、量化/反量化单元、激活函数查表单元;把这些单元之上的调度、融合、循环变换交给编译器;而把上层网络结构解析、算子调度策略交给运行时和框架层。
我自己经历过一个项目,团队想把Space-to-Depth这类只在少数模型里出现的算子做成硬件指令,结果为了支持这个算子消耗了大量验证资源,软件侧的使用率却非常低,最后只能作为“遗留特性”默默躺在手册里。反过来,如果所有算子都走通用可编程的方式,比如让编译器把任何计算都拆成通用的向量指令来跑,又会牺牲性能和能效。这个平衡点的把握,基本靠对目标应用场景的充分调研来决定。
我的建议是:在项目初期,建立一个算子热点表,统计目标模型在推理时的耗时占比和调用频次,把Top级别的算子作为硬件优化的重点,其他算子用可编程指令覆盖。这样软硬件边界就不再是拍脑袋拍出来的,而是有量化依据的。这个热点表,也会成为后续编译器调度策略和硬件验证用例的重要输入。
2. 硬件侧的核心设计细节:从指令集到数据通路
2.1 计算阵列选型:SIMT与脉动阵列的取舍
AI计算阵列的形态,目前主要有两个方向:一类是类似GPU的SIMT风格,大量通用ALU配合线程调度器,灵活性强,适合复杂控制流和多种算子形态;另一类是脉动阵列,数据像流过血管一样在阵列中沿固定方向流动,每个处理单元只做一次乘加并把结果传给邻居,数据复用率高、功耗低,但灵活性差。
两者在编程上差异很大。SIMT风格本质上是在硬件调度器上做线程级并行,软件侧只需要把计算映射成线程块即可;脉动阵列则要求编译器在编译期就决定数据在阵列中怎么流动、在哪里驻留、何时输出,这是一项非常考验编译器设计能力的工作。我见过有些团队拿到脉动阵列IP之后,写了满屏的调度伪代码,最终性能仍然离理论峰值差两倍以上,问题就出在数据流的安排上。
实际操作中,如果是做边缘端推理芯片且模型相对固定,脉动阵列通常是更优的能效选择;如果是面向多模型、多种算子形态的通用AI芯片,SIMT风格加矩阵运算指令扩展会更容易做软件生态。还有一类混合方案,我称之为“矩阵核心+向量核+标量核”的三级异构:矩阵核心负责高密度乘加运算,向量核负责归一化、激活、逐元素操作,标量核负责控制逻辑和地址计算。这种结构虽然设计复杂度高,但能较好地兼顾灵活性和能效,也是目前主流AI芯片IP的常见架构。
2.2 存储层次与数据流策略怎么定
存储层次设计在AI芯片里分量极大。很多团队把精力都花在计算阵列上,结果系统上板之后发现性能瓶颈全在数据搬运。这里有一个基本经验:在同等工艺下,片上SRAM的访问能耗比片外DRAM访问能耗低一到两个数量级,因此片上数据复用做得越好,整体能效就越高。
存储层次通常是两级:寄存器或Buffer级、片上SRAM / Cache级、再加片外DDR。设计时要明确每个数据流的驻留位置。以卷积为例,主要有三种数据流策略:Weight Stationary(权重驻留)、Input Stationary(输入驻留)、Output Stationary(输出驻留)。权重驻留适合权重复用率高的场景;输入驻留适合输入特征图复用高的场景;输出驻留则以减少输出累加次数为目标。没有绝对的最好,只有最适合目标模型组合的策略。
这几类数据流策略对软件栈的要求也不一样。比如Weight Stationary需要编译器知道卷积核尺寸并把它切进计算阵列,Output Stationary则需要注意累加器的位宽和溢出控制。我实测下来,对于大多数卷积网络,把输入特征图切块放进片上SRAM、对输出通道做分块累加,是一个稳定性很高、也最容易在编译器中落地的方案。因为它的数据流控制逻辑简单,编译器只需要维护一个“当前SRAM中驻留了哪些数据块”的状态表,就能比较自然地生成搬运指令和计算指令的交错序列。
2.3 同步、流水与片上网络的工程实现
如果你以为AI芯片的硬件设计只是计算阵列,那就大错特错了。真正让硬件团队掉头发的,往往是同步机制和片上数据通路。当一个算子被切分成若干个小任务、分发到不同计算核心上执行时,核心之间如何同步、数据怎么流动、如何避免死锁和竞争,都是非常磨人的工程细节。
同步机制通常有硬件同步原语,比如栅栏(Barrier)、原子操作、以及依赖计数器。设计这些机制时要特别注意:它们既要保证正确性,又不能在正常流程中过度消耗时间。我见过一个设计,每个算子执行完都要做一次全芯片栅栏同步,结果模型一深、算子一多,同步开销占到了总执行时间的百分之十几。
片上网络(NoC)也是设计重点。AI芯片的流量模式跟普通多核CPU不太一样,更多是单向的、大块的数据搬运,比如从DMA搬入SRAM、从SRAM搬入计算阵列、累加结果搬回SRAM。这个时候,传统的复杂路由网络反而没有优势,简单的总线或环形拓扑配合专用的DMA通道往往更高效。更关键的是,DMA引擎要支持二维地址变换(strided copy),这可以直接支持对输入特征图子块的高效搬运,减少软件侧的地址计算负担。
3. 软件侧的实现要点:算子映射、编译器与部署
3.1 算子映射:循环切分到底在解决什么问题
硬件流片回来之后、软件栈之前,有一层非常关键的抽象叫“算子映射”。它的本质是把神经网络中的张量运算,翻译成硬件指令序列,算法上对应的就是循环变换和循环切分。
以卷积为例,原本是个七重循环:N、Cin、H、W、Cout、Kh、Kw。你不可能把整个循环完全展开,硬件阵列尺寸、SRAM容量都有限。因此编译器需要做循环切分(Tiling),把大循环切分成适合硬件阵列大小的小块,同时安排数据搬运和计算的重叠。这个过程中涉及两个经典概念:循环顺序(loop order)和寄存器分块(register blocking)。循环顺序决定数据流动的局部性,寄存器分块决定数据在寄存器级和阵列级的复用程度。
我做过的实用策略是:先把输出通道(Cout)分成大块,再把输入通道(Cin)进行分块,接着在空间维度(H, W)上切分。对于每个计算核心,尽量让输入特征图子块和权重子块同时驻留在片上SRAM中,然后循环执行乘加运算。这样设计的好处是:数据搬运量小,每次搬运都能支持足够多的计算,MAC利用率能稳定保持在70%以上。
另外要提一个细节:编译器生成的指令序列一定要考虑“数据搬运与计算重叠”。也就是说,当计算单元正在算当前数据块时,DMA引擎同时搬运下一块数据。这个流水线重叠如果不做,你会发现计算单元利用率非常低。实现层面的关键是设置双缓冲(double buffering),在SRAM中分配两组Buffer,交替承担“计算中”和“搬运中”的角色。
3.2 内存分配与数据搬移的调度策略
如果软件栈里有一个角色最容易被忽视,那就是内存分配器。AI芯片的芯片内SRAM碎片化管理,直接决定你能同时驻留多少个算子执行。我建议内存分配采用“区域划分 + 池化管理”的方式:把SRAM按用途划分为输入缓冲区、输出缓冲区、权重缓冲区、临时缓冲区,每个区域内部再按算子需求动态分配。
有一个很实际的经验:临时缓冲区的总大小,要按照“最大单个算子”的峰值需求来预留,而不是按所有算子需求之和。因为实际运行中算子之间是串行的,临时空间可以被复用。若按之和来预留,需要更大的SRAM,直接推高成本。这个道理听起来很简单,但很多团队在早期架构评估时都会算错。
搬移调度方面,DMA指令应该由编译器生成,而不是运行时动态计算地址。编译器知道每一层算子的形状,可以在编译期把搬运清单变成一张二维表的偏移量,运行时只需要按顺序触发这些DMA。这样可以省掉大量地址计算逻辑。如果你做的是全静态编译方案,甚至可以提前规划好整张图的执行顺序和数据生命周期,做到“零动态分配”。我强烈建议,AI推理芯片一到两年之内的项目都优先走静态方案,因为它的验证成本低、性能可预期;等生态做大了再考虑动态执行图路线。
3.3 量化模型在芯片上的精度与性能调优
AI芯片几乎绕不开量化,因为整数运算的能效远高于浮点。INT8推理已经是标配,BF16/FP16用于训练回传或者大模型推理,更激进些的INT4、INT2也在特定场景落地。但量化不是一个简单的“截断位宽”问题,它牵涉到数值范围分布、量化参数的选择,以及硬件对量化算子的支持程度。
在软件侧,量化分为训练后量化(PTQ)和量化感知训练(QAT)。PTQ算起来简单,但对数值分布敏感的模型精度可能掉很多。QAT精度高,但需要修改训练代码、重新训练模型,周期长。对于端侧推理芯片,我通常建议先上PTQ,用高精度校准集数据找到合适的scale和zero_point;如果精度不达标,再对有问题的层做QAT。
硬件侧的量化支持则需要关注两点:一是“双端量化”的指令支持,即支持卷积输入和输出分别使用不同的量化参数,这是大多数推理框架的标准行为;二是“反量化插入位置”,很多时候为了精度,计算累加器保持INT32,最后的激活层再进行反量化到INT8或FP16。这个序列在硬件上要能简洁地表达为一条融合指令,否则编译器就要产生额外的内存搬移。
我踩过的一个典型的坑:某个模型经过QAT之后精度已经达标准了,但在部署到芯片之后跑出来完全不对。排查了很久才发现是硬件在处理残差连接时,把上一层的FP16输出和当前层的INT8输入直接相加,数值范围没有对齐。后来修复方式是在残差分支也插入量化/反量化节点。因此,部署时一定要仔细检查多输入算子的量化一致性,尤其是Add、Concat、Reshape这些本身不做计算的算子。
4. 从仿真到流片调优:实测中必须面对的坑
4.1 搭建软硬件协同仿真环境
软硬件协同仿真环境是AI芯片项目中期投入产出比最高的部分。它本质上是在硬件RTL模型之上挂一个软件执行环境,让编译器生成的指令可以直接在这个模型上跑起来,输出逐周期的行为日志和计算结果。这个环境做得好,很多软件侧的bug在流片之前就暴露了,省下来的调试时间非常可观。
我建议把协同仿真环境分成三层:第一层是纯功能级仿真,只验证指令语义的正确性,跑得非常快,适合验证编译器生成的指令序列有没有原则性错误;第二层是周期级仿真,会模拟微架构的流水线、存储冲突、带宽限制,适合做性能评估;第三层是FPGA原型验证,速度最快、最接近真实行为,但开发和调试复杂度高。项目节奏上,先做第一层,尽早把软件栈跑通;等微架构稳定之后再做第二层;FPGA平台则可以平行推进,重点验DDR控制器和系统级集成问题。
另一个工程建议是,协同仿真环境中必须加入“指令覆盖率”统计。也就是统计编译生成的所有指令中,哪些指令类型被覆盖、哪些硬件模块被充分激活。覆盖率数据可以反向指引编译器开发者增加新的优化策略,也能用于验证团队评估硬件设计是否存在冗余逻辑。
4.2 实测性能与理论性能差在哪
从协同仿真到实测,最打击团队信心的一个环节就是跑分。明明理论算力标着202 TOPS,一跑真实模型,居然只有30多TOPS。这里面的差距,几乎总在以下四个地方:第一,内存带宽瓶颈。前面说过,数据复用倍数低,算力就喂不饱;第二,算子融合不够。有些团队把多个算子拆成了独立的指令序列,每个算子之间都要把中间结果写回SRAM再读出,白白浪费了一半带宽;第三,流水线stall。比如计算单元在等DMA搬运、或者DMA在等总线授权;第四,动态形状和Padding处理不对称,导致某些输入尺寸下计算阵列利用率减半。
我自己的排查习惯是:先看整张图的每一层算子实际周期数,跟理论周期数做个对比表。如果单层算子的performance大于理论值的1.5倍,就深入看该层的指令序列,借助周期级仿真工具拉出stall因果关系。曾经有一层卷积算子性能异常,最终定位到是SRAM bank冲突——两个输入块恰好落在同一个bank,访问串行化,导致每个周期实际只执行了预期的一半MAC。
另外建议把图优化工具做得“透明”一点。编译器优化之后的计算图、指令序列、以及SRAM分配表都以可视化或日志形式吐出来。这样不仅便于算法团队审查,也方便硬件团队反向确认某些异常数据,到底是由模型引起还是芯片引起。
4.3 常见问题与排查思路速查
下面这个表,算是我在多个AI芯片项目里踩坑经验的压缩版。覆盖了从编译到上板最常见的问题,每一条都是真实扔过调试日志的。
| 问题现象 | 可能的根因 | 排查方向 |
|---|---|---|
| 算力利用率偏低 | 数据搬移与计算没有重叠 | 检查指令序列中DMA与计算指令的流水间隔,确认双缓冲是否生效 |
| 量化模型输出偏差大 | 多输入算子的量化参数不一致 | 检查残差连接、Concat、Add、以及Reshape后的Scale/ZP值 |
| 编译后死循环或卡死 | 同步屏障等待条件未满足 | 检查核心间依赖关系,查看同步计数器是否溢出 |
| 某些层计算结果时对时错 | SRAM数据覆盖被提前触发 | 检查内存分配表中生命周期标记,确认数据块是否被提前释放 |
| 带宽利用率只有预期一半 | DMA访问模式的bank冲突 | 调整数据块的地址偏移,尽量按bank数对齐,避免冲突访问 |
| 同一模型不同输入尺寸性能差异巨大 | Padding和Tiling未做该尺寸适配 | 查看循环切分尺寸是否为阵列宽度的整数倍,必要时做block padding |
4.4 编译器性能优化的两个突破点:算子融合与自动调优
前面反复提到算子融合,这里具体展开。AI芯片的算子融合,核心目标是在保证语义不变的前提下,减少中间结果的内存读写。最常见的融合组合包括“卷积+批归一化+激活”三段式融合,和“矩阵乘法+残差Add”融合。
卷积加BN加ReLU的融合,思路是把BN的缩放和偏移参数在编译期折算到卷积的Weight和Bias里,这样运行时就完全不需要单独的BN算子,直接输出ReLU结果。这个优化几乎对所有CNN模型都有效。
自动调优则是编译器里更进阶的玩法。你可以预定义一组切分策略,包括循环顺序、分块大小、双缓冲的位置、融合与否,然后通过一个性能模型或者直接在硬件上做short-run评测,找到每个算子最优配置。这个过程听起来复杂,但在AI芯片的编译器里价值巨大。因为算子形态随着模型迭代总会有新的变化,而人工调参完全跟不上模型变化的速度。
5. 写在最后:一点个人体会
AI芯片的软硬件设计,表面上拼的是架构和算力,实际上拼的是工程系统能力。做这个方向这几年,我最大的体会是:软件栈和硬件架构必须从一开始就一起设计、互相约束,而不是等硬件做完再写编译器。硬件设计的每一个灵活性和限制条件,最终都会在软件栈里被放大或者被消化。
另一个很深的感受是,做AI芯片没有捷径,所有宣称“一键部署、开箱全速”的方案在实际模型面前都需要做大量调优。真正的核心竞争力,来自于对目标场景的深刻理解、对编译器细节的偏执打磨,以及对每一次性能异常追查到底的耐心。如果你正在做或者准备做AI芯片,我希望这篇文章能帮你少走几个弯路——至少,在看到那些好看的理论峰值数字时,能多问一句:喂得饱吗?调得动吗?跑得稳吗?