☰
AI芯片软硬件协同设计:从需求拆解到编译器调度的完整链路
2026/10/12 21:56:59 网站建设 项目流程

很多做AI芯片的朋友都问过我一个问题:软硬件协同设计到底有没有一个“标准流程”?我前几篇分别聊过量化、编译器和算子的底层优化,但在项目里把这些串起来的时候,往往会发现真正的瓶颈是“两侧定义接口”的那层功夫。第四篇正好借一个刚结束的参考项目做一次复盘,讲讲从需求拆解到驱动代码落地的完整链路里,软硬件双方到底是怎么互相“逼”出方案的。

先说清楚这篇的服务对象:你不是第一次看这个系列,已经了解AI芯片的基本构成,比如MAC阵列、片上SRAM、DMA和指令集大概是什么样的。你真正想搞明白的,是这些模块之间如何确定彼此的分工边界,以及为什么某次流片回来性能远远达不到理论值。这篇就用一个智能边缘场景的推理芯片设计过程来带出这些判断逻辑。

1. 从真实负载反推硬件规格:不能一开始就谈“算力”

做AI芯片最容易犯的错误,是先用一个TRm接着觉得“算力高就赢了”。实际上AI推理芯片的性能,从来不是靠峰值算力撑起来的,而是靠“在约束条件下让数据持续流过计算单元”。这个参考项目的目标场景是某类端侧多模态感知应用,包含一个约2亿参数的稀疏化视觉模型和一个喂给它的音频流后处理子网。

我们把负载拆开看,AI推理任务里最有价值的信息不是“这个模型有几十层”,而是每层的数据类型、张量形状、复用模式和访存比例。于是先做了一层算子级剖析。最终分析出四大类算子:常规卷积、深度可分离卷积、矩阵乘以及大量Elementwise融合操作,它们分别占整个模型迭代时间的45%、17%、28%和10%。

于是硬件设计的第一条约束就有了:不能设计成纯矩阵乘加速器,因为矩阵乘只占28%,其余需要SIMD化和流水化的支持。于是计算阵列被规划为由16x16的MAC阵列构成的“张量核”中心,外加一条独立的SIMD流水线来处理Elementwise和激活操作,并在缓存和连接上将“非张量指令”的延迟单独照顾。

由此得到的架构基准确立为:INT8峰值约16 TOPS,MAC阵列最高频率500MHz,权重与激活的片上平均复用率大于70%。这些数字不是拍脑袋定的,它们全是从模型里每个算子的输入尺寸、通道数和卷积核大小反推出来的。

1.1 算力数字的推导过程

我们取矩阵乘部分作为例子。假如某层输入是 2048x2048xINT8,要做相同维度的矩阵乘,MAC次数为 2048x2048x2048次乘加,再加上加法,一共大约17.2 GFLOPs。若要求整层在1.07毫秒内完成,所需算力就是 17.2G / 1.07ms ≈ 16.1 TFLOPs。注意这里已经把乘和加分别计入了FLOPs,这也是芯片规格书上常见的INT8 TOPS口径。

在设计MAC阵列规模时,我们确定频率500MHz,目标算力16 TOPS,那么每周期需要的整数运算次数是:

16e12 / 500e6 = 32000

也就是每个时钟周期要完成32000次运算。因为一个乘累加单元一个周期完成2次运算(一次乘法一次加法),MAC数量取 16000 个。再按16x16的模块方案,128个模块横着排就得到了设计规格。这个推导过程很枯燥,但它是后续一切讨论的基础。

1.2 片上存储与数据搬运的匹配计算

MAC阵列的胃口只是半边。按上面的带宽估算,如果所有数据都从外部DRAM来,一个周期内需要16KB+的输入,在500MHz下约为8TB/s的带宽,这显然不现实。于是我们把“可复用的数据留在片上”作为第二条硬约束。

卷积层里,每个输入特征图都会被多个输出通道共用,如果编译器生成的调度能把这种复用显式暴露给DMA控制器,就能把DRAM实际需求降到算力需求的十分之一以下。我们的优化目标是:让片上SRAM容量刚好覆盖一层计算所需的全部中间激活和权重切片,而不是想办法堆内存。在那个设计里,选择了6MB的片上SRAM并配合三级DMA预取流水,效果是实际DRAM带宽需求降到了约12GB/s。

这里有个设计心得:片上存储的容量选择不是做加法,而是做“最极端层的满足度测试”。把模型里所有算子的活数据逐一过一遍,选出峰值内存最大的三层,用它们的范围去设定SRAM的下界,这样既不浪费面积,又不会出现某层跑不过去的尴尬。

2. 计算阵列与数据通路:为什么用MAC阵列全覆盖反而累赘

定完主结构,接下来是最容易引发软硬件两侧“长期拉锯”的部分:计算阵列到底要多“通用”。我们的软件团队最初希望支持任意形状的二维卷积、转置卷积、空洞卷积,甚至把上采样也吃进去。这个诉求听起来合理,但硬件团队逐条估算晶体管代价后发现,如果全部做成硬件直通,核心面积至少膨胀35%,而利用率可能下降15%以上。

所以收敛的结果是:硬件支持的是规则卷积和矩阵乘的基础原语,剩下的非规则操作由编译器拆解成由多个规则操作组合而成的序列。这个分工背后其实是一条清晰的原则:硬件做“确定性的高频动作”,软件做“灵活但低频动作的解释器”。

2.1 PE阵列内部的数据流选择

PE阵列有两种典型的数据复用方式:输出固定与权重固定。输出固定意味着每个PE轮流处理不同输入通道的加部分和,权重寄存器可以长期保持。权重固定更适合卷积,因为权重可以被多个输入位置复用,减少片上寄存器文件的周转。

我们最终选的是二者混合:卷积带专用数据广播网,矩阵乘则切换成输出固定模式。硬件上看起来只是多一组布线,但编译器侧就得维护两种数据流描述。用代码看会更直观,比如在编译器的IR阶段,我们会给调度器增加一个数据流策略标记:

enum class DataflowType { OutputStationary, WeightStationary, RowStationary }; struct ConvConfig { DataflowType flow; int tile_width; int tile_height; int unfold_ratio; };

这个结构体是整个编译后端的核心输入之一。调度器看到WeightStationary就会优先把权重块持久化到各PE寄存器,然后让特征图数据按行广播。如果把unfold_ratio设为1,意味着不做im2col展开,卷积滑窗按照硬件广播网直接映射,这能省掉大量中间张量搬运。

2.2 片上网络与流水线的双重视角

数据通路不只是电缆。我的习惯是把片上网络当作“缓存协议的一部分”而不是“简单的互联”。在5M SRAM分库后,每个存储库的端口带宽不同,而卷积和矩阵乘对存储带宽的需求也不同。这时候片上网络的作用不是平均分发,而是依据编译器的访存模式做动态路由。

我们做了一次很有意思的实测:将网络拓扑从全互连改成“两簇局部互连加一根高带宽双向干道”,面积少花了18%,而典型模型性能只掉了约4%,最终计算单位功耗性能反而提升了11%。这个结果直接说明了,AI芯片数据通路不是越连通越好,而是越匹配越好。

软件侧为配合这条总线结构,需要在地址映射上做“通道亲和性”优化:把特征图A分配到与MAC阵列簇0直连的SRAM库,矩阵乘B矩阵分配到簇1直连库,而从干道另外一侧加载权重的DMA则单独占一根通道。这些地址规划虽然不起眼,但在最终性能验证时,明显减少了总线仲裁冲突,流水线平均气泡率从12%降到了3%。

3. 指令集与软硬件边界:ISA设计是双方博弈的结果

如果说架构是骨架,指令集就是软硬件双方共同起草的合同。这个合同没法独断地由一方完成。硬件团队的本能是把每类算子做成一条封闭指令,这样前端逻辑好写;软件团队的本能则是把所有细节暴露给编译器,这样优化空间最大。两边都有道理,但我们最终以“可测试性”为收敛标准设计了轻量可组合的ISA。

这版ISA不是长指令字也不是纯RISC风格,而是一个带显式流水阶段标记的类RISC指令集,每条指令在同一拍内只描述一个动作,由编译器负责调度组合。这样做的最大好处是每一个行为都能被性能计数器精准追踪,对后端的功耗和性能回归特别友好。

3.1 指令规划的颗粒度

我们在指令集设计上反复纠结的是:计算指令要不要携带形状信息。早期方案里,矩阵乘指令带上了M、N、K三个维度,看起来能一次性算完一整块,但编译器每次改变tile大小就要换一种指令,代码生成的组合空间几乎是无穷的。后来改成基础语义指令,比如:load、store、conv、matmul、elementwise、pool、activate,形状信息全部由寄存器配置描述。

这样做之后,一条矩阵乘的伪汇编看起来是这样的:

config_tile M=32, N=16, K=8 load_weight acc_buf, weight_ptr, size=4096 sync_dma wait_all matmul acc_buf, act_buf, weight_buf, mode=INT8 elementwise act_out, acc_buf, relu store out_ptr, act_out, size=512

注意sync_dma这条指令,它实际上在硬件里触发一级流水屏障。编译器在调度时会尽可能把后续的计算指令填入DMA搬运的空档,但sync_dma总是被放在真正依赖数据的指令之前。这个机制让硬件可以乱序执行一部分操作,但边界清清楚楚,软件也在同步时知道数据已经就位。

3.2 编译器后端的指令调度策略

调度策略在工程上完全是统计驱动。我们要先在模拟器上采集指令组合的耗电和延迟,再生成一个调度成本表,作为后端启发式搜索的评价函数。具体算法我们采用了列表调度的加强版,优先级函数里同时包含DRAM排队深度、SRAM复用距离和PE空闲率,并在搜索窗口内做贪心回溯。

这一个模块是整个项目里改动最频繁的。原因在于硬件设计稳定后,性能爬坡几乎全靠编译器。曾经有一版调度器因为权重大小设置不合理,把卷积层内部的数据复用顺序打乱了,导致DRAM模组访问量增长了40%,功耗直接超标。后来我们加了一个约束:任何调度方案在生成后都要过一次“DMA冲突检查器”,如果某个Bank被连续访问超过6次,就强制插入一条等待指令并重新调整顺序。

这个检查器其实反映的是一种设计哲学,硬件是确定性的,那么软件就必须在确定性之上做显式的约定。与其让硬件做复杂冒险预测,不如让编译器把所有冒险装进自己的口袋里。这点对AI芯片尤其成立,因为AI负载规律性极强,统计特征比通用处理器明确得多,硬件做预测器反而是一种浪费。

4. 量化校准与编译器约定的跨层联动

量化从来不是模型训练之后单独跑一遍的工具,它是芯片设计流程里贯穿始终的契约。在硬件端,INT8乘累加器的输出累加精度决定了后续是否需要额外的rescale指令;在编译器端,每一层算子的输入输出scale和zero point都必须对齐。很多团队等到片子回来才开始调量化,结果发现某些层位的分布跟编译器假设的完全不同,此时再改硬件已经来不及了。

我们在这个项目里采用的做法是:量化信息统一放在一个runtime配置表里,编译器直接读取这个表生成中间表示,而不是让量化模块生成一堆散落的scale参数。这张配置表通常长这样:

算子输入scale输出scalezero point运算模式
conv10.0078120.015625128per-tensor
matmul_30.0039060.0019530per-channel
depthwise_20.0156250.015625128per-channel

这样设计有一个直接的好处:编译器在做算子融合时,可以顺着这张表判断“当前接口处是否需要rescale”。例如两个卷积直接融合时,如果第一个的输出scale恰好等于第二个的输入scale,编译器就能省掉一层quantize和dequantize操作。实测下来,有这种联动设计的模型比没有联动时,整体INT8推理时间平均缩短了22%。

4.1 校准数据集的选择陷阱

校准数据集的选取是最容易翻车的地方。有人喜欢用训练集的子集,有人随机挑几张自然图,都不太对。校准的目的是让各层的激活分布保持和真实推理接近,因此要覆盖所有典型的数据形态,最好还包括一些边界值。比如我们的多模态模型里有一路输入是低照度图像,如果不把这类样本加进校准集,激活值在低亮度区间会被严重截断,导致INT8推理精度掉1.5个点以上。

量化后的精度验证也要分场景。我一向主张对每个输出张量做逐bin的误差统计,而不是只看最终精度。因为逐层误差统计能直接告诉编译器,哪一层需要保留高精度累加,哪一层可以激进地全部采用INT8计算且不损失任何性能。编译器根据误差表给出的“敏感度名单”动态切换某些算子的实际执行精度。

4.2 量化参数如何指导硬件scale模块设计

硬件上我们为每个PE旁边配了一个轻量rescale模块,它的作用不是做复杂的浮点转换,而是做一次定点移位加截断。如果量化配置表里统一使用2的幂次scale,硬件甚至可以省掉乘法器,直接用移位器完成转换。为了得到这个简化,量化训练阶段我们对scale参数做了幂次约束。

有人担心幂次约束会让模型精度不可接受,实际测试显示:在端侧视觉模型上用幂次约束做QAT,精度损失通常在0.2%以内,换来的是硬件面积节省约8%和每次rescale功耗减少60%。这个交换在边缘设备上非常划算。这个细节当初差点被砍掉,后来量化团队用三组实验数据说服了硬件团队,也让我更加确信:跨层联动的前提是两边都得知道对方有哪些“便宜的参数约束”可以利用。

5. 实测性能低谷的定位方式:一次完整的性能排查

片子回来后第一次跑全模型,硬件利用率只有理论峰值的43%。当时大家第一反应是MAC阵列太慢,但性能计数器把每一类指令的活跃周期打印出来后,真相完全不同。

查表的顺序是这样的:

统计项数值问题暗示
MAC阵列有效周期43%计算本身大量空泡
DMA等待周期31%数据到达不及时
访存冲突周期18%多簇争抢带宽
同步等待周期8%依赖链过长

DMA等待占了31%,很明显是“预取距离不够”。我们排查后发现,编译器生成的DMA请求总是等到当前tile用完才开始发下一tile的搬运指令,也就是没有做流水预取。这属于调度算法没有把“双缓冲”特性显式建模。解决方案是修改调度器的依赖分析,把所有load指令允许提前到上一tile的计算完成之前,而这时硬件上的SRAM双缓冲就能自动吸收两个tile之间的切换。

这个改动在模拟器上看只提升了不到10%,因为模拟器里的DRAM延时就假设得很好。但上了真实芯片后,DMA等待周期从31%降到了9%,MAC阵列利用率直接抬到76%。后续继续把片内缓存替换策略改成“按层调度”,利用率才最终到80%。这也印证了那句话:模拟器里看起来细微的差别,到真实硬件上会被放大很多倍。

5.1 性能计数器的设计要前置

后面我复盘出一个重要经验:性能计数器在设计阶段就要规划好,不能等片子回来再补。为此我们在每个软件可感知的执行单元上挂了至少三类计数器:指令分发计数、数据接受计数、写回计数。编译器还在IR阶段就定义了每个算子映射到计数器上的预期值,跑完就能自动比对偏差。

相比通用处理器,AI芯片的数据流规律让这套做法非常顺手。你在做下一块芯片时,千万不要把性能分析工具留到最后。否则所有怀疑都会落成“是不是MAC不够多”,而不是回到指令调度的根因上去。没有计数器,调优就是盲人摸象。

5.2 现场调试的软件工具配合

性能定位还需要一套能查看片上中间张量的调试工具。我们在驱动层加了一个快照接口,可以让软件把SRAM里任意地址的内容拉出来与仿真器对比。有一次定位精度问题时,就是通过快照接口发现某个通道的scale值跟配置表不一致,根源是驱动在加载权重时把scale数组的字节序搞错了。这种bug,如果光靠终端输出指标根本发现不了。

快照接口的成本不高,但它在片上调优阶段节省的时间是巨大的。这类工具建议在每个子系统都放一个,包括DMA描述符、SRAM地址映射、MAC阵列中的寄存器配置。芯片回来后,你一定会因为多看了这些底层细节而少熬几个通宵。

6. 面向下一代架构的软硬件职责再分配

项目结束后,我们把这套设计里的几条分工经验抽象成了方法论,准备用在下一版芯片上。这几天反复想,觉得最值得分享的是下面几条:

  • 硬件只做“高频且确定性”的操作,其余全部交给编译器解释组合。
  • 指令集设计的终极标准不是功能完备,而是“能查性能计数器就能定位问题”。
  • 量化参数的语义必须单一,不干净的数据会导致编译器做出不可预判的优化。
  • 模拟器友好未必等于芯片友好,凡是有条件,尽量在早期跑通真实链路测试。

我亲身经验是,AI芯片的软硬件协同设计没有终点。每个版本都会在某个没想到的地方漏出水来,但只要双方的接口契约足够清晰,所有问题都能快速收敛到某一侧去解决。就我个人而言,把这个接口契约文档当成“项目里优先级最高的产出物”来维护,效果比任何架构评审都好。下一篇有机会的话,我想把编译器后端里一套很少公开的“按层功耗感知调度”方法单独拿出来聊透,那部分我们踩坑最深,收获也最大。

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

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

立即咨询