1. 从一张架构图说起为什么NPU的数据搬运比计算本身更值得琢磨第一次接触NPU架构的时候我和大多数人一样注意力全在MAC阵列的算力峰值上——多少TOPS、支持什么精度、峰值功耗多少。直到真正上手调优一个卷积算子才发现瓶颈根本不在乘加单元上而是卡在数据供给这一环。算力单元大部分时间在等数据就像一条八车道的高速公路入口却只有一个收费站。这个现象在业内其实不算秘密。NPU的data flow也就是数据从外部存储一路搬到计算单元旁边的完整路径决定了这颗芯片实际能跑出多少有效算力。标题里提到的三个关键词——HBM、DMA、多bank Shared Memory——恰好构成了这条路径上最核心的三段片外的大容量存储、负责搬运的引擎、以及计算单元伸手就能够到的片上暂存。我打算把这条链路拆开来讲清楚。从HBM的物理特性怎么影响访问模式到DMA描述符怎么设计才能跑满带宽再到Shared Memory为什么要分bank、bank冲突到底怎么产生的、怎么在写kernel的时候绕开它。整篇内容适合正在做NPU算子开发、性能调优或者单纯想搞明白AI芯片数据通路设计的读者。不需要你手上有板子但最好对计算机体系结构有一点基本概念比如cache、总线、DDR这些词不陌生。下面这张表是我自己总结的这条数据通路上各环节的核心关注点先给个全局印象层级典型容量关键指标主要瓶颈HBM8-64GB带宽、延迟行激活开销、刷新DMA描述符队列吞吐、并发度描述符粒度、对齐Shared Memory几十KB-几MB带宽、bank数bank冲突、容量2. 整体数据通路设计为什么是HBM加DMA加多bank SRAM这套组合2.1 从冯诺依曼瓶颈到NPU的存储层次传统CPU靠多级cache来缓解内存墙但NPU的工作负载有个显著特点数据复用模式高度可预测。卷积的权重、feature map的复用规律在编译期就能分析出来所以NPU不需要像CPU那样靠硬件自动预测而是可以用软件显式管理片上存储。这就催生了scratchpad memory便签式存储的设计思路也就是我们说的Shared Memory。这套层次的设计逻辑其实很朴素HBM负责大容量因为模型动辄几百MB到几个GB片上根本放不下DMA负责高效搬运因为计算核心不应该浪费周期去处理地址计算和总线握手Shared Memory负责低延迟高带宽供给因为MAC阵列每个周期都要吃进大量操作数。我见过一些新手会问为什么不直接用HBM喂给计算单元答案很简单HBM的延迟在几百纳秒量级而MAC阵列的周期是纳秒级差了两三个数量级。如果每次乘加都去HBM取数算力利用率会低到没法看。所以必须有一层靠近计算单元的SRAM做缓冲把延迟藏起来。2.2 三段式流水线的设计取舍把这条通路画成流水线大概是这个样子HBM到Shared Memory是一段Shared Memory到寄存器是一段寄存器到MAC是一段。每一段都有自己的带宽和延迟特性设计时要保证三段流水线的吞吐匹配否则最慢的那段就是瓶颈。这里有个常见的误区很多人只盯着HBM带宽觉得带宽越大越好。但实际上如果DMA引擎的并发度不够或者Shared Memory的bank设计不合理HBM带宽再大也灌不进去。我实测过一个caseHBM理论带宽900GB/s但实际算子只跑出不到200GB/s的有效带宽问题出在DMA描述符切得太碎每次搬运的数据量太小总线握手开销占比过高。提示评估数据通路时不要只看单点峰值要看端到端有效带宽。从HBM读出的数据量除以算子总耗时才是真实可用的带宽。2.3 为什么Shared Memory要分bankShared Memory分bank的根本原因和DDR分bank类似单端口存储每个周期只能服务一次访问如果多个计算单元同时要取数就会排队。分bank之后只要访问落在不同bank上就能并行服务。但NPU场景和GPU有个区别NPU的访问模式往往更规整编译器可以提前规划。所以NPU的Shared Memory bank设计有时候会更激进比如bank位宽做得更宽或者支持多播。这些设计选择背后都是对典型算子访问模式的统计结果。3. HBM侧的关键细节访问粒度、行缓冲与刷新开销3.1 HBM的物理结构决定了访问不能太碎HBMHigh Bandwidth Memory通过硅中介层和芯片连在一起位宽极大通常1024bit以上靠宽总线堆出高带宽。但它的内部结构和DDR一样是bank、row、column的层次。一次访问要先激活一行row activate把整行读到行缓冲再从行缓冲里选列。这个特性意味着如果连续访问落在同一行效率极高如果频繁跨行每次都要重新激活开销很大。HBM的行大小通常在1KB到2KB量级所以DMA搬运的数据块最好大于这个尺寸并且地址连续。我做过一个对比实验同样搬运1MB数据按4KB块连续搬和按256B块随机搬有效带宽差了将近三倍。前者能充分利用行缓冲后者几乎每次都在激活新行。3.2 刷新开销那个容易被忽略的固定成本HBM是DRAM需要定期刷新刷新期间不能响应访问。这个开销在带宽利用率高的时候占比不大但在低负载或者访问模式不好的时候会变得明显。更麻烦的是刷新时机不受控制可能正好卡在关键路径上。实际调优时我一般会建议把大块连续搬运安排在流水线的稳定阶段避免在算子启动和收尾的敏感时期做大量HBM访问。编译器调度时也会考虑这一点把DMA的发起时机和计算重叠起来。3.3 访问对齐不是玄学是硬性要求HBM访问有对齐要求通常要求地址对齐到总线位宽。比如总线位宽是256bit那地址最好是32字节对齐。不对齐的访问会触发读改写带宽直接打对折甚至更惨。这个坑我在早期项目里踩过。当时DMA描述符里的源地址没做对齐结果性能只有预期的一半。后来查手册才发现非对齐访问会被拆成两次。解决办法很简单在数据布局阶段就把tensor的起始地址对齐到256字节同时保证每个维度的stride也是对齐的。对齐情况有效带宽说明256B对齐100%理想情况128B对齐~85%轻微拆分64B对齐~60%明显拆分非对齐50%读改写4. DMA引擎描述符设计、并发度与流水线重叠4.1 DMA描述符的粒度怎么定DMA搬运不是一次搬一个字节而是按描述符来。一个描述符通常包含源地址、目的地址、搬运长度、stride等信息。描述符的粒度直接决定了DMA引擎的效率和灵活性。粒度太粗灵活性差遇到非连续数据就要多次搬运粒度太细描述符数量爆炸引擎处理描述符本身的开销就上来了。我的经验是描述符的搬运长度最好在1KB到16KB之间这个区间既能保证行缓冲命中率又不会让描述符队列太深。对于卷积这种有大量非连续访问的场景可以用多维描述符。比如一个描述符描述一个二维块包含行数、行内长度、行间stride。这样硬件可以自动展开不需要软件生成大量一维描述符。4.2 并发度多通道DMA怎么配合高端NPU通常有多个DMA通道可以并行搬运不同的数据块。但并发度不是越高越好因为HBM的带宽是共享的多个通道同时抢带宽反而可能互相干扰。我一般会这样配置权重搬运用一个通道feature map用另一个通道输出写回用第三个通道。三个通道的优先级根据流水线阶段动态调整。比如计算阶段权重已经就位就可以降低权重通道的优先级把带宽让给feature map。这里有个细节DMA通道之间的仲裁策略很关键。如果是轮询仲裁可能出现某个通道饿死的情况如果是优先级仲裁低优先级通道可能一直拿不到带宽。实际调优时要看具体硬件的仲裁机制必要时通过软件调度来规避。4.3 流水线重叠让DMA和计算同时跑数据通路设计的精髓在于重叠。理想情况下DMA搬运第N1块数据的同时计算单元在处理第N块数据。这样HBM带宽和计算算力都能被充分利用。实现重叠的关键是双缓冲或者多缓冲。Shared Memory里开两块区域一块给当前计算用一块给DMA写入。计算完成后交换角色。这个机制听起来简单但实际实现时要注意同步DMA写完要通知计算单元计算单元用完要通知DMA可以覆盖。注意双缓冲的同步如果靠轮询会浪费周期如果靠中断会有延迟。很多NPU用硬件信号量或者barrier指令来做效率最高。写kernel时要确保同步指令插在正确的位置否则要么死锁要么数据竞争。5. 多bank Shared Memorybank冲突的成因与规避5.1 bank冲突到底是怎么产生的Shared Memory分bank之后每个bank独立服务访问。如果同一个周期内多个访问请求落在同一个bank的不同地址上就会冲突只能串行处理。这就是bank冲突。举个具体例子假设有32个bank每个bank位宽4字节总位宽128字节。如果32个线程分别访问地址0、4、8、...、124正好每个bank一个无冲突。但如果32个线程都访问地址0、128、256、...这些地址的bank索引相同因为bank索引通常是地址除以bank位宽再模bank数就会全部落到同一个bank冲突32次。5.2 常见的冲突模式与规避方法最常见的冲突模式是stride访问。比如矩阵转置时按列访问就会产生stride冲突。解决办法有两种一是padding在每行末尾加几个填充元素改变stride让bank索引错开二是swizzle把地址做异或或者位重排打散bank分布。padding的方法简单直接但浪费存储空间。swizzle更优雅但需要编译器和硬件配合。我在实际项目里更常用padding因为可控性强调试也方便。冲突模式典型场景规避方法代价stride冲突矩阵转置padding存储浪费广播冲突权重共享多播支持硬件复杂度随机冲突稀疏访问swizzle地址计算开销5.3 多播当多个计算单元要同一份数据卷积里有个典型场景多个输出通道共享同一份输入feature。如果每个计算单元都去Shared Memory读一遍bank压力很大。多播机制允许一次读取把数据同时送给多个消费者大幅降低bank访问次数。多播的实现通常是在Shared Memory输出端加一个广播网络。硬件检测到多个请求地址相同时只读一次然后复制多份。这个机制对卷积非常友好能显著提升有效带宽。但多播也有代价如果请求地址不完全相同硬件还是要分别处理。所以编译器要尽量把共享访问组织成地址完全一致的形式才能吃到多播的红利。6. 实操过程手写一个数据搬运与bank优化的小例子6.1 场景设定与参数计算假设我们要实现一个简单的矩阵乘法C A × BA是64×256B是256×64C是64×64。数据放在HBM里我们要通过DMA搬到Shared Memory再喂给计算单元。先算一下数据量A是64×256×4字节64KBB是256×64×4字节64KBC是64×64×4字节16KB。Shared Memory假设有256KB足够放下A和B的一块tile。分块策略把A按行分块每次搬16行即16×256×416KB。B按列分块每次搬16列即256×16×416KB。这样每次计算16×16的输出块。6.2 DMA描述符配置对于A的搬运源地址是A的起始地址加上行偏移目的地址是Shared Memory的A缓冲区。由于A是行优先存储16行连续数据在HBM里是连续的可以用一个一维描述符搞定长度16KB。对于B的搬运B是行优先存储但我们要搬的是16列这些列在HBM里不连续每列之间间隔64×4256字节。所以需要用二维描述符行数256每行搬16个元素64字节行间stride 256字节。// 伪代码配置B的二维DMA描述符 dma_desc.src_addr B_base col_offset * 4; dma_desc.dst_addr smem_B_buffer; dma_desc.row_count 256; dma_desc.row_size 16 * 4; // 64字节 dma_desc.src_stride 64 * 4; // 256字节 dma_desc.dst_stride 16 * 4; // 目的端连续存放6.3 Shared Memory的bank规划假设Shared Memory有32个bank每个bank位宽4字节。A缓冲区按行存放每行256个元素正好是256×41024字节是32的倍数。这意味着A的每一行起始地址的bank索引都是0如果计算单元按列访问A就会产生严重的bank冲突。解决办法给A的每一行加padding。原本每行256个元素改成260个元素多出的4个元素做填充。这样每行起始地址偏移260×41040字节1040除以4再模32等于4bank索引每行错开4个。按列访问时相邻行的同一列落在不同bank上冲突大幅减少。B缓冲区类似处理。B的每一列在Shared Memory里是连续的按行访问B时也可能冲突。给B的每行也加padding从16个元素加到20个。6.4 同步与流水线双缓冲的实现Shared Memory里开两组A和B缓冲区一组给计算用一组给DMA写。用一个硬件信号量标记哪组可用。流程大概是DMA写buffer0写完置信号量计算单元等信号量读到后开始算buffer0同时DMA写buffer1计算完buffer0后置信号量DMA可以覆盖buffer0计算单元转去算buffer1。如此循环。提示双缓冲的深度可以根据HBM延迟和计算时间调整。如果HBM延迟远大于计算时间可能需要三缓冲甚至四缓冲来填满流水线。7. 常见问题与排查技巧实录7.1 有效带宽远低于理论值这是最常见的问题。排查思路按顺序来先看DMA描述符的搬运长度是否太小如果小于1KB优先加大再看地址是否对齐非对齐访问会直接砍半然后看访问模式是否连续跨行频繁的话考虑调整数据布局最后看DMA通道之间是否互相干扰尝试调整优先级或错开搬运时机。我遇到过一个case理论带宽800GB/s实测只有150GB/s。查了一圈发现是描述符长度只有256字节而且地址没对齐。改成4KB对齐搬运后直接拉到600GB/s以上。7.2 bank冲突导致计算单元等数据如果计算单元的stall周期很高而DMA侧看起来正常大概率是bank冲突。用性能计数器看Shared Memory的冲突次数如果冲突率高就要检查访问模式。常见的冲突来源是stride访问和广播缺失。先确认数据布局是否加了padding再看编译器有没有生成多播指令。有时候手动调整一下循环顺序把最内层循环的访问变成连续访问冲突就消失了。7.3 DMA和计算之间的同步死锁双缓冲的同步如果写错很容易死锁。典型症状是DMA和计算互相等对方的信号量。排查时先确认信号量的初始状态再检查wait和post的配对是否正确。我一般会在仿真环境里先跑一个小case把信号量的状态打印出来确认流水线能正常流转。上板之后再跑大case避免在硬件上调试死锁效率太低。问题现象可能原因排查手段解决方向带宽低描述符碎、非对齐看描述符长度和地址加大粒度、对齐计算stallbank冲突看冲突计数器padding、swizzle死锁同步错误仿真打印信号量检查wait/post配对数据错竞争看时序波形加barrier7.4 一些不太常见但很坑的问题HBM刷新导致的偶发性能抖动这个很难复现但确实存在。如果发现性能曲线有周期性毛刺可以查一下刷新周期是否和算子执行周期有共振。还有DMA描述符队列溢出。如果软件生成描述符的速度超过硬件消费速度队列会满DMA引擎会stall。这时候要么降低描述符生成速率要么加大队列深度。最后是Shared Memory的容量规划。如果tile切得太大Shared Memory放不下就要回退到更小的tile但这样DMA搬运次数增加带宽压力更大。这个平衡点需要根据具体硬件的Shared Memory容量和HBM带宽来算。8. 写在最后几个我踩过的坑和常用技巧关于HBM访问我的经验是尽量让DMA搬运大块连续数据单次搬运不要小于4KB。如果数据布局不允许宁可先在HBM里做一次重排也不要让DMA去搬碎片。DMA描述符的设计上能用多维就用多维让硬件去展开循环比软件生成一堆一维描述符效率高得多。描述符的队列深度要留足余量至少能覆盖两到三个流水线阶段。Shared Memory的bank规划padding是最简单有效的办法。padding的大小要算清楚让相邻行的bank索引错开错开的量最好是bank数的互质数这样冲突最少。双缓冲的同步能用硬件信号量就别用软件轮询。硬件信号量的延迟是周期级的软件轮询可能浪费几十个周期。如果硬件不支持那就把轮询循环写得紧凑一点减少开销。最后性能调优一定要有数据支撑。不要凭感觉猜瓶颈在哪里用性能计数器把HBM带宽、DMA吞吐、bank冲突率、计算单元利用率都打出来哪个低就查哪个。我见过太多人凭直觉优化了半天结果瓶颈根本不在他以为的地方。