AI 芯片早就不是停留在纸面上的概念了。这几年我前后参与过几款NPU IP和边缘推理芯片的软硬件协同设计,从最初的架构评估到最终流片回来跑通模型,整个过程踩过的坑、悟出来的道理,比看十篇学术论文都管用。今天这篇算是个开篇,把AI芯片软硬件设计这条线上的核心思路、关键环节和实操方法串一遍,后续再按子主题逐层展开。
先说清楚这篇文章能给你什么。如果你正准备入行芯片设计、算法移植或者嵌入式AI方向,想知道一颗AI芯片从需求到落地到底经历了什么;或者你已经在一线写RTL、调编译器、搬算子,总觉得软硬件之间隔着一层窗户纸——这篇文章就是捅破那层纸的。我不打算堆概念,而是按真实项目中“从架构定义到软硬联调”的顺序,把每个环节里最关键的决策逻辑和最容易翻车的地方讲透。
1. 内容整体设计与思路拆解
1.1 为什么“软硬件协同设计”这件事绕不开
很多刚接触AI芯片的朋友,第一反应是“这不就是硬件上堆一堆乘法器嘛”。真这么简单,市面上就不会有那么多流片回来性能打不过CPU团队移植方案的翻车案例了。
一颗AI芯片能不能真正发挥算力,取决于三件事:架构上算力单元怎么组织、存储层级怎么设计、软件栈能不能把模型高效映射到硬件上。这三件事互相咬合,任何一环掉链子,整颗芯片的实际利用率都可能只有纸面峰值的百分之二三十。我见过一款宣称8 TOPS的NPU,跑实际检测模型时帧率还不如一颗3 TOPS的竞品,原因就是编译器生成的指令流让MAC阵列频繁空转,DMA搬运和计算完全没有重叠。
所以软硬件协同设计不是项目管理上的口号,而是技术路径上的必然选择。硬件架构的每个决策,都要回答“软件好不好映射”的问题;软件栈的每层设计,都要回答“硬件资源怎么才能被榨干”的问题。这个思路贯穿从需求定义到量产的全过程。
1.2 从“算力焦虑”到“系统效率”的认知转换
行业里有个普遍的误解:算力数字越大芯片越强。真实情况是,AI芯片的价值单位是“有效算力”,也就是在真实模型、真实数据精度、真实内存带宽约束下能跑出来的吞吐量。
这里有个很直观的类比。一台配备了高性能发动机的跑车,如果变速箱匹配稀烂、轮胎抓地力不足,实际圈速可能被一台普通性能但整车调校优秀的车吊打。AI芯片就是这个道理——MAC阵列是发动机,存储系统和数据通路是变速箱和轮胎,编译器是换挡逻辑。你做软硬件设计,本质上就是在做整车调校,而不是单一零件的堆料。
基于这个认知,我在需求定义阶段从来不看单纯的TOPS指标,而是盯三个参数:峰值算力、实际可达利用率、单位功耗有效算力。利用率目标通常定在70%以上,低于这个值就说明架构或软件栈有系统性缺陷,后面无论怎么优化都是打补丁。
2. 硬件架构设计:算力、存储与数据流的三角平衡
2.1 MAC阵列与数据流:不同计算模式的取舍
AI芯片的核心计算单元是MAC(乘累加)阵列。但同样是一堆MAC,排列方式不同,性能差异巨大。业界常见的三种数据流是权重固定(Weight Stationary)、输出固定(Output Stationary)和输入固定(Input Stationary),三者的本质区别是“谁在片上待着不动,谁被反复搬运”。
我举个具体例子。假设你有一个256×256的MAC阵列,跑一个3×3卷积。如果采用权重固定的数据流,权重留在片上,输入特征图和部分和在阵列间流动,这样可以最大化复用权重,减少片外带宽压力。对于卷积核权重重用率高的算子,这种模式效率很高。而全连接层和Transformer里的矩阵乘,输入和权重都需要大量复用,此时更适合采用输出固定或行静态数据流。
实际项目中,我倾向于在架构里同时支持至少两种数据流模式,通过配置寄存器切换。代价是控制逻辑复杂一些,但换来的是对不同算子更高的适应度。这里有个关键技巧:在设计数据流之前,先拿目标模型集跑一遍算子分布统计,看看卷积、全连接、矩阵乘、向量算子分别占多少比例和多少运行时间。数据流的选择必须跟着这个统计走,而不是跟风选一个所谓的主流方案。
2.2 存储层级设计:带宽和容量的动态平衡
存储设计是AI芯片最容易出问题的地方。我见过不少团队在架构评审时把重心全放在MAC阵列性能上,结果系统级仿真一跑,发现带宽根本喂不饱算力,最后推倒重来。
片上SRAM容量与带宽是一对矛盾:SRAM越大,能放下的权重和中间激活越多,数据复用越好;但SRAM面积和功耗都很大,容量做上去后物理设计压力陡增。这里有一个经典的计算方式:假设你的NPU峰值算力是P TOPS,在INT8精度下每周期需要消耗的数据量至少是 P × 2 / 时钟频率 字节每秒(乘累加一次要读两个操作数)。如果目标利用率是80%,片上SRAM带宽至少要达到 1.6 × 2P / fclk,而如果复用率不行,这个带宽需求还会成倍放大。
在低功耗边缘芯片项目里,我一般把片上存储分成两级:一级是紧贴MAC阵列的寄存器文件或小块缓存,用于存放当前计算窗口的数据;第二级是较大的共享SRAM(几百KB到几MB不等),用于缓存权重分块和中间特征图。两级之间用高带宽的内部总线连接。这个设计能有效降低对片外DRAM的依赖,而片外DRAM接口则采用AXI或类AXI协议,支持AXI QoS以保证多主设备访问时的带宽公平性。
2.3 从RTL到SoC集成:芯片设计中的关键流程
硬件架构定了之后,落地到实际芯片需要经过完整的RTL设计、验证和SoC集成流程。RTL设计阶段,我习惯先把控制通路和数据通路分开写——数据通路专注于乘法器、加法器树、累加器的流水线设计,控制通路专注指令解析、状态机和中断逻辑。分开写的好处是时序收敛时能精准定位关键路径,而且验证时可以分别做定向激励。
验证环节,除了传统的UVM验证环境之外,我强烈建议从RTL设计早期就引入“参考模型对比验证”。具体做法是:用Python或C写一个功能完备的NPU参考模型,RTL的测试平台跑一条指令序列,参考模型也跑同样的序列,两者输出逐周期或逐块对比。这个方法在定位编译器和硬件之间的语义不一致问题时,效率极高。
SoC集成时最容易被低估的是中断和同步机制。NPU通常作为一个带有DMA能力的主设备挂在系统总线上,它计算完一个任务后要能主动通知CPU。这里的同步机制如果设计得不够灵活,比如只支持单一中断、没有完成队列,后面前端框架跑大数据流时会频繁出现CPU空转等待。我在实际项目里会坚持在NPU里实现一个硬件完成队列——每次任务完成写入一段内存队列,CPU通过轮询队列来感知任务状态。这个设计在大模型推理场景下好处非常明显,CPU不需要每次等待单次中断,可以批量处理多个已完成任务。
3. 软件栈设计:让硬件跑满的隐形引擎
3.1 编译器前端与算子映射:图优化和数据布局
很多人以为AI芯片的软件栈就是把PyTorch模型转换一下就能跑。实际上,从框架模型到芯片指令之间隔着一座大山,这座山的名字叫编译器。
编译器前端负责接收ONNX、PyTorch或其他格式的模型,做计算图级别的优化。这个阶段的优化包括算子融合、常量折叠、死节点消除、并行分割等。算子融合对AI芯片性能的影响巨大——以卷积+BN+ReLU为例,如果分成三个独立算子执行,中间结果都要写回DRAM再读出来,访存开销比计算开销还大。融合之后,中间数据全部留在片上SRAM,性能能提升两三倍不止。这不是什么高深技巧,而是整个软件栈里性价比最高的优化手段之一。
图优化之后是算子到硬件指令的映射。这里最大的坑是数据布局。由于MAC阵列计算时需要把输入特征图切块、向量化,如果数据在内存中的排布方式与硬件读取模式不一致,DMA搬移就会变得非常低效。我在设计硬件时就会定义好“推荐的NHWC与NCHW存储格式”,并且让编译器在图优化阶段输出一份内存布局规划,而不是等内核编译时再临时调整。软硬件坐在一起定这个数据布局规范,是我参与过的项目中反复验证过最有价值的协同事项之一。
3.2 指令集与CodeGen:设计指令就是设计硬件接口
AI芯片的指令集设计,本质上是在定义软硬件之间的唯一契约。指令集定得太粗,硬件灵活性受限,编译器做不了精细调度;定得太细,指令开销大、译码复杂,硬件面积和功耗都不划算。
从我实践的角度,一套好用的NPU指令集至少包含几类指令:配置类(设置输入输出地址、尺寸、精度模式)、计算类(矩阵运算、向量运算、激活函数)、搬运类(DMA加载/存储)、同步类(屏障、等待、信号量)。其中最容易做坏的是配置类指令——有些设计把几十个寄存器绑定在一条长指令里,结果每个算子内核都要花几十个周期写配置,计算本身的周期反而被淹没。更合理的做法是支持“预取配置”:下一层算子要用的配置在当前算子计算期间就并行加载,这个机制通常叫配置预加载或double-buffer配置模式。我在做微架构设计时,特意在指令流水线上留出一个专用的配置通路,就是为了实现这种重叠。
CodeGen阶段,即从内部IR生成具体指令序列,是另一个细致活。我要求编译器后端按“数据块生命周期”来做调度:先把计算图按SRAM容量切分成多个块,每个块内紧密排列计算指令和DMA指令,让DMA搬运前一个块数据的过程与当前块计算重叠。这样做下来,DMA与MAC阵列的重叠率通常能稳定在85%以上。如果没有这个块级流水调度,DMA空跑等待的概率会高很多。
3.3 运行时与AI框架适配:最后一公里的集成
软件栈的上层是运行时库和框架适配层。运行时库负责设备管理、内存管理、任务队列提交,而框架适配层把PyTorch/TensorFlow的算子请求转成NPU的算子调用。
这里的核心工作是设备内存管理。NPU的片上SRAM和片外DRAM资源都有限,运行时需要实现内存池化、内存复用和地址映射管理。我在项目里一律不用简单的malloc/free,而是为NPU实现一个专用的内存池:按“生命周期”分类,长期存活的权重和短期存活的中间激活分开管理,短期内存池在每次算子执行完立刻释放复用。这个优化在跑分割、检测这类多阶段模型时,能有效减少内存碎片和分配开销。
框架适配层的另一个关键点是异步执行模式。默认情况下,框架是同步调用的——算子提交后等硬件执行完再返回。这会严重降低多算子流水效率。我采用的方案是做一个异步任务队列:框架层把算子请求封装成任务结构体,丢进队列就返回;运行时后台线程负责从队列取任务、提交给NPU、等待完成回调。同步接口保留给需要强制同步的少数场景,绝大多数算子走异步路径。实测下来,对于包含几十个算子的模型,端到端推理延迟能降低20%到35%。
4. 实操过程与核心环节实现
4.1 从需求到架构选型的五步流程
第一步是收集并清洗目标模型集。把你要跑的模型都收集起来,转成统一的中间表示,导入性能分析工具统计计算量、参数量、激活量、算子类型分布。这一步至少花一周时间,但性价比极高。
第二步是建立带宽模型。根据模型集的统计结果,估算不同SRAM容量下的片外访存量。方法是模拟“存储块+计算块”的执行模型:SRAM容量决定了数据复用窗口,复用窗口越大,片外访问越少。这一轮就能画出容量-带宽曲线,架构师根据面积预算在曲线上选点。
第三步是数据流与MAC阵列规模选择。这一步要和编译器组一起评估,因为不同数据流对应的指令模型和调度复杂度完全不同。我可以给一个直观参考范围:对于主打视觉模型的小型芯片,128到512个MAC乘累加单元比较常见;面向大模型推理的数据中心芯片,规模会大几个数量级。
第四步是定义指令集和存储映射规范。这一步输出的是软硬件契约文档。我坚持由同一个人主导编写这份文档,避免硬件和软件各写一份最后对不上。文档至少要定义:指令编码格式、寄存器地址映射、存储格式、DMA描述符格式、中断和错误码体系。
第五步是进行系统级性能评估。用周期近似仿真器或FPGA原型,把前三步的方案跑一遍典型算子集合,量化实际利用率和带宽占用情况。如果和预期目标差距大,回到第二步迭代。这个五步流程走完,架构基本定稿,后面再改的成本就会成倍上升。
4.2 一个NPU内核的完整计算流程拆解
以一颗小型NPU为例,完整跑一个卷积算子的流程是这样:CPU通过配置寄存器写入该算子的输入地址、权重地址、输出地址、卷积参数、精度模式;NPU控制器解析配置,启动DMA去DRAM搬运权重到片上SRAM;DMA搬运权重的同时,NPU的MAC阵列开始计算已经就绪的数据块;每个数据块计算完成后,部分和写入片上SRAM或直接写回DRAM;所有块计算完毕,控制器写完成队列并触发中断,CPU感知后回收内存。
这个流程中,DMA搬运与MAC计算的重叠是性能关键。我在RTL里设计了一个“DMA请求队列”,MAC阵列每完成一个数据块,就立即通过队列发出下一个DMA预取请求。请求队列深度一般取2到4,太深占用SRAM资源,太浅在数据依赖链较长时容易断流。这个设计在FPGA原型上验证过,对卷积、矩阵乘这种规则算子,DMA和计算的重叠率可以做到接近95%。
关于精度模式,我在这个系列后续要单独展开。这里只说一点:INT8量化不是简单把权重从FP32截断,而是要做校准、做逐张量或逐通道的缩放因子。硬件上需要支持对输入和权重的动态缩放操作,通常是一条独立的scale指令。没有这条指令,推理精度往往会掉得没法看。
4.3 软硬件联调:跑通第一个模型的关键经验
流片回来后或FPGA原型上跑通第一个真实模型,是整个项目里最激动也最揪心的时刻。我第一次带领团队做软硬联调时,模型跑出来的结果全错,排查了一整天才发现是编译器对一组边界像素的处理和硬件对补零逻辑的理解不一致——硬件按左补零右补零分别处理,编译器却统一补零。这类问题靠看代码很难发现,必须靠逐层数据比对。
我总结了一套高效的联调方案:先用单算子模式跑一个极小网络,比如只含一个3×3卷积的网络,输入用固定随机整数且不做量化,逐层比对输出。确认通过后,换成完整的小模型做端到端比对。这里注意比对时要考虑数值精度差异,FP32和INT8的结果本来就会有微小偏差,要设定一个合理的容差范围,比如按元素相对误差不超过1%来判定。跑通了这一步,再上真实应用模型,此时的调试范围就只集中在算法层面了。
软件联调环境上,我强烈建议在Host机器上搭一个“指令级模拟器”,它和RTL同时维护、同步更新。软硬件联调中的大多数问题都能先在模拟器里复现,毕竟仿真器里边可以任意插入检查和打印,而在真实的设备或FPGA上抓内部信号要痛苦得多。这个模拟器本质上是一个可执行的指令集规范——它跑过的模型越多,软硬件之间的语义契约就越可靠。
5. 常见问题与排查技巧实录
5.1 硬件侧的高频问题和定位方法
问题一:MAC阵列利用率上不去。这类问题的典型表现是跑算子时,计算单元空转、功耗曲线明显偏低。优先查DMA是否有断流,也就是SRAM中待计算数据是否时常为空。另一个常见原因是指令发射速度不足,控制器的译码和配置加载占了太多周期。定位方法是给MAC阵列加性能计数器,记录空转周期数和原因。
问题二:片上SRAM数据被意外覆盖。这通常来自DMA地址计算错误或编译器为两个算子分配了重叠的缓冲区。我专门在家目录维护过一份“内存踩踏案例集”,十条里有九条是编译器对SRAM地址取模逻辑与硬件缓存行边界不一致导致的。解决思路是软硬件同时遵守同一个“地址对齐规范”,并且在一处统一校验。
问题三:时序收敛困难。AI芯片里MAC阵列和SRAM往往是全芯片功耗密度最大、时序最紧的区域。我常用的方法是给MAC阵列做树形布局,让乘法器树的每一级都在物理上保持相近的距离,同时把多个SRAM宏单元分散放置而不是聚集成一大块,这样能有效缓解局部拥塞。
为了让你在排查时有个着手点,我整理了一个小表:
| 现象 | 可能原因 | 优先排查动作 |
|---|---|---|
| 利用率低 | DMA断流或指令发射慢 | 检查性能计数器和DMA队列深度 |
| 结果错误但数值接近 | 量化缩放因子错误 | 对比单算子输出与参考模型 |
| 结果严重错误 | 数据布局或补零逻辑不一致 | 检查编译器生成的内存地址和硬件补零单元 |
| 偶发卡死 | 中断或完成队列丢失 | 检查同步屏障和中断状态寄存器 |
| 性能越调越差 | 缓存命中率极低 | 分析内存访问trace,调整分块策略 |
5.2 软件侧的高频问题和解决思路
编译器生成的代码质量不稳定,是软件侧最常见的问题。同一个模型,换一种输入分辨率,性能可能突然掉30%。这通常是因为分块策略对形状参数敏感。我的做法是在编译器里加一个“分块搜索器”——对每一层算子列举几种候选分块方式,估算访存量和DMA次数,自动选最优。虽然增加了编译时间,但换来的是稳定可控的性能表现。
另一个常见问题是驱动层的内存泄漏。NPU任务提交频率很高,如果每次提交都分配一次DMA描述符而不及时释放,跑几分钟内存就见顶。解决方法是引入描述符池,预先分配一批固定大小的描述符,任务完成后回收复用。这个优化可以说立竿见影。
算子库的精度问题也很常见。特别是那些手写的特殊算子,比如RoPE位置编码、FlashAttention里的online softmax片段,容易出现边界条件处理错误。我的习惯是给算子库建立神经网络的“算子树回归测试集”,每次修改后跑全量回归,确保修一个算子不破坏其他算子。
5.3 跨界协作:软硬件工程师如何高效配合
软硬件协同项目最容易扯皮的是责任边界。出了bug,硬件说软件地址算错了,软件说硬件时序踩坏了,来回拉锯浪费大量时间。我在项目里用两个机制解决这个问题。
第一,建立“接口行为定义”文档,把指令行为、存储一致性、中断语义、错误处理流程全部白纸黑字定下来。任何跨模块问题先查文档,文档没有覆盖的再升级讨论。这比靠口头约定高效得多。
第二,推行“双方共同维护仿真测试用例”。硬件工程师写的定向测试,软件工程师要能看懂并用于回归;软件工程师发现的新边界case,要能反哺到硬件验证环境里。实践下来,这个做法的收益非常明显——很多深层次bug都是软硬件双方交换用例后暴露出来的。
还有一个不算技术的提醒:软硬件联调一定要尽早开始,不要等RTL完全冻结才开始写驱动和编译器。我的经验是,指令集规范定稿后就可以启动软件栈开发,先把编译器跑在指令集模拟器上,驱动开发用虚拟设备接口并行推进,这样流片回来时软件栈已经有相当成熟度,能大大缩短从样片到量产的周期。在我参与过的成功项目里,“软件等硬件”而不是“硬件等软件”,是进度可控的关键所在。
最后分享一个我这些年最深的体会——AI芯片的软硬件设计,本质上是在做一个“翻译器”:把算法的数学表达,翻译成硬件的高效运动。这个翻译的质量取决于架构师、RTL工程师、编译器工程师和算法工程师能不能真正听懂彼此的语言。技术文档里通常不会写这些,但这些恰恰决定了项目能不能从纸面性能变成实际产品力。开了这个头,下一篇文章我想深入聊聊指令集设计和编译器后端调度的那些细节,尤其适合已经拥有一个能跑通的NPU、想把利用率从60%拉到90%的团队参考。