做了快十年的芯片和算法联合设计,我越来越觉得,AI芯片最核心的竞争力不在于堆了多少TOPS算力,而在于软硬件这套东西能不能真正咬合在一起。我见过太多项目,流片回来以后芯片能亮,但跑模型总是跑不出设计时预估的性能,耗电也不对,最后查下来往往不是晶体管的问题,而是软件栈和硬件微架构从第一天起就没有对齐。今天想围绕“AI芯片的软硬件设计”这个主题,把我这些年做端侧AI加速芯片积累的理解重新梳理一遍,给正在入行或者准备立项的朋友一个参考。
这篇文章会覆盖几个层面:为什么说AI芯片设计的真正难度在软硬件协同、硬件端计算阵列存储和互联的热点、软件栈里编译器算子库和运行时怎么跟硬件对话,再到一个具体矩阵乘法优化案例的完整链路。无论你是刚接触硬件设计还是写编译器,或是想评估一颗AI芯片到底好用不好用,都可以在这篇里找到实际判断标准。
1. 先从软硬件协同说起:AI芯片为什么不能只造快电路
1.1 为什么软硬件必须一起设计:软件栈才是真实瓶颈
很多人理解AI芯片,第一反应是“把矩阵乘加多堆一点,频率拉高,就完事了”。实际上AI芯片是一个高度定制化的领域专用处理器,它的每一次硬件资源分配,都是在替某一类软件写法做提前投票。你说你设计了四条指令,能跑卷积,能跑矩阵乘,但编译器如果要为这条指令做复杂的地址拼接,甚至要为每个cycle手工编排数据的载入时机,那这个硬件所谓的“通用”就是假的。
我在某团队做过一个模拟项目X,第一版硬件设计得很漂亮,算力阵列规模大,内存带宽也预留了不少,但是软件接口文档迟迟没有冻结。结果就是硬件团队按自己理解的寄存器接口写RTL,编译器那边也不知道该生成什么序列,等FPGA版本跑起来以后,光是对齐握手信号就花了一个月。问题不在哪一边,而是在立项初期没有把软硬件接口当成项目的核心交付物,软件栈的规划成了硬件的附属品。真实的经验是:AI芯片的软件栈不是硬件完成之后的“补课”,而是从第一天就开始约束微架构选择的关键输入。
这里的本质原因在于,AI计算里的数据搬运成本远高于计算本身。一个矩阵乘法,如果硬件结构不能感知到激活值、权重、输出梯度这些数据的复用模式,就会盲目地反复从片外取数。软件可以靠tiling和流水线把数据搬运藏到计算后面,但前提是硬件必须提供足够的片上缓冲、DMA和同步机制。没有软件视角去定义这些机制,芯片大概率只会成为一个“跑分很吓人、实际用起来很别扭”的算力堆料。
1.2 设计起点是工作负载:先把模型统计做好,再谈面积和频率
经常有团队一上来就问:我的NPU该做多大MAC阵列?要不要上稀疏?INT8该做到多少TOPS?我一般都会反问一句:你手头真实要跑的模型,算子类型占比、张量形状分布、精度要求、延迟约束,统计过没有?这是做AI芯片软硬件设计最容易被跳过的一个环节,也是很多芯片落地后利用率上不去的根源。
我曾经拿到过一个目标检测模型集合,统计之后发现,真正消耗时间的是几个大矩阵乘和大量的元素级算子,而不是硬件团队花了很多面积去支持的高精度特殊函数计算单元。如果当初不按负载分布去做取舍,芯片面积会更大,功耗也压不住。一个好的做法是,把典型模型像“基准测试集”一样管理起来,每个模型输出它的关键算子热力图、内存访问量、内存占用峰值,甚至模拟几种候选微架构下的大致周期数。这项工作不需要精确到cycle级,但可以用来检验:硬件结构和软件的执行序能不能匹配上。
做负载分析的另一个作用,是提前暴露软硬件接口的取舍矛盾。比如端侧模型里很多层是channels小的卷积,如果硬件只照顾超大矩阵做的分割和调度,小规模计算就会在阵列上产生大量空闲。软件编译器可以做算子融合或重新张量化,但硬件如果没有灵活地支持这些变换,编译器也无能为力。所以负载分析不只是为了“确认需求”,它是软硬件设计目标工作量和面积分配的共同底座。
2. 硬件侧三个核心模块拆解:计算阵列、存储层级、片上网络
2.1 计算阵列怎么选:从MAC到乘累加矩阵,以及精度选择的账
AI芯片武最常见的基础单元是MAC,也就是乘法累加单元。所谓算力堆叠,本质是不少MAC在同一个cycle里对不同的数据做乘加运算。这里有一个关键设计点:计算单元阵列之间的连接方式,决定了数据是“广播”还是“脉动”还是“单指令多数据”的流动路径。脉动阵列往往在维度匹配的矩阵乘里面效率很高,每个处理单元只和相邻单元通信,数据像水一样流过阵列,但一旦遇到不规则形状的张量,利用率就会明显下降。SIMD风格阵列更灵活,但要保证每个单元能同步从正确的地址取数,片内数据通路就得更宽。
做选择时得算一笔账。假设一张INT8矩阵乘可以做到128 TOPS,听起来很猛,但如果你同时要考虑功耗,MAC在翻转率较高的时候动态功耗占比很大,而65%以上的线程可能都在做无效的零值相乘。如果负载里有30%的稀疏度,硬件加一点结构化稀疏处理就能让有效吞吐翻倍,但代价是额外的稀疏索引解析电路和更复杂的编译调度,软件栈要能配套。不要只盯峰值,要在真实模型的算子分布上去测有效利用率。
精度也是软硬件接口里很难回头的决定。FP16能兼顾范围跟精度,INT8就得认真做量化感知训练和校准;如果你一开始盯上BF16或者FP8,那软件端的loss缩放、梯度更新的写法也要跟着改变。我通常建议:对端侧推理,优先考虑INT8加少量FP16混合精度,因为带宽和功耗收益都很明确;对训练场景,再考虑BF16甚至FP8。精度选择会影响寄存器位宽、乘加器结构、累加器的位宽上限,编译器后端所有类型转换规则也要跟着定死。
2.2 存储层级怎么搭:先算数据搬运成本,再定SRAM容量和片外带宽
AI芯片性能瓶颈基本都在存储层级,这句话听着老生常谈,但实际设计时大多数人还是习惯先定算力后补存储。正确的顺序应该是反过来:先明确你的片外带宽、片上SRAM容量和数量级,然后反推算力能不能被持续喂饱。一个非常经典的Roofline模型思路:计算强度等于“每字节数据能支撑多少次操作”。如果架构的计算强度低于算法需求,就跑不到硬件峰值;如果计算强度太高,意味着SRAM大量面积可能被浪费。
用具体的数据说。假设一个矩阵乘层,尺寸不小,需要把几十MB的权重从片外搬进来。如果片外带宽是32GB/s,搬64MB需要2毫秒;而如果计算阵列做10 Tops INT8,算完这一层大概需要6.4毫秒左右。看起来搬数时间小于计算时间,但如果编译器不做double buffering,处理器会先搬完再算,时间就接近两者相加,效率下降很多。硬件设计者需要提供的,不只是SRAM容量本身,还包括多块SRAM之间可同时读写的能力、DMA和计算并行执行的通道,以及在硬件上管理这些buffer生命周期的机制。
另一个容易踩的坑是SRAM的bank冲突。表面上有很大一片SRAM,但多个处理核同时访问同一bank地址时会被串行化。软件能做的是尽力在编译阶段做好数据排布,而硬件端至少要支持不同bank独立访问。两者需要在bandwidth模型里达成一致:软件告诉硬件“我预估访问分布是这样的”,硬件通过仿真验证这个访问分布下会不会出现脉冲式冲突。这块如果做不好,实测带宽可能只有理论值的一半,再好的tiling策略也白搭。
2.3 片上网络和数据通路:最容易被忽略的等待之源
计算阵列和存储层级谈完了,还有一个很多人初期不重视、后期查性能问题头大的模块:片上网络。它本质上负责把多组计算单元和存储单元连起来,像个十字路口。多核结构里,如果一组核要把中间结果送给另一组核,走的是片上网络,那这条通路的带宽、延迟和仲裁策略,会直接决定核间同步的开销。早期设计选一个结构简单的交换网络,听起来够用,但到了多模型并发或者一层里多种算子混合执行的时候,就可能出现某个核霸占总线导致其他核空转的情况。
我在某图像处理Demo上就吃过亏。硬件是一堆小计算核,每个核有自己的SRAM,核之间靠一个简单的环形网络通信。单算子跑起来没问题,但某个模型有很多concat和elementwise操作,在核之间来回倒腾数据,环网的延迟叠加以后,核利用率直接掉了一半。后来看了profiling,发现大部分时间都花在“等待数据到达”和“同步屏障”上。解决办法是调整算子切分边界,让大部分计算在核内完成,减少跨核数据交换;同时硬件上把网络从单环改成双环,流量分散一些。这件事告诉我:软硬件一起设计时,留给“数据流动”的注意力至少要跟留给“计算单元”的注意力一样多。
3. 软件栈设计:编程模型、编译器和算子库,一个都不能少
3.1 编程模型与指令集:硬件接口需要“说人话”
把AI芯片投放到市场之后,面对两块用户:一是做算法训练的工程师,他们要的是“我丢一个模型进去,工具链能给我一个高效的部署包”;二是做算子开发的高性能工程师,他们要的是“硬件的指令语义清楚,资源边界明确,我能手动写出接近峰值的kernel”。这两类用户不是一波人,所以编程模型通常要分两层表达。
底层的指令集有两个设计方向。一种是加载存储式指令,每条指令做一件明确的事,比如从SRAM加载一个tensor,做一次矩阵乘,再把结果存回;另一种是更粗粒度的任务派发指令,把某一组矩阵乘的参数表直接甩给硬件,让硬件的执行引擎自己完成调度。后者对程序员更友好,但指令解析、地址生成和分支控制的硬件复杂度会上升,编译器要做的事情也更多。核心原则是让硬件原语和软件中间表示之间,不要出现“语义鸿沟”。如果指令集的设计使得一个简单的tensor操作要拆成十几条微指令,编译器就很难把上层优化逻辑和硬件行为对齐。
我看到的实际趋势是,主流AI加速芯片几乎都对用户提供tensor级或图级编程接口,底层则维护一个直观的指令抽象。无论走哪条路,不能回避的是内存模型的定义:哪些内存区域可被哪个计算单元访问,粒度多大,谁负责同步。这些定义不清晰,写算子的人只能靠经验猜时序,排查问题变成玄学。
3.2 编译器做到什么程度:图优化、内存规划与调度
AI编译器对于现代AI芯片来说,不再是“可选组件”,而是决定产品好坏的核心主战场。从模型到机器码,编译器的主要工作可以粗略分成三个层面:图优化层、算子层优化、指令层规划。
图优化层最常见的就是算子融合,把相邻的elementwise算子合并成更大的计算体,减少中间结果的写回和读入。一个很典型的例子是“卷积+批归一化+ReLU+量化”融合成单个物理算子,节省的不仅是计算时间,更关键的是中间数据的存储与带宽。很多AI芯片软件栈说自己性能好,拉开差距的地方就在融合策略够不够狠:边的结构、精度约束、memory的别名分析都有关系。这块要有一定的硬件知识,否则常常因为片内buffer数目不够而融合失败。
指令层规划里最重要的是规划片上buffer的使用与生命周期。编译器知道每个算子的输入输入数据量和计算量后,要决定:先加载多少进来、算完的结果留在哪里、什么时候写回片外。这个过程和操作系统做寄存器分配一样,只不过“寄存器”变成了可能有大几十KB甚至几MB的分布式SRAM。规划的越好,DMA和计算的重叠程度就越高。我认为所有硬件团队都应该提供一个周期级的性能模型,让编译器可以在生成指令之前模拟几十种调度方案,选代价最小的。没有这个模型,编译器只能靠启发式,性能就很难收敛。
3.3 算子库与运行时:为什么还要手写Kernel
算法工程师通常不会直接接触编译器生成的底层代码,但算子库的功力会直接决定精度与性能。AI芯片支持的算子种类不可能全部靠编译器自动生成高效实现。比如带特殊padding的卷积、大stride的转置、混合精度计算,这类形态用通用代码生成器打出来的性能通常不如手写,因为代码生成器很难精确控制每个时钟周期的buffer使用。因此,成熟的软件栈都会维护一组手工优化的算子库作为关键路径,同时编译器只承担能处理的部分。
运行时同样重要,它负责任务队列、内存池管理、多核调度和异步执行。多个模型并发推理的时候,runtime要把计算请求切分给不同计算核,并且处理好中断与同步。这里很考验工程能力的地方是内存分配器。片内SRAM是稀缺资源,如果每个线程都简单做静态分配,就会出现很多碎片,算子之间的中间buffer复用也做不出来。我看到过某个优化项目,仅仅把runtime的内存分配从按算子的静态layout改成按“生命周期全局统一规划”,多模型并发下的有效利用率就提高了两位数百分比。
4. 实操复盘:一个矩阵乘法在AI芯片上的完整优化链路
4.1 矩阵乘法为什么是最好的软硬件试金石
为什么聊AI芯片软硬件设计总绕不开矩阵乘法?因为它能覆盖掉大部分AI计算的核心特征。一个矩阵乘法要把A矩阵的元素和B矩阵的元素进行乘累加,计算量是访问量的好几倍,理论上很适合加速,但如果实现不对,就可以因为带宽浪费而跑得极其低效。硬件设计里可以借它来验证MAC阵列的利用率、SRAM复用策略、DMA和流水线控制,软件设计里可以用它来验证tiling的切分逻辑和编译器的调度输出。
拿我手头一个模拟项目X的真实场景举例。C = A × B,张量尺寸大概是M×K和K×N,其中M=512、K=4096、N=8192。计算量是2×M×K×N,也就是约34.4 GFLOPs。如果采用INT8计算,约34.4 GFLOPs其实就是34.4 GOPS。假设硬件能力是10 TOPS INT8,理论上只需要约3.4毫秒就能完成。但这么小的计算却在输入输出上涉及大约76MB的DRAM访问,按照32GB/s的带宽就要约2.4毫秒。听上去还好,但这只是算一个层,最原始的代码如果一点点搬数,没有利用复用,总耗时能被数据访问主导到计算时间的十倍以上。矩阵乘法于是变成了软硬件接口是否高效的最直接试金石。
4.2 Tiling与双缓冲:把数据搬运藏到计算后面
解决上面问题的主角是分块和流水线。分块,也就是tiling,意思是别把整个大矩阵一次性搬到片上,而是切成小块循环处理,让每个数据被反复使用时都尽量留在SRAM里。经典的分块是考虑M、K、N三个维度分别切,一个tile内部计算时只用一部分A和一部分B,它们都有较高的复用率。切分得越细,片上buffer越容易放下,但分块太细会导致数据加载次数变多、控制开销变大;切分太粗,buffer又放不下。这个平衡点就是软硬件一起决定的。
在我那个项目里,最终选择的tile大小大约是64×64,A切片约896字节,B切片约1024字节,C累加结果约1024字节,整体控制在几KB以内,可以塞进片上SRAM且不影响其他buffer。更关键的优化是双缓冲(double buffer)。具体做法是:计算单元算tile n的时候,DMA同时去加载tile n+1的数据,两者物理上使用两份SRAM。如果硬件只提供一套缓冲,编译器只能先等待加载完再计算,时间轴变成“加载、计算、空闲、加载、计算、空闲”,双缓冲可以把这个等待基本隐藏掉。设计指令和runtime时,一定要把这种算搬重叠的机制当成标配,否则芯片的持续算力离峰值算力会差一大截。
在一个真实跑出来的profile里,优化前时间占比大概是:数据加载50%、MAC计算25%、同步等待25%;加入双缓冲和合理分块之后,MAC计算占比冲到65%以上,同步等待压到个位数百分比。这个变化不是靠提高频率,而是通过改造数据的流动方式实现的,这就是软硬件设计里面“协同”两个字最实际的体现。
4.3 性能数据分析:从Roofline看你的芯片到底喂饱了没有
优化做到位没有,不能凭感觉,要用客观的工具去测量。对于AI芯片,Roofline模型是最直观的判断工具之一。它会画出一条“天花板”,横轴是计算强度,表示每读取一个字节能完成多少次计算;纵轴是实际可以达到的性能。根据芯片的峰值算力和片外带宽,可以知道这条天花板的斜率。当你测出一个算子实际运行点落在了天花板之下,就该去查是什么资源没释放干净。
再用前面的矩阵乘来算:计算强度大约等于34.4 GFLOPs除以76MB的访问量,也就是约450 FLOPs/Byte,INT8下大约是450 OPs/Byte。如果芯片的峰值算力是10 TOPS,片外带宽32GB/s,那么它的“转折点”在约312 OPs/Byte。这个算子的理论计算强度高于转折点,说明它理论上是可以跑到芯片的实际峰值区域的。如果你实测结果远低于峰值,那就证明软硬件协同已经出问题,要么是tile没切好导致有效带宽下降,要么是缓冲冲突导致DMA等待,要么是指令调度的同步开销太大。拿Roofline去逐层过一遍,通常能精准地定位问题出在哪个operator上。
我还建议硬件团队把profiling工具做成第一方能力,而不是事后再找。片上的硬件计数器至少能告诉你:MAC单元有效利用率、内存读写总字节数、DMA空闲时间、跨核等待时长。软件编译器、runtime和这些计数器一起工作,才可能让“性能不好”不再是一个黑盒。
5. 踩坑实录与排查手册:性能和精度问题的处理思路
5.1 算力阵列老是“等数据”:低利用率的典型场景
大概每个做AI芯片的人都会遇到这种情况:单个内核测试很好,跑到真实模型利用率突然掉到20%以下。最核心的坑往往不是全局带宽不足,而是局部数据调度没有重叠。比如某个算子先要等前一个操作的全部结果写完再开始,两个阶段之间整条流水线就是空的,硬件算力单元只能停在那边等待。如果是这样,优先去检查算子边界,看能否把可并行的算子错开执行,或者直接把中间结果留在片上而不是写进主存再读出来。
另一个低利用率的常见原因是跨核通信设计不合理。核A算完的数据要送到核B,如果A和B之间必须经过片外缓冲区再做一次写读,性能会灾难性下跌。我排查过的一个项目里,一个简单的concat操作让执行时间多了整整3倍,就是因为经过DDR绕了一圈。硬件上该做的是提供核间直连通路,软件上该做的则是尽量不让跨核数据搬移发生在算子边界。两者都需要在软硬件设计时提前预留,否则只能事后打补丁。
5.2 量化精度翻车:不只是选几个scale那么简单
硬件支持INT8,软件也做了量化,可模型跑出来的精度始终差几个点。多数情况下问题出在量化参数选取没有考虑到激活值的真实分布。有人用简单的min/max去定scale,遇到长尾分布直接会让小数值区域被压扁。更稳妥的做法是收集每个激活张量在代表性数据集上的分布,再用百分位数或KL散度去校准scale,让量化误差在实际使用范围内最小。这个流程听起来简单,但它需要工具链里能同步导出每个中间张量统计,软硬件设计时必须提前考虑。
累加器宽度也很关键。INT8乘法的结果累加时,如果累加器位数不够,或者软件在关键位置没有做中间提升到INT32的保留,精度就会因为数值溢出而崩掉。硬件层面要确保乘累加阵列的累加通道比输入位宽宽很多,软件层面则要检查生成代码里有没有自动地把累加扩展到足够位宽。踩坑以后我最想提醒的是:别只看最终精度差一点,要一层层去对输出特征图逐通道找偏差最大的位置,这样才能定位是量化还是硬件计算路径的舍入规则不一致。
5.3 编译器优化失效:先定位热点,再决定要不要改代码
很多时候明明编译器开了一堆优化选项,性能就是不涨。我见过有人反复改编译pass的顺序,结果白忙一场,因为瓶颈根本不在图优化层,而在内存分配的策略或kernel实现的细节。拿到性能计数器之后,先分清是“计算密集”还是“访存密集”还是“同步密集”。如果是访存密集,就去看带宽是否跑满,buffer是否足够双缓冲;如果是同步密集,再考虑算子边界、跨核通信或者runtime锁的问题。
定位过程中最有效的做法是用二分法:先只跑最耗时的单个算子,确认它的单算子性能能达到硬件峰值的一定比例;再逐步加入相邻算子,看性能掉在哪一步。这样一圈下来,到底是硬件机制不支持,还是软件没编排好,基本能分得很清楚。不要一上来就怀疑编译器生成的代码,先把“单算子基线”立起来,后面所有讨论才有参考意义。
5.4 一张实战排查速查表
| 症状 | 可能的根因 | 处理思路 |
|---|---|---|
| MAC利用率低,但带宽未占满 | 同步等待多、跨核通信慢,或tile太小导致循环开销大 | 加大分块尺寸,检查是否有跨核中间结果绕片外;在硬件计数器里看片内等待周期 |
| 带宽跑满但性能仍不达标 | 数据复用率低,计算强度低于机器转折点 | 通过tiling提高片上复用,做算子融合减少中间张量;或者降低频繁遍历数据的次数 |
| 单算子很快,串起来变慢 | 算子间缺少pipeline重叠,中间结果反复写回DRAM | 启用双缓冲,尝试在片上保留中间buffer;用fuse策略合并相邻算子 |
| 量化后精度明显下降 | scale校准不合理或累加溢出 | 用代表性数据统计激活分布,校准量化参数;检查累加器位宽和舍入模式 |
| 并发多路模型跑起来互相拖累 | runtime资源分配太粗,核间同步冲突 | 统一规划内存生命周期,按实际计算负载做多核任务切分,减少全局同步屏障 |
最后聊一点个人体会
硬件和软件看起来是两个工种,但在AI芯片项目里,最有效的组织方式是让做编译器的人和做微架构的人坐在一起,共享同一份性能模型和同一套profiling结果。我在某跨平台系统项目中见过一种协作模式:软件团队每周发布一份“硬件体验报告”,把每个关键算子的实测带宽、等待耗时、指令占比全部亮出来,硬件团队根据报告决定下一轮微调往哪个方向走。这一套机制比任何漂亮的架构文档都管用。
如果你也正打算启动一颗AI芯片或者维护一套AI芯片软件栈,我只有一个建议:尽早把软硬件接口冻结,并让真实模型从第一周就跑在仿真或快速原型上。表面看多花了一点时间,实际上能省掉后续几个月的推倒重来。软硬件设计不是先有硬件再做软件,也不是软件去将就硬件,而是在第一行代码和第一版RTL之前,就已经用数据把两者的边界定义清楚了。