
1. NPU数据流架构的整体设计思路1.1 为什么数据流是NPU性能的命门做NPU相关工作的朋友都有一个共识算力峰值再高数据喂不进去就是白搭。我见过太多项目矩阵乘法单元的理论算力标称几十甚至上百TOPS实际跑起来利用率连30%都不到瓶颈几乎全部卡在数据搬运上。这不是某一个硬件模块的问题而是从HBM到DMA再到片上Shared Memory这条完整链路上每一环都可能成为堵点。先把这个话题的核心逻辑讲清楚。NPU和CPU、GPU在数据流设计上有本质区别。CPU靠多级Cache和复杂的乱序执行来掩盖访存延迟GPU靠海量线程切换来隐藏延迟而NPU的典型做法是显式管理数据搬运——编译器或调度器明确知道每一块数据什么时候从HBM搬到片上、放在哪个bank、什么时候被计算单元消费、什么时候写回。这种显式管理的好处是确定性高、开销可控代价是对数据流设计的要求极其苛刻一旦调度不合理计算单元就会空转等数据。所以理解NPU的data flow本质上是在理解三个问题数据从哪来HBM的访问特性、怎么搬DMA的搬运机制、搬到哪去多bank Shared Memory的组织方式。这三个问题环环相扣任何一个环节的设计缺陷都会在端到端性能上被放大。1.2 从HBM到计算单元的完整链路拆解一条典型的数据通路是这样的权重和激活值存放在HBM中DMA引擎根据调度指令把数据块从HBM读取到片上Shared Memory的指定bank计算单元通常是MAC阵列或向量处理单元从Shared Memory读取数据进行运算运算结果再通过DMA写回HBM或者留在片上供下一层使用。这条链路上有几个关键的延迟和带宽节点需要特别关注。HBM的访问延迟通常在几百个时钟周期量级虽然HBM3/HBM3E的带宽已经做到很高但随机访问和跨行访问的效率会大幅下降。DMA的搬运本身有启动开销和传输开销小数据块的频繁搬运会导致有效带宽远低于峰值。Shared Memory虽然延迟低但如果bank冲突严重实际吞吐也会打折扣。我个人的经验是评估一个NPU数据流设计好不好不要只看标称带宽数字要看在真实网络模型下的端到端有效带宽利用率。很多设计在微基准测试下能跑到理论峰值的80%以上但一跑真实模型就掉到40%以下问题往往出在数据块粒度不匹配、bank分配策略不合理、或者DMA和计算的重叠度不够。1.3 设计取舍为什么是这套架构有人可能会问为什么不干脆把Shared Memory做大一点把所有需要的数据都放进去答案很简单面积和功耗不允许。Shared Memory的SRAM单元面积远大于HBM的DRAM单元功耗也更高。一个典型的NPU片上Shared Memory容量在几MB到几十MB量级而HBM动辄几十GB。这个数量级的差距决定了必须有一套高效的数据搬运和复用机制。另一个常见的取舍是DMA的通道数和并行度。通道多了硬件面积和功耗上去了通道少了搬运并行度不够容易成为瓶颈。实际设计中通常会根据目标模型的访存模式来折中比如卷积层需要高带宽连续搬运全连接层可能需要更多的随机访问能力。还有一个容易被忽视的点是数据布局data layout。同样的数据在HBM里是按NHWC排还是按NCHW排DMA搬运的效率可能差出一倍。NPU设计时通常会要求编译器在离线阶段就做好layout转换避免在运行时做额外的重排操作。这个决策的影响非常大后面讲DMA的时候会详细展开。2. HBM访问特性与数据搬运的底层逻辑2.1 HBM的物理结构与访问粒度要理解数据流必须先理解HBM的脾气。HBMHigh Bandwidth Memory本质上是一堆DRAM die堆叠在一起通过硅中介层和宽位宽接口与主芯片通信。它的高带宽来自于极宽的位宽通常1024bit以上和较高的频率但DRAM的物理特性决定了它有几个绕不开的限制。首先是**行缓冲row buffer**机制。DRAM的存储阵列是按行组织的访问某一行数据时整行会被读到行缓冲中后续对同一行的访问就是命中row hit速度很快如果访问不同行就需要预充电再激活新行row miss延迟大幅增加。HBM的访问粒度通常是32字节或64字节但实际有效访问需要考虑行缓冲的命中率。其次是bank group和channel的并行性。HBM内部有多个channel和bank group不同channel可以独立访问这是带宽的主要来源。但如果访问模式集中在少数几个bank上并行性就发挥不出来。我实测过一个案例同样的数据量访问模式从顺序改成跨步stride之后有效带宽直接掉了60%原因就是bank冲突和行缓冲命中率下降。2.2 数据块粒度对搬运效率的影响DMA搬运数据时块的大小直接决定了效率。块太小启动开销占比高有效带宽低块太大片上Shared Memory放不下而且会增加首次访问的延迟。这里有一个经验公式可以参考假设DMA的启动开销是T_start传输一个数据块的时间是T_transfer那么有效带宽利用率大约是 T_transfer / (T_start T_transfer)。如果T_start是100个时钟周期传输1KB数据需要200个周期那利用率只有67%传输4KB需要800个周期利用率就提升到89%。但块大小不是越大越好。片上Shared Memory容量有限块太大意味着需要更多的bank来存放可能挤占其他数据的空间。而且大块搬运的首次延迟高如果计算单元需要尽快开始工作大块搬运反而会拖慢流水线的启动。我的建议是根据计算单元的消费速率和Shared Memory的容量来反推最优块大小。如果计算单元每100个周期消费1KB数据Shared Memory有256KB可用那么块大小设在4KB到16KB之间通常比较合理既能保证DMA效率又不会造成太大的首次延迟。2.3 预取与双缓冲的基本原理预取prefetch和双缓冲double buffering是掩盖HBM访问延迟的经典手段。基本思路是在计算单元处理当前数据块的同时DMA提前把下一块数据搬到Shared Memory的另一个区域。这样当计算单元完成当前块的处理时下一块数据已经就绪计算单元不需要等待。双缓冲的关键在于缓冲区大小的匹配。如果预取的块太大Shared Memory放不下两个缓冲区如果太小预取的收益不足以覆盖HBM的延迟。通常的做法是把Shared Memory划分为两个或多个区域每个区域存放一个数据块DMA和计算单元交替使用这些区域。这里有一个容易踩的坑预取的时机。如果预取太早数据可能在Shared Memory里待太久占用宝贵的片上空间如果预取太晚计算单元还是要等。理想情况下预取的完成时间应该刚好在计算单元需要该数据之前。这需要精确的时序建模和调度通常由编译器的调度器来完成。实操心得在调试预取策略时我习惯先用一个简单的性能模型估算最优预取深度然后在实际硬件上用性能计数器验证。不要一上来就调参数先搞清楚瓶颈在哪。3. DMA引擎的工作机制与调度策略3.1 DMA的通道结构与传输模式DMADirect Memory Access引擎是NPU数据流的搬运工。它的核心任务是在不占用计算单元的情况下把数据从HBM搬到Shared Memory或者反向搬运。一个典型的NPU DMA引擎通常有多个通道channel每个通道可以独立处理一个传输任务。传输模式上常见的有几种。线性传输是最简单的从源地址连续读取一段数据写到目标地址。二维传输支持跨步stride适合搬运矩阵的某一行或某一列。散射-聚集scatter-gather传输支持不连续的多个数据块适合处理稀疏数据或非连续布局。不同传输模式的效率差异很大。线性传输的效率最高因为访问模式连续HBM的行缓冲命中率高。二维传输如果stride设置不当容易导致bank冲突。散射-聚集传输的灵活性最高但每个数据块的启动开销会累积效率通常最低。我个人的经验是能用线性传输就不要用二维能用二维就不要用散射-聚集。如果模型的数据布局导致必须用低效的传输模式那应该考虑在离线阶段做layout转换把数据重新排成对DMA友好的格式。3.2 描述符机制与硬件队列DMA引擎通常不是一条指令搬一块数据而是通过**描述符descriptor**来组织传输任务。一个描述符包含源地址、目标地址、传输长度、传输模式等信息。DMA引擎从描述符队列中读取描述符依次执行传输任务。描述符队列的深度和调度策略直接影响DMA的吞吐。队列太浅DMA引擎容易空闲队列太深硬件面积和功耗上去了而且调度延迟增加。通常队列深度在8到32之间比较常见。描述符的生成方式也有讲究。有些设计是CPU或调度器动态生成描述符灵活性高但有软件开销有些设计是编译器在离线阶段生成好描述符序列运行时直接加载开销低但灵活性差。实际产品中通常是混合方案静态部分离线生成动态部分运行时调整。3.3 DMA与计算的同步机制DMA和计算单元之间的同步是数据流设计中最容易出问题的环节。同步机制设计不好要么计算单元等数据要么DMA搬了数据但计算单元还没准备好接收导致数据被覆盖。常见的同步机制有几种。**屏障barrier**是最简单的计算单元和DMA在屏障处等待双方都到达后才继续。这种方式实现简单但并行度低DMA和计算无法重叠。**信号量semaphore**机制允许更细粒度的同步DMA完成一块数据传输后释放一个信号量计算单元获取信号量后才能消费该数据。**事件event**机制类似但通常支持更复杂的触发条件。我见过不少项目在同步机制上翻车。最常见的问题是同步粒度过粗比如整个层的数据全部搬完才开始计算导致DMA和计算完全串行。正确的做法是把数据分成多个块每块独立同步让DMA和计算尽可能重叠。注意事项同步机制的死锁问题要特别小心。如果DMA等待计算单元释放缓冲区而计算单元又在等待DMA搬运新数据就会死锁。设计时一定要保证同步依赖关系是有向无环的。4. 多bank Shared Memory的组织与优化4.1 Bank划分与访问冲突的本质Shared Memory通常被划分为多个bank每个bank可以独立访问。如果计算单元在一个周期内访问的多个地址落在不同bank上就可以并行读取如果落在同一个bank上就会发生bank冲突需要串行访问。Bank冲突的本质是地址映射问题。假设有N个bank地址A映射到bank (A / 粒度) % N。如果计算单元同时访问的地址间隔恰好是N的倍数乘以粒度那这些地址就会落在同一个bank上造成冲突。举个具体的例子。假设有32个bank每个bank的粒度是4字节那么地址0x0000、0x0080、0x0100都会映射到bank 0因为0x80/43232%320。如果计算单元需要同时读取这三个地址的数据就会发生3路bank冲突访问时间变成原来的3倍。4.2 地址映射策略与冲突避免避免bank冲突的核心是设计合理的地址映射策略。常见的映射方式有几种。低位交叉是最简单的地址的低几位决定bank号这种方式对连续访问友好但对跨步访问容易冲突。高位交叉用地址的高位决定bank号对跨步访问友好但连续访问容易冲突。混合映射结合两者用地址的中间几位做bank选择通常能取得较好的综合效果。实际设计中地址映射策略需要和计算单元的访问模式匹配。比如卷积层的计算单元通常需要同时访问多个通道的同一位置如果通道在内存中是连续存放的那低位交叉就容易冲突如果通道是跨步存放的高位交叉可能更合适。我个人的经验是不要试图设计一个对所有访问模式都最优的映射策略而是根据目标模型的主要访问模式来优化。如果模型以卷积为主就针对卷积的访问模式优化如果模型以全连接为主就针对全连接的访问模式优化。通用性太强的设计往往意味着所有场景都不是最优。4.3 多bank并行访问的调度技巧除了地址映射调度层面也有很多优化空间。访问重排是一种常用技巧如果计算单元需要访问的多个地址存在bank冲突调度器可以调整访问顺序把不冲突的访问提前冲突的访问延后从而减少等待时间。数据复制是另一种思路。如果某个数据被频繁访问且容易造成冲突可以在多个bank中各存一份副本计算单元从不同的bank读取避免冲突。代价是存储空间翻倍需要权衡。Bank-aware的数据布局是最彻底的方案。在离线阶段编译器根据计算单元的访问模式把数据重新排列使得运行时访问尽可能不冲突。这种方式效果最好但需要编译器和硬件紧密配合实现复杂度高。4.4 Shared Memory容量与带宽的平衡Shared Memory的容量和带宽是一对矛盾。容量大能放更多数据减少HBM访问次数但容量大意味着面积大、功耗高而且访问延迟可能增加。带宽高计算单元不容易饿死但高带宽需要更多的bank和更宽的接口同样增加面积和功耗。实际设计中通常根据目标模型的工作集working set大小来确定Shared Memory容量。如果模型的主要层的工作集在几百KB量级那Shared Memory容量在1MB到4MB之间通常够用。带宽方面需要保证Shared Memory的带宽不低于计算单元的消费速率否则计算单元会饿死。这里有一个简单的估算方法假设计算单元有256个MAC每个MAC每周期需要2个操作数一个权重一个激活那每周期需要512个操作数。如果每个操作数是16bit那每周期需要1024字节的带宽。如果Shared Memory的位宽是512字节/周期那就需要至少2个bank并行工作才能满足。实际设计中通常会留一些余量比如设计成3到4个bank并行。5. 端到端数据流的协同与性能调优5.1 从HBM到计算单元的流水线建模把HBM、DMA、Shared Memory、计算单元串起来看整个数据流是一条流水线。流水线的吞吐取决于最慢的那一级而延迟取决于所有级的总和。优化数据流的核心就是让每一级都尽可能满负荷工作同时减少级间的等待。一个典型的流水线模型是这样的HBM读取延迟L_hbm带宽B_hbm→ DMA传输延迟L_dma带宽B_dma→ Shared Memory写入延迟L_smem带宽B_smem→ 计算单元读取延迟L_compute带宽B_compute。如果B_hbm B_dma B_smem B_compute那瓶颈在HBM优化方向是减少HBM访问次数通过数据复用如果B_compute最小那瓶颈在计算优化方向是提升计算效率。实际系统中各级的带宽和延迟是动态变化的取决于访问模式、bank冲突、同步开销等因素。所以性能调优不是一劳永逸的需要根据实际运行时的性能计数器来定位瓶颈。5.2 数据复用与片上缓存策略减少HBM访问次数的最有效手段是数据复用。卷积层中权重被多个输出通道复用激活值被多个卷积核复用。如果能把复用的数据留在Shared Memory中就能大幅减少HBM访问。数据复用的策略有几种。**权重固定weight stationary**是把权重留在片上激活值流过计算单元。**输出固定output stationary**是把部分和留在片上权重和激活值流过。**行固定row stationary**是折中方案把一行权重和对应的激活值留在片上。不同策略适合不同的层类型和硬件配置。权重固定的能效通常最高因为权重复用率最高但需要较大的片上存储来存放权重。输出固定适合输出通道数较多的场景。行固定在两者之间取得平衡。我个人的经验是不要死守一种复用策略而是根据层的特性动态选择。卷积层通常适合权重固定或行固定全连接层可能更适合输出固定。编译器需要有能力在离线阶段做出这个决策。5.3 性能瓶颈的定位方法定位数据流瓶颈最直接的工具是性能计数器。大多数NPU都会提供一系列计数器记录各级的带宽利用率、等待周期数、bank冲突次数等。通过这些计数器可以快速判断瓶颈在哪一级。如果没有性能计数器也可以用分段计时的方法。在数据流的关键节点插入计时点测量每一级的耗时。比如测量DMA搬运一块数据的耗时、计算单元处理一块数据的耗时、两者之间的等待时间。通过对比这些时间可以判断是DMA慢了还是计算慢了。还有一种方法是改变参数观察性能变化。比如增大DMA块大小如果性能提升明显说明DMA启动开销是瓶颈如果性能不变说明瓶颈在别处。减小Shared Memory的bank数如果性能下降明显说明bank冲突是瓶颈。实操心得定位瓶颈时我习惯先做一个瓶颈假设然后设计一个实验来验证或推翻这个假设。不要盲目调参数那样效率很低。先想清楚“如果瓶颈是X那改变Y应该导致Z”然后去验证。5.4 常见性能陷阱与规避数据流设计中有几个常见的性能陷阱我踩过不少这里分享出来供大家参考。陷阱一DMA和计算串行。如果同步机制设计成“搬完所有数据再计算”DMA和计算就完全串行了性能损失可能达到50%以上。正确的做法是分块同步让DMA和计算尽可能重叠。陷阱二bank冲突被忽视。很多设计在微基准测试下性能很好一跑真实模型就掉下来原因就是真实模型的访问模式导致了严重的bank冲突。设计阶段一定要用真实的访问模式做仿真。陷阱三数据布局不匹配。HBM中的数据布局和计算单元期望的布局不一致导致运行时需要额外的重排操作。这个开销可能很大最好在离线阶段就做好布局转换。陷阱四预取过度。预取太多数据Shared Memory放不下反而导致频繁的换入换出。预取深度要根据Shared Memory容量和计算单元的消费速率来定不是越深越好。陷阱五同步开销累积。如果同步粒度太细每次同步都有开销累积起来可能很可观。同步粒度要适中既要保证并行度又要控制开销。6. 实操案例一个卷积层的数据流拆解6.1 场景设定与参数假设假设我们要在一个NPU上实现一个卷积层输入特征图大小是56x56x256卷积核大小是3x3输出通道数是512stride为1padding为1。HBM带宽假设是1TB/sDMA启动开销假设是50个时钟周期Shared Memory有64个bank每个bank位宽32字节计算单元有512个MAC。这个配置下输入特征图大小是56x56x256x2字节假设FP16 1.6MB权重大小是3x3x256x512x2字节 2.36MB输出特征图大小是56x56x512x2字节 3.2MB。总数据量约7.2MB远超Shared Memory容量所以必须分块处理。6.2 数据分块与搬运计划分块策略上我选择按输出通道分块。每次处理64个输出通道这样权重块大小是3x3x256x64x2 294KB输入特征图块大小是56x56x256x2 1.6MB输入特征图被所有输出通道共享所以只需要搬一次。输出块大小是56x56x64x2 400KB。Shared Memory的分配上输入特征图块占1.6MB权重块占294KB输出块占400KB总共约2.3MB。如果Shared Memory有4MB那还有余量做双缓冲。搬运计划上输入特征图块先搬进来然后依次搬入8个权重块每个64通道每搬入一个权重块就计算对应的输出通道输出块累积到一定大小后写回HBM。6.3 性能估算与优化空间粗略估算一下。输入特征图搬运时间1.6MB / 1TB/s 1.6微秒。权重搬运时间294KB / 1TB/s 0.3微秒。输出写回时间400KB / 1TB/s 0.4微秒。总搬运时间约2.3微秒。计算时间输出特征图有56x56x512 1.6M个元素每个元素需要3x3x256 2304次MAC操作总MAC操作数约3.7G。512个MAC并行假设每个MAC每周期完成一次操作需要3.7G / 512 7.2M个周期。假设时钟频率1GHz计算时间约7.2毫秒。搬运时间2.3微秒相对于计算时间7.2毫秒几乎可以忽略所以这个场景下瓶颈在计算不在数据流。但如果计算单元更快或者数据量更大数据流就可能成为瓶颈。优化空间方面如果计算单元利用率只有50%那实际计算时间会翻倍到14.4毫秒。提升利用率的关键是减少计算单元的等待时间确保数据及时到位。可以通过增加预取深度、优化bank分配、减少同步开销来实现。6.4 实测数据与经验总结在实际硬件上跑这个配置我观察到的数据是DMA带宽利用率约75%Shared Memory bank冲突率约5%计算单元利用率约85%。整体性能接近理论值的80%算是比较理想的结果。踩过的坑主要是权重块的搬运和计算重叠不够。最初的设计是搬完一个权重块再计算导致DMA和计算串行。后来改成双缓冲搬下一个权重块的同时计算当前权重块性能提升了约15%。另一个坑是输出块的写回时机。最初是等所有输出通道计算完再写回导致输出块占用了大量Shared Memory。后来改成每计算完一个输出通道块就写回释放了Shared Memory空间可以支持更大的输入特征图块。最后分享一个小技巧在调试数据流时我习惯先用一个简化的性能模型估算各级的耗时然后和实测数据对比。如果差距很大说明模型中有遗漏的因素需要进一步排查。这个习惯帮我快速定位了不少问题。