
这个系列写到第三篇了。前两篇聊了AI芯片的整体规格怎么定、计算阵列的微架构怎么选这篇进入最麻烦也最考验功力的部分把硬件架构和软件工具链真正捏到一起去。AI芯片的软硬件设计分开看都有套路难的是两者之间的接口线——指令集怎么定、内存模型怎么统一、数据流协议怎么设计、验证怎么同步。这篇文章的读者画像是三类人做AI芯片架构的工程师做AI编译器、runtime和驱动的工程师以及给AI加速器写算子库和部署工具的人。不需要你有流片经验只要做过AI模型推理部署并且被“硬件不支持这个算子”或者“算子性能奇差”坑过就能从这里找到原因和解决方法。1. 整体设计思路软硬件协同的边界与节奏1.1 为什么AI芯片必须“软硬一起设计”做AI芯片和做传统ASIC有一个本质区别传统ASIC功能固定硬件定义基本等于产品定义而AI芯片要跑的模型是变化的硬件一旦流片基本改不动算法却可能下个月就提出新结构。AI芯片设计本质上是在“可变的算法集合”和“固定的硬件资源”之间找平衡而且这个平衡必须由软硬件两边的工程师一起找。很多团队把平衡做得不好常见两种死法。第一种是硬件自己定等算法框架换新结构后发现算子要么跑不起来要么性能惨不忍睹只能用CPU兜底NPU形同虚设。第二种是软件拼命打补丁把一堆本来该硬件处理的数据重排在CPU上做NPU算力利用率低得可怜整颗芯片的功耗还降不下来。举一个我自己踩过的坑。早期一个项目团队先拍板了架构8路MAC阵列没有设计独立的累加缓存。结果算法侧跑深度可分离卷积时depthwise卷积在MAC阵列上的利用率不到20%。这套架构本来是为普通卷积设计的大阵列按通道并行但depthwise卷积每个通道只有少量乘加大阵列根本喂不满。如果当时软硬件一起定义完全可以在硬件里加一个小型串行处理单元或者把MAC阵列改成可重构的分组模式。这种教训很贵因为改动架构意味着前端设计重做、综合重跑、验证重来。所以正确姿势不是“先定硬件再写软件”也不是“纯软件模拟一切”而是把算法、算子、指令、微架构当成一条链一起打磨。每个算子都要回答四个问题算法上是什么计算、指令上用什么表达、微架构上怎么实现、软件上怎么调度。这四个问题的答案在项目第一天就要有一个初版之后持续迭代。1.2 需求分层与三个冻结节点软硬件协同设计的第一步是把需求分好层。我习惯把整个项目分成四层算法层、算子层、指令层、微架构层。算法层只关心跑什么模型、精度要求多少算子层列出会被频繁调用的计算单元比如卷积、矩阵乘、激活、归一化指令层定义硬件提供什么能力微架构层决定每个单元怎么实现。层与层之间要有清晰的接口不能乱穿。算法层不需要知道某个卷积是用直接卷积还是矩阵乘实现指令层不需要知道编译器内部优化了几轮。实际执行中三个“冻结”时间点特别重要模型版本冻结、算子清单冻结、指令集冻结。模型版本冻结不是指永不更新而是确定当前研发周期内拿哪些模型作为性能验收基准。算子清单冻结是指把验收模型涉及的所有算子汇总成一张表表里标清楚每个算子的频率、精度要求、数据规模分布。指令集冻结是指硬件提供的基本指令不能随意增删每条指令的编码、语义、异常行为都定死。三个冻结节点之间要留出缓冲期。我见过最混乱的项目指令集一边在冻结编译器一边在为已经不存在的指令做适配最后两边都是返工。还有一个方法很推荐把FMEA式的失效模式分析用在软硬件划分上。对每个算子列出硬件实现的面积、时序、灵活性风险以及软件实现的性能、内存、功耗风险然后分成三档硬件必做、软件做硬件兜底、纯软件。比如Sigmoid这种高频非线性激活硬件做一个查表单元是划算的某个生僻的自定义激活函数硬件去做就不划算软件兜底更合理。这样划分之后的架构软硬件两边都不会被对方的“特殊性”绑架。2. 硬件核心子系统设计计算、缓存与指令集2.1 计算阵列与数据复用带宽问题的第一道坎AI芯片的性能瓶颈大多数情况下不在运算本身而在数据搬运。先算一笔账假设MAC阵列是16x16共256个MAC主频1GHzFP16精度峰值算力是256x2x1GHz约512 GOPS。如果每个操作数都从片外DDR读一次运算要读两个FP16数也就是4字节256个MAC跑满1GHz就需要约1TB/s的DDR带宽。而主流DDR接口的实际可用带宽通常在几十GB/s这个量级差了十几倍。结论很直接计算阵列的数据不能都从片外读必须做片上复用。数据复用的分层很经典可以理解成食堂打饭。第一层是“餐盘里的菜反复夹”对应寄存器级复用同一份输入数据在MAC阵列内部被多个输出通道共享第二层是“整盘菜放桌上一桌人吃”对应SRAM级复用权重块加载到片上SRAM后反复使用第三层才是“去食堂窗口打菜”对应DDR访问。设计要做的就是让前两层尽量承接大部分数据需求。以3x3卷积举例输入特征图56x56输入通道64输出通道64FP16精度。总计算量约2x56x56x64x64x9也就是2.3亿次乘加。输入数据是56x56x64个值约392KB权重是3x3x64x64个值约72KB总数据量约464KB。如果这些数据全部从DDR读按64GB/s带宽算光读数据就要7毫秒以上但把这464KB搬进SRAM按块计算搬运总量就从“每个计算元素都搬一次”变成“每个数据块搬一次”片外访存需求量下降一个数量级。这也是为什么AI芯片的SRAM容量、带宽规划和MAC阵列规模必须一起算不能各定各的。分块tiling时有一个基本不等式要记牢SRAM容量 权重块 输入块 输出块 少量中间缓存。比如一个tile定为通道块16、高28、宽28输入块是28x28x16x2字节约25KB权重块是3x3x16x16x2字节约4.6KB输出块也是25KB合计约55KB一个64KB的SRAM就能放下。这个参数选择直接决定DDR带宽需求和MAC阵列利用率是整个架构牵一发动全身的地方。除了片外带宽片上SRAM到MAC阵列的带宽同样关键。256个MAC跑满每个周期要消费1024字节的FP16数据而普通SRAM单口带宽可能只有几十字节每周期。解决方式是在MAC阵列内部做广播和复用同一行输入广播给多列同一列权重广播给多行。这就是为什么脉动阵列、权重固定阵列这类结构能普及本质上是把带宽需求从“每个周期灌满全部操作数”降成“每个周期只灌入一小撮数据其余靠内部转发”。2.2 专用指令集怎么定软硬件的翻译契约AI芯片的指令集不需要像通用CPU那样什么都能做但必须精确表达深度学习算子里的数据流模式。定义指令集时架构师、编译器后端负责人、runtime负责人必须坐在一起不能硬件工程师自己拍板。一个典型AI芯片指令集包含几类数据搬运类负责DDR与片上SRAM之间的移动计算类触发MAC阵列执行矩阵乘或卷积同步类等待某块缓冲区就绪控制类负责循环和跳转。给出一个简化但贴近实际的指令模板LOAD_W dst_sram_addr, src_ddr_addr, length # 权重从DDR搬到SRAM LOAD_IN dst_sram_addr, src_ddr_addr, length, mode # 输入数据搬运mode表示布局 MAC_OP w_addr, in_addr, acc_addr, M, N, K, dtype # 计算MxNxK矩阵块 STORE_OUT acc_addr, dst_ddr_addr, length, scale, zp # 结果写回附反量化参数 SYNC buffer_id, event_id # 等待指定位数据就绪指令编码里要预留地址模式位和精度模式位FP16、INT8、BF16在同一套指令上要能切换。很多芯片把scale和zero_point直接编码进STORE_OUT指令里就是为了让反量化在数据搬运过程中顺带完成不用CPU中途插手。写代码时指令集的“可扩展性”比想象中更重要。我们曾经没留足够的立即数字段后面想加一个带掩码的累加清零操作发现字段挤不进去指令格式差点重设计。建议指令格式在设计阶段做一次未来算子扩展推演把可能出现的算子形态列一列看现有编码能否覆盖。覆盖不了就提前加模式位或者保留保留字段这比后面打补丁便宜得多。2.3 内存子系统与同步机制让流水线不空转指令发出后AI芯片内部是流水线运转的搬运单元把数据搬进SRAM计算阵列读SRAM计算结果写回SRAM再由搬运单元写回DDR。这个流水能不能顺畅一是看内存子系统规划二是看同步机制是否精细。同步机制最常见的是栅栏barrier和事件event。栅栏用于多个执行单元在某个汇合点必须同步的场景事件用于“某个缓冲区数据就绪后另一侧才能开读”的场景。原则是尽量细化同步粒度避免全局锁。我见过一个项目所有DMA搬运完成都发同一个全局中断计算阵列经常在等一个其实根本不相关的搬运信号白白空转几百个周期。改成每块缓冲区一对事件之后性能立刻有可感知的提升。同步机制设计时要提前防范死锁。两个执行单元各自等对方的事件就会形成死锁。业界常规做法是给同步事件加超时和错误上报但芯片设计阶段更值得花时间的是做小规模形式化验证把所有同步路径都跑一遍别等芯片回来再靠抓波形定位。3. 软件工具链落地编译器、调度器与运行时3.1 编译器后端从计算图到指令序列有了硬件指令集软件工具链的第一步是编译器。整体流程分三段前端解析模型格式把计算图转成内部IR中端做图优化包括算子融合、常量折叠、精度选择后端做指令选择、内存规划、调度和指令生成。算子融合是收益最明显的优化没有之一。以卷积批归一化ReLU为例批归一化在推理阶段可以折叠成卷积权重和偏置的线性变换ReLU是逐元素激活。三个算子分开实现时每个算子的结果都要写回DDR再读出来特征图数据等于多走了两趟融合后卷积结果直接留在片上SRAM里做scale和激活最终只写一次DDR。在256KB SRAM的芯片上这一个融合大约能省一半的特征图读写带宽。图优化里几乎每个模型都能找出ConvBNReLU这种融合做编译器优化时先从这里下手性价比极高。后端内存规划是AI编译器和CPU编译器的最大区别。CPU编译器主要做寄存器分配AI编译器必须规划大块SRAM哪些地址放权重哪些放输入哪些放输出哪些是双缓冲切换区。做不好这块调度器再努力也白搭。实际做法通常是把SRAM按用途分区每个区域一个指针池编译时做线性扫描式分配尽量避免运行时动态分配。中间结果临时落DDR还是留在SRAM也要根据数据的生命周期和后继消费位置做全局决策。卷积输出如果紧接着被下一个卷积当作输入那就留在SRAM如果中间隔着Pooling且Pooling后还要读就考虑临时落DDR。3.2 调度与双缓冲让搬运和计算重叠起来调度器的目标很干净让DMA搬运和计算阵列并行工作避免“一个干活另一个等待”。最常用的技术是双缓冲ping-pong buffer。把同一块逻辑缓冲区拆成两个物理副本当前计算用A副本时DMA可以预取下一块数据到B副本当前计算结束计算阵列切到B副本DMA再往A副本写下一份。计算单元几乎感知不到数据搬运延迟。双缓冲的基本条件是Td Tc即DMA搬完下一块数据的周期数不能超过当前块的计算周期数。打个比方搬运块大小64KB总线每周期传64字节搬完需要1024个周期当前块计算耗时如果是2000个周期双缓冲就能完全覆盖。如果Td大于Tc计算还是会等搬运这时候要么加大tile块、减少搬运次数要么提高总线位宽和频率。调度序列在AI芯片上建议采用静态调度编译器编译期就算好每条指令的预计执行时间和同步点。AI执行流高度规则循环展开后指令序列几乎可预测静态调度完全够用。一个卷积算子的调度序列大致是这样的LOAD_W w0 - SRAM_B0 LOAD_IN x0 - SRAM_A0 WAIT_EVT A0_READY MAC_OP w0, x0 - acc0 STORE_OUT acc0 - DDR LOAD_W w1 - SRAM_B1 LOAD_IN x1 - SRAM_A1 WAIT_EVT A1_READY MAC_OP w1, x1 - acc1 STORE_OUT acc1 - DDR第二组数据搬运和第一组计算正好重叠这就是双缓冲的落地形态。编译器生成的指令序列里同步指令的位置很关键放早了会白白等待放晚了会读到脏数据需要在模拟器上反复校准。3.3 算子库与Kernel实现卷积算子怎么落编译器负责把计算图变成指令算子库负责把这些指令封装成高效Kernel。实际部署中最值得手写优化的就是卷积和矩阵乘因为它们在各种模型里占比最高。卷积落地的三种方式直接卷积、im2col矩阵乘、隐式GEMM。直接卷积适合3x3小卷积核代码结构直观im2col把卷积展开成矩阵乘能吃满MAC阵列效率但展开过程会产生中间数据占用SRAM隐式GEMM不显式展开计算时用索引模拟矩阵乘的寻址省内存但控制逻辑更复杂。具体芯片上怎么选要看MAC阵列尺寸、SRAM带宽、feature map布局和硬件对非连续地址访问的支持。有个细节经常被忽略数据布局。很多芯片同时支持NCHW和NHWC但为了对齐MAC阵列的加载宽度往往要引入CHWN这种中间布局。权重数据从DDR读入SRAM前做一次通道维重排把通道维提前到最内层MAC阵列的利用率能提升不少。这个重排可以在DMA搬运时顺带完成也可以在编译器里预生成重排后的权重别让runtime在推理时临时做。写Kernel时数据类型处理是另一个坑。FP16计算和INT8计算往往共用一个MAC阵列但累加路径位宽不同、对齐规则不同。切换精度需要复位累加器、重新设置scale和zero_point这些序列如果有一步错位结果精度会莫名其妙地漂移。算子库的每个Kernel里最好把“精度切换”封装成固定入口函数禁止各个Kernel各自乱写。3.4 运行时与驱动用户态、内核态的通路软件工具链最后一公里是runtime和驱动。用户态runtime负责向上提供部署API、向下提交任务队列内核态驱动负责管理硬件资源、处理中断、映射内存。两者交互通常是环形队列加doorbell机制应用把一组任务描述符写入共享内存的环形队列再写寄存器通知硬件硬件完成一批任务后通过中断通知驱动驱动回收描述符并唤醒等待应用。这里要特别说内存一致性问题。AI芯片和主CPU共享DDR时DMA搬运前必须确保CPU侧的缓存数据已经回写否则NPU读到的是旧数据。解决方式是在任务提交前对相关内存区域做缓存刷写或者直接分配一致内存区间。我们调试一个推理服务时遇到过随机推理结果错误排查半天发现是共享内存的CPU缓存没刷新。那之后runtime提交任务的每一笔内存操作都严格走统一DMA映射接口再也没出过同类问题。驱动另一个常见坑是异步生命周期管理。硬件还在读取任务描述符时用户态已经把这块内存释放了驱动就会访问悬空内存。内存池管理驱动为任务描述符维护一个固定深度的内存池复用空闲项不从用户态直接映射回收。这个深度按“最大并发任务数1”预留避免极端情况下描述符覆盖。4. 协同验证与调试在量产前把坑踩完4.1 四层验证策略从模拟器到FPGA到板级软硬件协同设计能不能收敛验证策略决定效率。我们的验证分四层每层解决不同目标。第一层是指令集模拟器也就是仿真器。软件工具链和架构团队起步阶段最依赖的平台。它在服务器上运行速度比RTL仿真快几个数量级适合跑完整模型、验证指令语义和内存分配策略。早期开发编译器时所有指令的正确性都以模拟器为准硬件RTL仿真结果要和模拟器对齐两边偏差要么是RTL的bug要么是模拟器的bug性质很容易定位。我的建议是硬件团队把模拟器当成第一验证手段别一上来就上FPGA效率完全不同。第二层是周期精确仿真建模每条指令的周期和总线竞争用来做性能评估。速度比指令集模拟器慢但能给出定量性能预测。架构选型阶段用它跑典型模型算出MAC利用率、内存吞吐、流水线停顿周期。这层模型要和第一层功能模拟器共用指令集定义避免两套定义漂移。第三层是FPGA原型验证。RTL综合到FPGA后把整个软硬件栈跑起来包括编译器生成的指令序列、runtime、驱动。FPGA时钟频率比真实芯片低很多但跑全模型验证软硬件接口正确性非常有效。建议把性能计数器一起综合进去早期就能观察哪些模块是瓶颈。FPGA调试时优先抓SDRAM读写排队状态大部分数据传输问题在总线上都会留下痕迹。第四层是板级验证真实芯片贴片回来后做端到端回归。跑全部验收模型核对精度和性能指标是否和仿真模型一致。偏差超过预期的场景回到第三层和第二层对比能快速定位是仿真模型哪里简化了还是真实芯片时序问题。4.2 性能分析MAC利用率怎么算、瓶颈怎么找性能评估核心指标是MAC利用率定义实际完成的MAC运算次数除以MAC阵列规模乘以时钟周期数。举例16x16的MAC阵列运行1000个周期理论MAC次数是16x16x1000等于25.6万次实际完成12.8万次利用率就是50%。利用率低通常是两个原因数据没准备好计算阵列在等DMA搬运tiling尺寸没吃满比如tile的M维度是15而MAC阵列M方向是16每个tile都损失一格。调试时两个原因要分开。方法是在计算阵列入口和DMA完成信号上各挂一个计数器专门统计“计算等待搬运”和“搬运等待计算”两类stall周期。有一次我们做一个3x3卷积算子模拟器预测利用率72%实际只有50%。查了半天发现是输出累加缓冲设计太小计算阵列算完一块后必须等STORE_OUT把数据搬走才能继续累加每个tile结束都有空档。后来把累加区拆成两个bank交替使用利用率回到70%以上。性能调试不能只盯搬运单元和计算阵列累加缓冲这类中间环节经常是隐藏瓶颈。性能分析时还要注意模型层的差异。有些算子形状极不规则比如动态batch、变长序列静态调度很难把tile尺寸定得圆满。这时候优先保证“连续维度的tile吃满MAC阵列”不规则维度留给软件做循环填充能挽回不少性能。4.3 常见问题速查表与排查套路把项目里遇到频率最高的问题整理成一张表方便新手直接对号入座问题现象可能原因排查步骤解决建议计算结果整体异常指令编码错误字段错位用模拟器单指令测试逐个字段核对编译器后端加指令编码自检运行一段时间后卡死同步死锁等待事件成环模拟器加超时检测记录所有等待点细化同步粒度避免全局锁模型精度漂移FP16累加溢出scale设置错误逐层和基准框架对比最大值差异卷积层后插入临时结果校验DMA搬运数据错位内存对齐问题地址单位混淆核对地址映射确认长度单位统一用字节为单位驱动层做对齐检查推理性能低于预期MAC利用率低stall来源未分类先看MAC利用率再分计数stall类型根据最大stall来源调tile或加buffer驱动随机崩溃任务描述符生命周期管理错误检查是否在硬件完成前回收内存用内存池管理描述符禁止直接释放表里有一点值得展开精度问题排查。FP16在累加过程中溢出其实很常见尤其是ResNet类有残差连接的模型shortcut相加处数值偏大。我们经验是在模拟器里逐层和基准框架对比先看每层输出的最大值差异找到第一个差异大的层再往下定位算子级。一旦发现溢出优先改累加位宽其次考虑加scale千万不要只调归一化参数掩盖问题。5. 工具选型与研发节奏5.1 软硬件协同的流程迭代与文档管理软硬件协同设计和传统瀑布式开发不一样更像一个快速迭代的环每个版本先定模型集合再定算子清单然后硬件RTL和软件工具链同时推进每个周期末做一次联合验证用模拟器和RTL仿真比对性能和正确性。研发周期越靠后联合验证的频率越高改动范围越收敛最终冻结。文档是整个协同设计的黏合剂。指令集手册必须和编码实现同步更新寄存器表必须带默认值和访问权限说明内存布局图必须是编译器和前端设计共同维护的单一信息源。有个实操建议每次review指令集改动把编译器后端负责人和前端设计负责人同时拉上面对面把问题拍板别走邮件审批流程。邮件里“已读不回”的代价经常是两边各自按自己的理解实现最后联调时才发现对不上。5.2 工具选型对比参考这里列一张工具选型对照供不同资源条件的团队参考环节方案A自研为主方案B开源改造为主注意事项编译前端自研图优化规则基于成熟图优化库改造不要重复造轮子算子融合规则可以自研中间表示自定义后端IR贴近SRAM模型通用IR加后端自定义pass后端IR必须专用通用IR做前端即可周期模拟器从功能模型加计时扩展基于开源仿真框架改造性能评估确定后再补总线模型别一步到位验证工具自研测试用例生成脚本开源RTL仿真器加FPGA工具形式化验证按需引入不要全项目都上这张表的核心思想是不要过度迷信单一工具。AI芯片软硬件协同设计最终靠的是团队对“从算法到芯片”这条链路的全局理解工具只是把理解落地的加速器。工具选型阶段最容易犯的错是一上来就追求“完整版”结果界面复杂、维护成本高反倒耽误了验证周期。写在最后这个系列写到这我最大的体会是做AI芯片软硬件设计最难的从来不是某个模块怎么搭而是三步并一步的全局思考方式。拿到一个模型脑子里要同时浮现出它的计算图、指令序列、SRAM占用、可能出现的stall点。团队里我常和新人说这份工作最值钱的技能是“翻译”能力——把算法语言、指令语言、硬件时序语言翻译成同一种语言。最后分享一个实操小技巧无论项目多紧张每周一定跑一遍完整的端到端回归哪怕只跑一个小模型。这个习惯帮我们提前抓到了好多模拟器与RTL不一致的问题。等芯片回来才发现软硬件接口对不上那就不只是返工是整个项目的灾难。