1. AI芯片软硬件协同设计的整体思路拆解
1.1 为什么软硬件必须一起设计
做AI芯片这行的人都有一个共识:硬件性能再强,如果软件栈跟不上,实际跑出来的吞吐量可能连理论峰值的30%都不到。我见过太多团队花了大半年流片,结果驱动和编译器拖了后腿,芯片在板子上跑得还不如上一代GPU。所以“软硬件协同设计”不是一句口号,而是从架构定义阶段就要并行推进的工程方法。
核心逻辑其实不复杂:AI负载有很强的规律性,卷积、矩阵乘、激活函数这些操作的访存模式和计算密度是可以提前建模的。硬件设计者需要知道目标模型的计算图长什么样,软件设计者需要知道硬件的存储层次、指令延迟和并行度。两边信息不对称,结果就是硬件做了一堆用不上的加速单元,软件写出来的调度策略跟硬件流水线对不上。
我个人的经验是,在项目启动的前两周,硬件和软件团队必须坐在一起完成三件事:确定目标算子集合、确定数据精度策略、确定片上存储的分配方案。这三件事定下来,后面的工作才有共同的语言。
1.2 从计算图到硬件指令的映射路径
一个典型的AI模型从框架导出到芯片执行,中间要经过好几层转换。以常见的推理场景为例,路径大致是这样的:
- 训练框架导出模型文件(ONNX或类似格式)
- 图优化器做算子融合、常量折叠、死代码消除
- 量化工具把FP32权重转成INT8或FP16
- 编译器把优化后的图拆成硬件支持的指令序列
- 运行时按指令序列调度执行,管理数据搬运
每一层都有坑。比如算子融合阶段,如果融合策略太激进,可能导致中间结果精度损失累积;如果太保守,硬件利用率上不去。量化阶段更麻烦,某些层的激活值分布不均匀,直接做对称量化会丢掉大量信息,需要逐通道量化或者混合精度策略。
硬件这边要配合的是:指令集是否支持融合后的复合算子、片上缓存是否能容纳融合后的中间张量、DMA引擎是否能在计算的同时预取下一批数据。这些问题如果在架构定义阶段没有对齐,后期改动的代价极大。
1.3 方案选型的几个关键决策点
做AI芯片软硬件设计,有几个绕不过去的选型决策:
第一,指令集架构选RISC-V还是自研。RISC-V的好处是生态在逐步完善,工具链有开源基础,适合中小团队快速起步。自研指令集灵活性最高,但编译器、调试器、仿真器全部要自己搞,人力投入至少翻倍。我参与过的项目中,如果团队规模在50人以下,通常建议基于RISC-V做领域扩展,而不是完全从零开始。
第二,数据流架构选权重 stationary 还是输出 stationary。权重stationary适合卷积层占主导的负载,权重加载一次可以复用很多次;输出stationary适合全连接层和矩阵乘,输出结果留在片上累加。实际芯片往往两种模式都支持,但面积和功耗会上去。需要根据目标模型的结构分布来做取舍。
第三,片上存储用SRAM还是寄存器文件。SRAM容量大但访问延迟高,寄存器文件延迟低但面积大。常见的做法是分层:近计算单元用寄存器文件做操作数缓存,中间层用SRAM做张量缓存,最外层用DRAM做模型权重存储。每一层的容量和带宽需要根据目标算子的数据复用率来算。
2. 核心模块的硬件设计细节与实操要点
2.1 矩阵乘加速单元的设计取舍
矩阵乘是AI芯片里最核心的计算模块,占了整个芯片面积的40%到60%。设计这个模块时,最关键的参数是PE阵列的维度。假设目标算力是16 TOPS(INT8),每个PE每个周期完成一次乘加,主频跑在1GHz,那么需要的PE数量是:
16 TOPS / (2 ops per MAC × 1 GHz) = 8000 个PE
8000个PE如果排成100×80的二维阵列,面积大概在8到12平方毫米(取决于工艺节点)。但实际设计中不会把所有PE都做成全功能,通常会有部分PE支持稀疏计算或者混合精度,这会增加面积但提升灵活性。
实操中有一个容易被忽略的点:PE阵列的输入输出带宽必须匹配。如果PE阵列每个周期需要从SRAM读取256字节的操作数,但SRAM的带宽只有128字节/周期,那PE就会有一半时间在等数据。我通常会用下面的公式来验证带宽是否够用:
所需带宽 = PE数量 × 每PE每周期操作数字节数 × 主频
以8000个PE、每个操作数1字节、1GHz为例,所需带宽是8000 GB/s。这个数字非常夸张,所以实际设计中必须通过数据复用来降低带宽需求。比如权重stationary模式下,权重只需要加载一次,带宽需求可以降到原来的1/10甚至更低。
2.2 片上存储层次与数据搬运策略
片上存储的设计直接决定了芯片的实际利用率。我习惯把存储分成四级:
| 存储层级 | 典型容量 | 访问延迟 | 用途 |
|---|---|---|---|
| 寄存器文件 | 几十KB | 1周期 | 操作数缓存 |
| 近端SRAM | 几百KB到几MB | 2-4周期 | 张量块缓存 |
| 远端SRAM | 几MB到几十MB | 8-16周期 | 完整特征图缓存 |
| DRAM | GB级别 | 100+周期 | 模型权重和输入数据 |
数据搬运的策略核心是双缓冲和预取。双缓冲的意思是:当计算单元在处理当前数据块时,DMA引擎已经在后台把下一块数据搬到备用缓冲区。这样计算和搬运可以重叠,理论上的加速比接近2倍。
预取的距离需要根据DRAM的延迟和计算时间来计算。假设DRAM延迟是200个周期,一个数据块的计算时间是500个周期,那么预取距离至少要是1个块,最好提前2个块开始预取,以应对DRAM带宽波动。
注意:双缓冲的缓冲区数量不是越多越好。每增加一个缓冲区,SRAM面积就增加一份。通常2到3个缓冲区是性价比最高的区间。
2.3 指令集与微架构的配合
指令集设计最容易犯的错误是“为了灵活而灵活”。我见过一些芯片定义了上百条指令,结果编译器根本用不起来,实际执行时还是退化成几条基础指令的循环。好的指令集应该覆盖高频算子,同时保持解码逻辑简单。
以卷积为例,一条高效的卷积指令应该包含这些字段:输入特征图地址、权重地址、输出地址、卷积核尺寸、步长、填充方式、激活函数类型。如果这些信息都要用多条指令分别设置,指令开销会吃掉大量计算周期。
微架构这边要配合的是指令流水线的深度。流水线太浅,主频上不去;太深,分支预测失败和指令依赖带来的停顿会增加。对于AI负载,分支预测不是大问题(因为计算图是静态的),但指令依赖需要仔细处理。常见的做法是用记分牌或者Tomasulo算法来做乱序执行,但面积开销不小。如果目标负载的指令级并行度不高,顺序执行加软件流水线反而是更经济的选择。
3. 软件栈的实现与优化实操
3.1 编译器后端的算子映射
编译器后端的核心任务是把优化后的计算图映射到硬件指令。这个过程分三步:
第一步,算子合法性检查。遍历计算图中的每个算子,确认硬件是否支持。不支持的算子需要回退到CPU执行,或者用多个支持的算子组合实现。这一步的输出是一个带标注的计算图,每个节点标记了执行位置。
第二步,算子融合与分块。把相邻的、可以融合的算子合并成一个复合算子,然后根据片上存储容量把大张量切分成小块。分块的大小需要同时满足两个约束:块内数据能放进近端SRAM,块的计算时间足够覆盖数据搬运时间。
第三步,指令生成与调度。为每个块生成具体的指令序列,然后做指令调度,把DMA指令和计算指令交错排列,最大化重叠度。调度算法可以用列表调度或者基于整数规划的精确调度,前者快但效果一般,后者慢但能拿到最优解。
我实测下来,算子融合带来的性能提升通常在1.5到2.5倍之间,分块策略的影响在1.2到1.8倍之间,指令调度的影响在1.1到1.4倍之间。三个优化叠加,整体加速比可以到3到5倍。
3.2 量化策略的选择与精度补偿
量化是AI芯片软件栈里最影响精度的一环。INT8量化能把模型大小压缩到原来的1/4,计算吞吐量提升2到4倍,但精度损失需要仔细控制。
常见的量化方案有三种:
- 对称量化:把浮点范围映射到[-127, 127],零点是固定的。实现简单,但对激活值分布不均匀的层效果差。
- 非对称量化:零点可以偏移,能更好地覆盖激活值的实际分布。但计算时需要额外的零点修正,硬件开销略大。
- 逐通道量化:每个通道用独立的缩放因子。精度最好,但缩放因子的存储和读取会增加带宽压力。
我的经验是:权重用逐通道对称量化,激活值用非对称量化,第一层和最后一层保持FP16。这样组合下来,常见分类模型的精度损失可以控制在0.5%以内,检测模型在1%以内。
如果精度还是不达标,可以启用混合精度:对精度敏感的层用FP16,其余层用INT8。混合精度的调度需要编译器支持,实现复杂度会上去,但效果立竿见影。
3.3 运行时调度与内存管理
运行时是软件栈里最贴近硬件的部分,负责把编译好的指令序列喂给芯片执行。核心功能包括:任务队列管理、内存分配与回收、多核同步、异常处理。
内存管理有一个容易踩的坑:碎片化。AI负载的内存分配模式是“大块分配、频繁释放”,如果直接用通用的内存分配器,跑一段时间后就会出现大量碎片,导致大块分配失败。解决方案是用内存池:预分配几个固定大小的池子,每个池子管理一种典型大小的内存块。这样分配和释放都是O(1)的操作,碎片问题也解决了。
多核同步方面,如果芯片有多个计算核,核间通信的开销不能忽略。常见的做法是用共享内存加信号量的方式做同步,但信号量的争抢会成为瓶颈。更好的方案是用硬件队列:每个核有自己的任务队列,核间通过队列传递数据指针,避免锁竞争。
4. 常见问题与排查技巧实录
4.1 性能不达预期的排查路径
芯片跑起来但性能只有理论值的30%,这是最常见的问题。排查路径我通常按下面的顺序走:
第一,检查DMA带宽利用率。用性能计数器看DMA引擎的忙碌比例。如果低于70%,说明数据供给不足,计算单元在等数据。这时候要检查预取距离是否够大、缓冲区数量是否够多、DRAM访问模式是否连续。
第二,检查PE阵列利用率。如果DMA利用率正常但PE利用率低,说明计算任务的分块或调度有问题。可能是块太小导致流水线启动开销占比高,也可能是算子融合不够导致指令开销大。
第三,检查指令缓存命中率。如果指令缓存命中率低于90%,说明指令序列太长或者循环体太大。解决方案是缩小循环体、增加指令缓存容量、或者用循环展开减少分支。
第四,检查同步开销。多核场景下,如果核间同步频繁,同步开销可能吃掉20%以上的性能。解决方案是增大每个核的任务粒度,减少同步次数。
4.2 精度异常的定位与修复
量化后精度掉点严重,定位起来比较麻烦。我通常用逐层对比的方式:把量化模型和浮点模型在相同输入下的中间层输出拿出来对比,找到误差最大的层。
误差来源通常有这几个:
- 激活值动态范围估计不准:用校准集重新统计,增加校准样本数量
- 权重分布有长尾:对长尾部分做截断,或者用逐通道量化
- 累加器溢出:检查累加器的位宽是否够用,INT8乘加通常需要32位累加器
- 激活函数量化误差:ReLU类函数问题不大,Sigmoid和Tanh需要查表或者分段线性近似
修复策略按优先级排:先调校准集,再调量化粒度,最后考虑混合精度。大部分情况下,前两步就能把精度拉回来。
4.3 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 性能只有峰值30% | DMA带宽不足 | 看DMA忙碌计数器 | 增大预取距离、增加缓冲区 |
| PE利用率低于50% | 分块太小 | 看PE空闲周期数 | 增大分块、减少流水线启动次数 |
| 精度掉点超过2% | 量化策略不当 | 逐层对比中间输出 | 调整校准集、改用逐通道量化 |
| 运行一段时间后变慢 | 内存碎片 | 看内存分配失败次数 | 改用内存池 |
| 多核性能不线性 | 同步开销大 | 看核间同步等待时间 | 增大任务粒度、减少同步 |
| 指令缓存命中率低 | 循环体太大 | 看指令缓存miss率 | 缩小循环体、循环展开 |
4.4 几个只有踩过坑才知道的细节
第一个坑:DMA的地址对齐。很多DMA引擎要求源地址和目的地址按特定字节对齐(比如64字节),如果不对齐,性能会掉一半甚至直接报错。编译器在分配内存时就要保证对齐,运行时也要检查。
第二个坑:SRAM的bank冲突。如果PE阵列同时访问SRAM的同一个bank,会产生冲突,访问延迟翻倍。解决方案是把数据按bank交错存放,或者用多端口SRAM。前者需要编译器配合,后者面积开销大。
第三个坑:温度对时序的影响。芯片跑在高负载下温度会上升,时序可能变差,导致需要降频。如果散热设计没做好,实际持续性能可能只有峰值的60%。这个问题在架构定义阶段就要考虑,留足够的时序余量。
第四个坑:驱动程序的上下文切换开销。如果运行时频繁在用户态和内核态之间切换,开销会累积。解决方案是把常用的操作(比如任务提交、内存分配)做成用户态驱动,避免系统调用。
5. 软硬件协同验证的方法与工具
5.1 仿真验证的层次与策略
AI芯片的验证不能只靠流片后的实测,必须在设计阶段就做充分的仿真。仿真分三个层次:
第一层,算法级仿真。用Python或C++写一个行为级的模型,验证计算图的正确性和量化精度。这一层速度最快,可以跑完整的模型,但完全不考虑硬件时序。
第二层,周期级仿真。用SystemC或Verilog写一个周期精确的模型,验证指令调度、数据搬运、流水线冲突。这一层速度慢,通常只跑关键算子或者小规模模型。
第三层,FPGA原型验证。把RTL综合到FPGA上,跑真实的驱动和编译器。这一层速度比周期级仿真快几个数量级,可以跑完整模型,但FPGA的主频通常只有ASIC的1/5到1/10。
我的建议是:算法级仿真每天跑,周期级仿真每周跑,FPGA原型验证在每个里程碑节点跑。三层验证的结果要交叉比对,确保一致性。
5.2 性能建模与瓶颈分析
性能建模的目的是在流片前预测芯片的实际性能。我通常用一个简化的分析模型:
实际性能 = 峰值算力 × PE利用率 × DMA利用率 × 指令效率
PE利用率取决于分块策略和流水线设计,通常在60%到85%之间。DMA利用率取决于预取策略和DRAM带宽,通常在70%到90%之间。指令效率取决于指令集设计和编译器优化,通常在50%到80%之间。
三个因子乘起来,实际性能通常是峰值的20%到60%。如果建模结果低于20%,说明设计有严重瓶颈,需要回去改架构。如果高于60%,要么是模型太乐观,要么是设计确实优秀。
瓶颈分析用Roofline模型最直观:把算子的计算密度(ops/byte)和硬件的计算带宽比画在一张图上,一眼就能看出是计算瓶颈还是访存瓶颈。计算密度低的算子(比如深度可分离卷积)通常是访存瓶颈,需要优化数据复用;计算密度高的算子(比如标准卷积)通常是计算瓶颈,需要增加PE数量。
5.3 流片后的Bring-up流程
流片回来后的Bring-up是最紧张的阶段。我通常按这个顺序推进:
- 电源和时钟检查:确认所有电源域正常上电,时钟树没有异常。
- JTAG连接:通过JTAG读取芯片ID,确认芯片能正常响应。
- 内存测试:往SRAM和DRAM写入测试图案,读回来对比。
- DMA测试:跑一个简单的DMA搬运任务,确认数据通路正常。
- 计算单元测试:跑一个小的矩阵乘,对比结果和预期值。
- 指令集测试:跑一段简单的指令序列,确认流水线正常。
- 完整模型测试:跑一个真实模型,对比精度和性能。
每一步都可能卡住。我遇到最多的问题是时序违例导致的偶发错误,这种问题最难查,因为不是每次都复现。解决方案是加余量:电压稍微调高一点,频率稍微降一点,先保证功能正确,再逐步优化。
6. 一些个人体会与后续扩展方向
做AI芯片软硬件设计这些年,最大的体会是:架构定义阶段的一周,抵得上后期的一个月。很多性能问题、精度问题、验证问题,根源都在架构定义时没有考虑清楚。硬件团队和软件团队如果各做各的,最后拼在一起大概率出问题。
另一个体会是:不要追求极致的峰值算力,要追求极致的实际利用率。我见过太多芯片标称算力很高,但实际跑模型时利用率只有20%到30%。与其把PE阵列做大,不如把存储层次和调度策略做精。实际利用率每提升10%,效果比峰值算力翻倍还明显。
后续如果继续深入,有几个方向值得投入:一是稀疏计算的支持,利用模型剪枝后的稀疏性进一步提升有效算力;二是动态精度调整,根据输入数据的难度自适应切换精度;三是多芯片互联,把多个小芯片组合成大芯片,绕开单芯片的面积限制。
这些方向目前都有团队在做,但离成熟还有距离。如果你正在做相关的工作,建议先从单芯片的软硬件协同做起,把基础打牢,再考虑更复杂的方案。