☰
AI芯片软硬件协同设计:算力、指令、存储与精度的工程实践
2026/10/12 1:31:27 网站建设 项目流程

某次流片回来,硬件组顺利跑通了启动代码,软件组却在联调模型时发现,端到端吞吐只有标称峰值的三分之一。会议室里最有意思的一幕是:硬件说自己所有单元都正常,软件说自己调用了该调的指令,最后问题落到了“两者之间的口头约定太多,书面契约太少”。从那以后我一直在想,AI芯片的软硬件设计从来不是两拨人各自把工作做完再对接,而是一整套关于数据流、指令语义、存储行为和数值规则的共同设计。这个系列的第七篇,我想把过去几年在几个AI加速器项目里反复踩过的胶水层问题整理成几个剖面:算力口径、指令契约、内核映射、多核存储、精度语义,以及流片前的软件仿真环境。如果你正在做NPU、ASIC加速器或者工具链,这篇文章应该能帮你省掉不少联调期的返工。

1. 算力数字的游戏:为什么标称TOPS经常兑不了现

1.1 很多人算峰值时忘掉的乘法因子

我之前参与过一个图像类加速器项目,芯片手册上写了一个很漂亮的算力数字。拆开看,它无非就是MAC阵列规模乘以主频再乘以二。假设一个乘加操作被当成两次运算,理论公式就是:Peak = MAC数量 × 主频 × 2。比如256个MAC、1GHz、8bit,峰值约512G Ops。这个说法本身没错,但PPT上的数字往往没有告诉你它假设了数据全部在片上,假设没有bank冲突,假设不需要同步,甚至假设了稀疏跳过。

实际执行一条矩阵计算时,数据要从片外存储一级一级搬到MAC阵列里,每一级搬运都可能停顿;tile尺寸没算好,缓存会反复换入换出;两个核同时访问同一个存储体,流水线直接空转。我测过不少真实算子,单算子利用率通常落在40%到70%之间,低的时候甚至不到30%。这些数据不是硬件坏了,而是硬件本来就充满了等待。所以我建议团队把“峰值”这个词改成“可到达率”,也就是说在指定输入形状、指定精度、指定数据布局的前提下,芯片实际能交付多少有效计算。

1.2 算力口径要成为一种交付物

我习惯在项目启动阶段就做一份算力审计表,而不是等性能不达标时再来对质。这张表必须写清楚:模型形状、batch大小、精度、是否开稀疏、是否包含搬运耗时、是否包含核间同步、重复次数、编译选项。可以说,它是一把软硬件双方共用的尺子。

审计项必须填写说明
模型与输入图像尺寸/序列长度/batch输入形状直接决定tile划分
精度模式FP32/FP16/INT8等不同精度下MAC吞吐不同
数据来源片上缓存/片外存储是否要算加载时间
稀疏开关开/关稀疏模式对峰值影响极大
同步次数每次同步等待时间多核场景不能忽略
端到端口径含驱动/不含驱动统一发布标准口径

有了这张表之后,再看到有人宣称“利用率达到80%以上”,我会先问一句:这里面包不包括DMA耗时和同步等待?根据我的经验,绝大多数漂亮的指标都绕开了硬件真正难解决的存储墙问题。把算力口径定为交付物,并不是为了刁难某一边,而是让两边在争论开始之前就使用同一套计量方式。

2. 指令集与微架构的一次性契约:算子语义背后的硬件义务

2.1 指令粒度:从标量到张量级

硬件设计团队经常纠结一个问题:能不能把某个卷积流程做成一条超大指令?做AI芯片,标量指令主要用于地址计算和控制流,向量指令处理位宽对齐和元素级运算,张量指令负责矩阵和卷积的主计算。一条张量指令的好处是译码开销被大幅摊薄,一次就能描述整块阵列行为;坏处也很明显,一旦新算子不符合这条指令的表达模式,编译器只能把计算打散回标量或向量通道,性能会断崖式下跌。

我比较倾向的方案是,大指令不要试图覆盖所有算子,而是拆成“数据视图”和“计算视图”两部分。数据视图负责描述从哪取数据、什么形状、什么stride、是否允许转置;计算视图负责描述做什么运算、累加方向、精度选择、是否套用偏置。这样新算子来了,只要它能映射到已有数据视图和计算视图的组合,编译器就能继续享受张量指令的吞吐。否则每来一个新算子就加一条硬件指令,指令集很快会变得比软件更难维护。

2.2 硬件可编程边界,以及为什么不能全靠硬连线

有些团队问,常见的归一化、激活函数能不能都固化成硬件模块?我做过多个加速器项目,结论是硬连线部分越多,前期性能确实越好看,但软件模型一换代,新算子用不上那部分面积,等于浪费。正确的思路是定义“可编程算子内核”加“有限原语组合”:硬件只需要把最常用、最稳定的原语做扎实,比如矩阵乘、转置、循环内加偏置、非线性近似查找表、归一化系数乘法器。把这些原语的组合规则写清楚,编译器就能拼装出更多算子,硬件不用为每个算子单独做一套电路。

这里要特别提醒:指令集手册不是写给硬件团队看的,它的第一用户应该是编译器工程师和算子库开发者。因此每个指令条目至少要写清楚允许的输入布局、边界条件、未对齐时怎么办、是否允许原位写。凡是手册里含糊的字段,将来都会成为软件优化路上的雷。我在多个项目里见过因为一句“行为未定义”导致编译器团队不敢做激进优化的情况,最后只能靠手写汇编绕过,效率损失非常大。

3. 算子库与内核映射:高性能内核不只是编译器自动生成的功劳

3.1 编译自动优化和手写内核各负责哪一层

AI芯片的算子库最头疼的问题是覆盖度。编译器自动生成适合铺开,能够基于模板快速覆盖十几类乃至几十类算子,保证模型整体先跑通;但自动生成的代码往往在数据搬运和指令流水编排上不够精细。手写内核适合“二八原则”里的头部算子,比如矩阵乘、卷积、注意力主计算,同一个算子写几个版本也不算夸张。

我在项目里的分工习惯是:先用编译器自动生成把整个模型跑通,建立正确性和基线性能;然后对着性能热点一个一个替换手写内核。手写内核里最值得花时间的是双缓冲和异步DMA,把搬运和计算重叠起来,就这一条往往能让内核性能提升30%到40%。有些团队一上来就手写全部算子,结果覆盖度不够,模型在长尾算子上被迫走低效路径,整体性能反而不如“先自动生成、再热点打磨”的方案。

3.2 数据布局和分块:同一个模型形状,性能可能天差地别

硬件最舒服的是连续内存读写,但主流训练框架里常用的数据布局到具体硬件上未必连续。做内核映射时,经常要把通道维调整到最内层,或者按片上SRAM容量去切分tile。我曾经对比同一个全连接层,用256×256的tile可以完整塞进片上存储,换成512×512之后存储冲突率上升,性能直接掉了一半。这个现象说明性能问题不一定是算子本身慢,很可能是layout和tiling选错了。

从工具链设计角度看,编译器IR必须支持布局的推导与传播。如果每一层都要把数据重新拷贝成硬件喜欢的布局,那布局转换本身就会变成新的性能瓶颈。我见过不止一个项目把layout转换写得很随意,等到联调一测,模型推理时间的15%都花在memcpy上。这种浪费完全不该出现,但如果没有可感知的IR层信息,它就很难被发现。

4. 存储墙与多核互联:软硬件设计在“搬数据”这件事上正面交锋

4.1 先算清带宽预算,再谈芯片要放多少核

看一个AI加速器,不要先看算力,先看它每个算力单位摊到多少字节。假设一个核每秒执行F次运算,如果每个元素都要从片外读取,而总线带宽只有B字节每秒,那么大量指令会卡在等待数据上。工程上可以定义一个“计算访存比”,即每字节访存对应多少次有效运算。当一个算子的计算访存比低于某个阈值时,可以直接判定为访存受限,不再花精力优化MAC调度,因为这个算子的瓶颈永远在搬数据上。

这个判断能省掉很多无效调优时间。我经历过一个项目,内核团队反复调整矩阵单元的流水级数,但性能纹丝不动,后来测了一下片外带宽占用率已经到了95%,才知道问题根本不在计算单元而在存储接口。如果早点做带宽预算,我们完全可以改用压缩格式或者重新划分数据切分,把有效带宽利用率提上去。

4.2 静态调度和动态调度各有各的账

多核芯片最怕的是核间同步。静态调度在编译期就把任务分配好,运行期执行顺序固定,好处是同步开销低、行为可预测,缺点是遇到输入形状变化就不好办。动态调度能适应不规则的输入,但加锁和原子操作多了,死锁风险也会跟着上来。这里有一张我常拿给团队看的对比表:

对比项静态调度动态调度
执行顺序编译期确定运行期决定
同步开销低较高
场景适应性适合形状固定适合形状多变
可预测性高,容易复现低,依赖输入
死锁风险低中高
调试难度简单复杂

我的倾向是默认静态调度,只有当模型输入形态无法提前确定,或者单个算子执行时长抖动太大,才把部分流水线阶段切到动态调度。动态区间的边界应该选在数据量相对小、不会积累锁冲突的位置。这个边界到底划在哪,软硬件架构师和运行时开发必须一起定,不能等代码写完了再回头加。

5. 精度策略与数值语义:量化方案最怕在硬件快冻结时才拍板

5.1 混合精度策略会反过来要求硬件资源

做AI芯片的人常把支持FP16和INT8挂在嘴边,但实际工程里,一个模型的不同算子可能分布在FP32、FP16、BF16、INT8之间。编译器要做的选择不只是位宽,还包括计算单元的组织方式。假设矩阵阵列只做了INT8密集乘法,那么FP16占比高的模型在硬件上就会很难受,要么走低效的标量通道,要么反复做精度换算。

更好的设计是让同一个阵列支持多种精度模式,并且在切换时尽量复用同一组寄存器路径。精度模式的切换不是简单地把位宽从8改成16,因为MAC阵列的排列方式会跟着变:8bit模式可以排更多并行度,16bit模式MAC个数减半,数据对齐规则也可能不同,这些都必须由编译器在代码生成前就能感知到。硬件团队如果在架构定义阶段就听取软件团队对混合精度的真实需求,后面的算子库效率会高很多。

5.2 饱和、截断、溢出:数值语义是写进指令集手册的

还有一块常被忽略却非常关键的内容,是数值在跑到边界时硬件到底怎么处理。软件量化仿真通常按“饱和”建模,而硬件为了节省面积可能直接做截断,两者在中间层看起来平均误差不大,但经过几十层堆叠之后,模型输出漂移就可能从0.1%变成百分之好几。我因此要求项目提前定义每种精度的溢出策略、是否支持清零、是否支持舍入模式,这些都必须写进指令集手册。

软件仿真器要和RTL行为做一致性对比,否则模型在仿真器里跑出的漂亮结果根本不能代表上板后会发生的状况。很多项目直到联调阶段才发现一组很奇怪的错误峰值,最后定位到只是某个累加器溢出的处理不一致。这类问题一旦流片才被发现,几乎没有快速补丁,只能靠编译器强制插入修正指令,性能和代码复杂度都会明显恶化。

6. 虚拟原型、性能模型与PMU:让软件在芯片回来之前先干活

6.1 虚拟原型是工具链的共同底座

很多项目把软件工具链的完整启动时间放到流片之后,这是我见过最危险的项目管理决策。芯片还没回来,编译器和算子库根本不知道性能瓶颈在哪里,等到真芯片点亮再开始调软件,周期会非常紧张。我们的做法是在架构定义阶段就同步做三级模型:第一级指令集模拟器,只验证功能;第二级周期近似模型,能估算流水线停顿;第三级周期精确模型,只覆盖核心算子。

前两级不需要做得很准,但必须跑得快,让编译器能大量迭代;最后一级周期精确模型要尽量对标RTL,能输出关键硬件事件。有了这个底座,流片前工具链已经把大部分bug磨掉了,软件第一周就能跑通完整模型。这个流程看起来前期投入大,实际比“先流片后写软件”省太多时间,因为架构改动在仿真期往往只是改参数,流片后再想改就基本不可能了。

6.2 硬件事件计数器是给软件优化的仪表盘

最后一块拼图是片上的性能监测单元。我在芯片定义阶段就会要求把关键事件从RTL里引出来,比如MAC空闲周期、存储缺失、等待数据、同步等待、bank冲突、标量流水线气泡。编译器和算子库开发者靠这些计数器判断当前瓶颈到底在哪。有一回性能只有预期的六成,PMU显示等待数据占总等待时间的58%,把tile切小之后内存压力立刻下降,吞吐就回到了八成以上。

如果不看计数器,我们很可能会去改MAC流水线,方向完全搞错。所以PMU不是芯片调试结束后就能拆掉的附属品,而是软硬件设计闭环里的一等公民。编译器最好能把PMU反馈变成自动调优的输入,跑几轮性能测试后自己判断是继续增大tile还是减小tile,这也是AI芯片工具链走向成熟的一个标志。

后来我给自己定了一条很简单的规矩:新项目开工第一周,先把指令集手册的第一读者写成编译器工程师,把性能指标的统计口径写成表格,把每种精度的溢出行为写成文档,再把PMU事件列表加进架构设计目标。这四件事不需要很长的工期,但它能让软硬件两个团队在真正对簿公堂之前先有同一套语言。我自己好几个项目里,那些后来花几周甚至几个月去补的坑,几乎都能从这四件事里的某一件上找到根源。这个习惯看起来笨拙,却是很多年代价换来的。

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

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

立即咨询