☰
AI芯片软硬件协同设计:从架构定义到算子部署的实战指南
2026/10/19 1:09:20 网站建设 项目流程

1. 从一张芯片图纸到能跑模型,中间隔着整个软件栈

我做过几款AI芯片的软硬件协同,从架构定义阶段就把“下游软件能不能撑起来”当硬指标来卡,而不是芯片流片回来再找编译器团队补救。先说结论:AI芯片的软硬件设计,本质上是把模型、算子、指令集、内存层次结构和编译策略当作一个整体来规划,任何只谈算力多少TOPS、不谈可用性验证的方案,都会在落地时被打回原形。

很多人把“AI芯片”理解成一块带了很多乘法器的硬件加速器,真正决定这块芯片好不好用的,是它和上层软件栈之间的“咬合度”。比如同一个GEMM,硬件算力再高,如果数据搬运调度做得不精细,实际吞吐量可能只有峰值的20%;反过来,如果指令集和编译器之间接口设计得足够干净,用一块算力并不大的NPU也能跑出过得去的端侧模型性能。这就是软硬件协同设计的核心价值所在。

这篇文章适合三类人读:一类是刚进入芯片软件栈领域的开发者,想快速搞清从架构定义到算子部署的完整链路;第二类是写模型推理或训练框架、需要和底层硬件打交道的人,理解硬件侧约束能帮你写出更高效的调度策略;第三类是项目管理者,看完能建立对AI芯片研发节奏和排坑顺序的合理预期。本文不绑定任何具体芯片或厂商,谈的是通用方法和实操经验。

2. 硬件架构设计:真正决定性能的是数据搬运方式,不是峰值算力

2.1 算力宏大的陷阱:从算力公式看利用率差距

AI芯片架构评审时我最常问的一句话是:“这颗芯片在真实模型上的MAC利用率能做到多少?”绝大多数项目的立项PPT只写了“XX TOPS”,但这个数字是由时钟频率和MAC阵列规模直接算出来的理论峰值,真实跑模型时根本不可能打满。

举个具体的例子。一个标准的卷积层,需要的乘法次数约等于输出特征图元素数乘以卷积核大小。假设某NPU主频1GHz,MAC阵列为2x2排列共4个MAC,则理论峰值是4 GMAC/s,换算一下是8 TOPS,听起来很漂亮。但如果我们不做任何数据复用优化,每一拍都需要从DRAM搬出一块输入特征图和卷积核权重,DDR总线带宽很快就会成为天花板。通常一个MAC单元每拍需要输入一个激活值和一个权重值,也就是至少8字节,4个MAC就是32字节/拍,对应带宽需求32GB/s。但这只是不考虑复用时的极端结果,现实中通过行缓存和权重驻留可以把这个需求压低一个量级,能不能压则是架构设计的事。

我见过太多芯片的第一版架构文档里只写了“峰值算力3倍于上一代”,却完全没有提到输入缓冲区、权重缓冲区和行缓存分别配多大、数据复用路径怎么走。软硬件协同设计的第一课就是:用访存模型反推架构参数。合理顺序是,先用典型模型(ResNet级、MobileNet级、Transformer级)算出各类算子的访存强度,再根据目标带宽确定片上SRAM容量和计算阵规模,最后才是定频率。倒过来做的后果,往往是流片回来发现计算阵列喂不饱,芯片实际效果严重缩水。

2.2 计算核心选型:脉动阵列与SIMD的适用边界

AI芯片的计算核心大致可以分成三类:脉动阵列,适合卷机类规则计算;SIMD向量单元,适合Transformer里那些不规则但并行度高的算子;以及通用DSP核,负责处理控制流密集的分支逻辑。多数成熟方案的做法不是二选一,而是混合配置。

脉动阵列的思路是让数据像流水线上的工件一样从一个PE流向另一个PE,相邻PE之间只传部分和,减少从存储器取数的次数。这个设计特别适合矩阵乘:左边进来激活值,上面进来权重值,部分和再往右不断累加。但它的优点是它的缺点——数据一旦不规整,阵列利用率立刻掉下来。比如处理LLM里的GQA(Grouped Query Attention)时,不同头的形状并不完全一致,硬塞进同一个阵列就很容易出现空泡。

而SIMD向量单元就像一条并行处理的流水线,一条指令同时操作多个数据元素,处理softmax、LayerNorm、Gelu这种逐元素操作非常顺手。我们团队在后期的软硬件协同方案里,倾向于用一个小规模向量单元补齐脉动阵列处理不了的算子,调度器负责把整图切分成适合不同类型单元的Tile。整个分配策略在架构阶段就写进编译器的设计文档,这样硬件和软件从一开始就拥有相同的“算子分工共识”,避免后续互相对齐不了接口。

2.3 存储层级设计:片上SRAM是黄金资源,L2统一缓冲最核心

如果你把自己的数据在小本本上整理好了放在手边,和每次都要跑到库房翻资料来对比,就很好理解存储层级:寄存器相当于桌面,片上SRAM相当于手边的本子,DRAM相当于库房。不管计算阵多大,库房搬运速度永远是瓶颈,所以片上SRAM容量和分块策略决定了AI芯片的实用性能。

我参与过的一个项目,最初片上存储规划只有1MB,跑Alpha模型时激活值根本放不下,DMA反复搬运导致有效算力不到设计值的三成。后来强制在架构阶段把存储层级问题前置:典型网络的单层激活多大、残差连接要不要额外buffer、权重常驻空间占多少,全部作为硬指标评审。存储分层的架构思路是,L1缓存跟计算单元紧密耦合,放的是当前计算Tile的局部数据;L2统一缓冲是全局枢纽,所有DMA搬运都先经过它,同时承担着多核之间数据交换的集散地角色;再到L3,通常是可选的系统级缓存,用于吸收不规则访存。

L2缓冲设计的一个核心参数是bandwidth。它必须能同时服务输入DMA的预取、输出DMA的写回和计算阵列的读取。经验值是L2到计算阵列的读带宽至少是DRAM峰值带宽的两倍以上,否则一定会在某个瞬间出现计算阵列空转。而这个参数如果不到实际流片验证阶段,光靠仿真很难暴露,因为真实模型的访问模式比测试向量复杂得多——这就更加说明,架构设计阶段就要拿着真实网络做Cache命中率和DMA冲突分析,而不是拿着一个理想化访存模型验算两遍就说完成任务。

3. 软件栈设计:编译器和算子库是芯片的“第二架构”

3.1 不要把软件栈当作芯片流片后才启动的事情

软件栈通常包括四大块:编译器、算子库、量化工具链、运行时。一个通行的误区是硬件设计团队把RTL冻结后才把指令集手册扔给软件团队,然后期望6个月后软件就绪。实际上,编译器架构师应该在指令集定义阶段就和硬件工程师坐在一起,逐条指令讨论代价模型假设。

软硬件协同的设计顺序应该是:先梳理出目标模型的计算图,提取典型算子子图;然后根据这些子图确定指令集需要支持哪些“原生融合操作”,比如Conv+BN+ReLU在硬件中是否可以直接变成一个流水线指令,还是必须拆成三次读写;接着定义编译器内部IR和硬件指令之间的接口契约;最后才是RTL编码。如果IR设计得太抽象,比如完全照搬通用编译器的多级中间表示,又不针对NPU做后端优化,那编译器生成的代码质量和手写汇编通常会差出好几倍性能。

另一个关键点是指令集并不是越简单越好。为了保证芯片可编程性,需要设计一个足够表达复杂算子的VLIW指令格式,让编译器一次可以发射多个微操作。但VLIW的复杂度会直接推高编译器的指令调度难度,所以必须保持在“能力足够、编排可控”的中间地带。我踩过最大的坑是基于通用处理器那套“非法指令异常”逻辑来设计NPU指令集,结果在端侧部署时算子融合涉及的大量运行时判断直接拖垮了执行效率。正确做法是尽量把灵活性体现在编译期的shape推导和算子选择上,运行时只做参数传递和地址计算。

3.2 算子开发范式:手写汇编和算子模板库如何共存

算子库的构建有两种路线:一种是全部手写底层汇编,性能上限最高,但人力成本大到不可持续;另一种是提供一个参数化的算子模板框架,由编译器根据规则生成优化版本。实际产品需要两者共存。

以卷积算子为例,模板框架需要暴露几个关键参数:Tile大小、循环展开因子、DMA双缓冲深度、数据排布格式选择。编译器通过代价模型决定每个维度的取值,再调用模板生成对应的微内核。这么做的好处是换算法策略时不用重写汇编,比如从im2col切到winograd,只需要换一个算法包装器。手写汇编则保留给那些性能关键且对寄存器使用有极致要求的固定形状算子,像矩阵乘主循环,因为编译器自动分配寄存器时很难通过手写的精细度来贴合特定硬件PFU流水结构。

另外一个容易忽视的层面是算子验证环境应该与硬件仿真环境共用一套黄金向量。我们团队内部的做法是:每个写出的算子库都要在硬件仿真前先跑一遍数值黄金比对,数值类型(INT8、FP16、BF16)各自维护一套基线;除此以外,针对量化敏感层额外用随机输入做统计对比,防止硬件RTL改动后引入只在特定数值分布下出现的精度偏差。这一条建议在实际项目的联调阶段省了我大量时间。

3.3 量化工具链:不只是做PTQ/QAT,更要管住数值流

很多AI芯片软件栈最薄弱的环节是量化工具链。硬件宣称支持INT8,但实际部署时模型精度掉点,这个锅往往被归到“用户的模型太敏感”,其实是量化工具链错误处理了激活值分布。

常用的做法是PTQ,先跑一批校准数据,统计各层的激活值min/max和直方图,然后选取合适的scale和zero point。但问题是,仅按逐张量的min/max做对称量化在长尾分布模型上会有严重误差。正确做法是引入基于信息论的量化参数搜索策略,比如用KL散度、熵或均方差来挑选截断阈值,而不是简单按最大绝对值。这项工作要在验证阶段就接入量化敏感性分析。我见过一个端侧检测模型,只做了逐通道权重量化和逐张量激活量化,INT8掉点超过3个点,后来花半天时间给敏感层换成混合位宽精度,立刻减到0.5以内。

做量化感知训练(QAT)也一样,要在训练阶段就模拟硬件实际支持的舍入模式和近端截断行为,不要用一把理想化的伪量化配置训练,流片之后还得为数值行为差异重训模型。说白了,量化工具链的验收标准不是一个漂亮的精度报告,而是交付给算法团队的报错信息足够清晰,让他们知道到底哪一层对量化最敏感,以及为什么。

4. 从算子上板到性能拉满:一次真实NPU部署的全流程详录

4.1 部署主链路流程

以端侧NPU跑一个轻量检测模型为例,完整链路是这样的:

  1. 算法团队交付ONNX模型;
  2. 量化工具链完成PTQ并输出量化后的tflite格式计算图;
  3. 编译器前端完成图优化(算子融合、常量折叠、死代码消除);
  4. 编译器后端做子图切分,也就是把整个计算图切成硬件上某个计算单元可执行的子图,标出哪些归向量单元、哪些进加速阵列、哪些必须在CPU上回退;
  5. 每个子图经过指令调度器生成汇编级中间表示,再做寄存器分配和DMA搬运规划;
  6. 通过运行时(Runtime)加载到NPU设备上执行;
  7. 最后是性能分析工具输出算子上板耗时、DMA带宽占用和MAC利用率。

线段每一环都可能成为瓶颈。比如编译器前端没做Conv+BN+ReLU融合,后续每个算子会多出来一次额外访存;后端调度时DMA双缓冲深度不够,计算单元就要等预取的下一块数据进来。部署前的模型性能和上板后的性能通常差10%~30%,这几乎全是软件栈的编排问题。

以一个小型网络为例,单层推理耗时中,通常计算只占30%~40%,数据搬运占40%~50%,剩下的是算子和算子之间的调度开销。所以做性能优化时,第一刀永远是砍搬运,第二刀才是优化计算本身。这里列出一组我们某次实测数据的加速效果对比:

优化手段耗时变化瓶颈类型
Conv+BN+ReLU算子融合耗时降低32%显存搬运受限
DMA双缓冲深度从1调到3耗时进一步降低18%计算等待受限
权重常驻L2缓存耗时再降12%权重重读受限
将单分支串联改为并行调度耗时降低9%核间同步受限

第一优先级永远是减少访存开销,第二优先级才是堆计算效率。这套调优顺序几乎适用于所有NPU平台。

4.2 最难调的算子:Transpose和Reshape为何成为隐性瓶颈

Transformer类模型部署中,最难处理的是Transpose、Permute、Squeeze等形状变换算子。直觉上它们只是元数据变化,不改数据本身,但在NPU上它们往往触发整片存储的重排。因为我们通常要求张量在L2里以固定NHWC或NC1HWC0布局对齐,一旦计算图里出现transpose,编译器就必须生成一次DMA搬运。

我的经验是,在中间表示设计阶段就要让shape推断和布局推导两个pass直接参与“算子融合”。很多高性能实现的办法是把transpose融合到相邻算子的下标映射里,比如attention里的QKV合并计算,可以通过改变一个维度的步长直接把矩阵乘法输出重排成目标布局,而不是显式生成一个transpose节点。这个优化需要编译器前端和后端同时配合,属于比较进阶的IR改造工作,但对实际性能改善非常明显。

关于Reshape,一个常见陷阱是无脑把它视为内存视图变化,结果在硬件上因为需要连续地址所以强行走了一次拷贝。最好在编译中跟踪每个张量的存储布局是“逻辑连续”还是“物理连续”,分情况处理才不会白白浪费带宽。

4.3 多核并行切分:负载均衡比你想象的难

多核NPU部署时,常见的做法是把batch维或通道维切到各个核上跑。听着简单,实际会踩两个大坑:一个是切分点选不好导致不同核的访存冲突;另一个是子任务量差异大导致负载不均衡,有的核在空等,有的核在排队。

拿一次真实的多核部署举例,一个卷积层输出有64个通道,8个核各分8个通道,听起来很均衡。但输出通道的写回地址恰好都在同一DRAM bank上,多核同时写回时严重冲突,导致最终的核间加速比只有5.2倍而不是8倍。排查了三天才发现是bank冲突。后来在编译器里加入了一个地址扰动机制,对每个核的写回地址做偏移,让它们尽量均匀落在不同bank上,加速比从5.2提升到了7.4倍。

切分策略对编译器的主要要求是能够自动推导每个切分区域的访存范围和bank分布,而那种根据硬件地址做bank哈希的机制也一样重要。对软件栈的启示是,多核性能和单核性能完全不是一回事,如果你在架构阶段没有定义好DDR控制器到多核之间的仲裁策略,后期软件无论如何优化都会碰壁。

5. 性能分析的三个大坑与排查脚本思路

5.1 只看层耗时不看MAC利用率等于没分析

性能分析的第一件事是拿到每一层的耗时分布,这个往往只占分析工作量的5分钟。真正重要的下一步是判断这一层是计算受限还是访存受限。简单算一算就够:层计算量(MAC数)乘以2得到FLOPs,除以该层理论峰值算力(FLOPs/s),得到纯计算耗时;再拿层总访存量除以带宽得到纯访存耗时。哪个大,瓶颈就是哪个。

我见过不少团队在汇报中只列了一个指标“整模型35ms”,但完全没分析这35ms里有多少浪费。比如某层理论计算只需0.5ms,实际耗时却花了2.1ms,说明非常多的空泡在等待数据。如果不拆解开,就会误判为硬件太弱或者模型太大,搞不好就去改架构了。一个标准的分析脚本逻辑应该是:先自动提取每层的for循环边界和内存访问范围,然后算出理论计算量和访存量,最后再调用性能计数器实测耗时,三层数据统一汇总成一张表格。手动比对这些数据,你才能定位到问题出在计算流水、DMA调度还是数据对齐。

5.2 硬件性能计数器:你会用,它会救你;不会用,它会骗你

可编程的性能计数器通常有:周期计数器、指令发射数、MAC空泡数、DMA等待周期数、缓存缺失率等。但我必须提醒,不同工具链里每个计数器是“事件发生次数”还是“事件阻塞周期”,含义完全不同。我曾遇到过把DMA阻塞周期统计成DMA消息数量的报告,单位错了两三个数量级,导致整个性能判断颠倒。

排查思路是建立一套“耗时分层”的统计:第一层,应用层耗时,来源于运行时的timeline同步记录;第二层,任务调度层耗时,主要用于定位持锁等待、队列排队等情况;第三层,硬件执行层耗时,来源是性能计数器,用于区分计算和访存比例。三层数据放在一起,某一层次数据和另外两层对不上,自然就能定位问题所在。这里把常见问题的排查思路整理成表:

现象可能原因优先排查方式
单层耗时远大于理论计算时间DMA预取深度不够;存储bank冲突查看MAC空泡计数,统计DMA等待周期
多核加速比远低于核数负载切分不均;银行冲突;同步开销对比各核实际dispatch周期,查看写回bank分布
连续两层融合后没提速布局转换引发额外搬移编译IR查看插入的Copy节点,检查layout推导效果
INT8精度掉点严重量化参数按整层截断不合理;特殊层不应量化分层精度比对,看输出差异集中在哪一层
模型启动时连续小算子耗时高算子下发开销逐次累加将多个小算子patch为一个kernel,减少启动次数

5.3 从“差得离谱”到微调参数的经典调优现场

我再分享一个实际案例。部署一个MobileNetV3风格模型时,我的第一版编译器生成的代码整体性能只达到手写模型的约70%。定位因素是编译器默认把每个算子的输入输出都按NCHW布局存储,导致连续多个算子之间有大量布局转换带来的重排搬运。调整策略很粗暴但也有效:让编译器通过cost model为每个算子选择NHWC或NC1HWC0布局,最终全图只需在入口和出口做两次布局转换,单层耗时总和下降了40%。

再一个案例是量化后的Sigmoid输出。量化模型里Sigmoid往往要用查表实现,工具链默认在CPU上执行,每来一个batch都要同步等待,非常拖速度。后来我们把它改写为向量单元支持的近似多项式,精度损失不到0.1%,但耗时下降了5倍以上。这些优化不在硬件架构文档里写清楚,做软件的可不会自动知道该怎么配合。

6. 工具链选型和交付节奏:规模化的软件栈是怎么搭出来的

6.1 自研编译器还是基于开源框架改造

多数团队从基于开源的图编译框架起步,逐步定制后端,这比完全从零写一套NPU编译器科学得多,主要是生态成熟和调试工具丰富。但开源框架的设计初衷通常面向GPU类硬件,很多访存模型和调度算法跟NPU的结构对不上,需要大量后端适配工作。

一个小团队最合理的路径是:先用开源框架快速跑通一条端到端的算子部署样例,验证硬件可用性;然后逐步把NPU特有的融合规则、布局优化和调度策略下沉到自己的自定义pass里;最后再考虑替换官方不擅长的部分。完全自己从零写编译器的开销是团队leader很难向上汇报的,不仅开发周期要一两年,还难以吸引到熟悉各编译框架细节的人才。

6.2 芯片流片前后软件栈的交付顺序

我把软件栈的交付节奏拆成三个阶段,亲测能有效降低流片后软硬件联调的焦虑:

  • 流片前6个月:指令集模拟器先行。此时所有算子库开发都在模拟器上验证逻辑正确性,同时并行做性能评估模型的校准。目标是当RTL freeze时,算子库的C模型已经通过黄金向量验证。
  • 流片前3个月:C模型和RTL对拍。以RTL仿真器或FPGA原型平台的输出为基准,把算子库的期望行为和实际RTL行为做完整比对,重点是边界值和溢出行为。
  • 流片后1个月内:上板冒烟测试。先跑一个打印Hello式的最小kernel,再跑卷积主循环,最后跑完整模型,每步都检查寄存器和DMA配置是否符合预期。

很多团队把模拟器开发当作“帮助程序员先写代码”的工具,但实际上它最重要的价值是“先把硬件行为固化成规范文档的可执行形式”。有了这个,寄存器级别的配置错误在上板前就能暴露大半。

7. 我曾经踩过最狠的坑,和现在每次评审都先问的问题

做软硬件协同这几年下来,我反思最深的教训有几个。一个是前期过于关注单个算子的极致性能,完全忽略了全链路的易用性。结果模型集成时发现我们需要为每个新结构手写一个算子,还必须把所有寄存器配置暴露给算法工程师,项目进展变得极其缓慢。另一个是硬件模块冻结的时间点被不断推迟,编译器一直在追逐寄存器和指令集的漂移,导致后端调度器一改再改,整个软件栈的稳定性被严重拖累。

现在每个新项目启动时,我不管评审报告写得多漂亮,一定会先问三件事:第一,拿真实模型跑过完整的模拟器吗,MAC利用率是多少?第二,编译器团队是否参与了指令集定义的Review?第三,软硬件之间的接口契约是否有冻结日期表?如果这三项里有任何一个答案含糊,我基本能判断这项目后期一定会在“软硬件咬合”这个环节付出沉重代价。

最后分享一个非常实用的小技巧。在初期规划NPU指令集时,把“寄存器直接暴露给编译器”的程度设计成可配置的选项,这会让编译器在做指令调度时拥有更多灵活性。不要在架构阶段就把所有控制细节都封死在硬件内部,给软件留一道可组合的口子,等于给自己留了一条后期调优的路。当然,这条口子不能太宽,因为完全可配置的状态空间会爆炸,导致编译器无法在合理时间内完成高质量调度。好的软硬件协同设计,就是在硬件效率和软件可表达性之间找到那条最窄的平衡线,然后精确地守住它。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询