资讯动态

NPU数据流设计:HBM、DMA与多bank Shared Memory协同优化

发布时间:2026/10/19 1:51:41 来源:尧图企业网站定制
1. 从一张架构图说起为什么数据流才是NPU的命门很多人第一次接触NPU注意力都会被MAC阵列的规模吸引——多少乘加单元、峰值算力多少TOPS这些数字确实好看。但真正上手写算子、调性能之后你会发现算力从来不是瓶颈数据喂不饱才是。一个标称几百TOPS的加速器如果数据流设计得不好实际跑起来可能连十分之一的利用率都摸不到。这一章要聊的就是这件事数据怎么从HBM里出来经过DMA搬运最终落到多bank的Shared Memory上供计算单元消费。这条链路听起来简单但里面藏着大量工程取舍——bank怎么划分、DMA怎么调度、片上缓存怎么复用、搬运和计算怎么重叠。每一个决策都会直接反映到最终的吞吐上。我写这篇东西的出发点是自己在做算子移植和性能调优时踩过的一堆坑。网上讲NPU架构的文章不少但大多停留在“有HBM、有DMA、有Shared Memory”这种名词罗列层面真正把数据流动路径讲透、把参数选择讲清楚的很少。所以我把自己的理解整理出来适合两类人看一是刚接触NPU、想搞明白片上数据到底怎么走的人二是已经在写算子、但性能总是上不去、想从数据流角度找瓶颈的人。核心关键词就三个HBM、DMA、多bank Shared Memory。这三个东西串起来就是NPU数据流的主干道。2. 整体数据流设计为什么是这三层结构2.1 存储层次的基本盘容量、带宽、延迟的三角博弈任何计算芯片的存储设计本质上都是在容量、带宽、延迟这三个维度上做妥协。HBM容量大、带宽高但延迟也高而且功耗不低片上SRAM也就是我们说的Shared Memory延迟极低、带宽可以做得非常宽但面积贵、容量小。中间必然需要一个搬运机制把数据从大容量存储搬到小容量高速存储这就是DMA的职责。你可以把这套结构类比成厨房HBM是冰箱容量大但拿东西慢Shared Memory是案板就在手边切菜方便但面积有限DMA就是那个来回跑腿的人负责把食材从冰箱搬到案板。厨师计算单元只负责在案板上切菜如果跑腿的人不够快或者路线规划不好厨师就得干等着。这个类比里有个关键点容易被忽略案板往往不止一块。这就是多bank Shared Memory的由来。单块案板一次只能放一样东西多块案板可以让厨师同时处理不同食材前提是跑腿的人知道该往哪块案板上放。2.2 为什么不用统一大缓存有人会问为什么不干脆做一块巨大的片上SRAM省掉DMA这一层答案很直接面积和成本。片上SRAM的位密度远低于HBM做大了芯片面积爆炸良率下降成本飙升。而且大容量SRAM的访问延迟也会上升物理上离得远的bank走线延迟就够你喝一壶。另一个原因是灵活性。DMA作为一个独立的搬运引擎可以设计成高度可编程的支持各种数据布局变换——比如把HBM里连续的NCHW张量搬成片上计算需要的分块格式。如果让计算单元自己去HBM取数这种布局变换会变得非常低效。2.3 数据流设计的核心目标让计算单元永远不等数据整条链路的设计目标可以浓缩成一句话让计算单元永远有数据可算。听起来像废话但真正做到很难。因为搬运和计算的速度往往不匹配你需要让它们重叠起来——在算当前这块数据的时候下一块已经在搬了。这就是所谓的double buffering或者multi-buffering。实现重叠的前提是DMA和计算单元能并行工作而且Shared Memory有足够的空间容纳多份数据。这就引出了后面要讲的bank划分和buffer管理策略。提示评估一个NPU数据流设计好不好最直接的办法是看它在典型算子上的DMA利用率和计算利用率。如果两者都高说明重叠做得好如果DMA利用率低而计算单元在等说明buffer不够或者调度有问题。3. HBM侧的关键细节不只是“大内存”那么简单3.1 HBM的物理特性对数据流的影响HBMHigh Bandwidth Memory通过硅中介层与计算die互连位宽可以做到非常宽比如1024bit甚至更宽这是它能提供高带宽的根本原因。但高带宽不等于任意访问模式都能跑满。HBM内部由多个channel和bank组成连续访问和随机访问的实际带宽差距可能有好几倍。这意味着DMA在搬运数据时访问模式非常关键。理想情况下DMA应该以大块连续的方式读取HBM充分利用burst传输。如果搬运的粒度太小、地址太分散HBM的实际带宽会大打折扣。我在实际项目里遇到过一个典型问题某个算子的输入张量在HBM里是跨步存储的stride很大DMA按元素逐个搬运结果HBM带宽利用率只有理论值的30%不到。后来改成先做一次布局重排把数据整理成连续块再搬带宽利用率直接拉到80%以上。3.2 地址对齐与突发长度HBM访问对地址对齐有要求。通常建议DMA的搬运起始地址按burst长度对齐比如256字节或512字节这样每次访问都能命中完整的burst不浪费带宽。如果起始地址没对齐硬件可能需要进行read-modify-write效率会明显下降。突发长度burst length的选择也需要权衡。太短命令开销占比高太长灵活性差可能搬了用不上的数据。一般经验是搬运粒度控制在几KB到几十KB之间比较合适具体要看Shared Memory的bank大小和计算单元的分块需求。3.3 多通道并行访问HBM通常有多个独立通道DMA如果能把搬运请求分散到不同通道上可以进一步提升有效带宽。这要求DMA引擎支持多通道并发或者至少能把大块搬运拆分成多个子任务分别落到不同通道的地址范围上。这里有个实操技巧如果数据在HBM里的物理地址分布已知可以在软件层面把大张量切分成多个子块让DMA描述符分别指向不同通道的地址区间。这样即使DMA引擎本身不支持自动通道交织也能通过软件调度实现类似效果。4. DMA引擎数据流的调度中枢4.1 DMA描述符的结构与编程模型DMA的工作方式是“描述符驱动”的。每个描述符描述一次搬运任务源地址、目的地址、搬运长度、步长信息、以及一些控制标志。软件把描述符链写好DMA引擎按顺序执行。一个典型的描述符包含这些字段字段说明常见取值src_addrHBM侧源地址按burst对齐dst_addrShared Memory目的地址按bank对齐length搬运字节数几KB到几十KBsrc_stride源侧步长连续时为0dst_stride目的侧步长连续时为0flags中断、链式等控制位按需设置描述符链的设计直接影响DMA的调度效率。如果每个描述符只搬很小一块描述符本身的读取和解析开销就会占比过高。一般建议单个描述符搬运的数据量不要太小同时用链式描述符减少软件干预。4.2 搬运与计算的重叠机制实现重叠的核心是双缓冲或多缓冲。具体做法是把Shared Memory划分成若干块bufferDMA往buffer A写的时候计算单元从buffer B读。等A写完了计算单元切到ADMA去写B如此循环。这里的关键参数是buffer数量和每块buffer的大小。buffer太少重叠不充分buffer太多Shared Memory不够用而且管理开销上升。实践中2到4块buffer是比较常见的配置。还有一个细节DMA写buffer和计算单元读buffer之间需要同步。通常用信号量或屏障机制来协调确保计算单元不会读到还没写完的数据DMA也不会覆盖正在被读的数据。这个同步如果做得太粗会引入不必要的等待做得太细又会增加控制开销。4.3 DMA调度策略优先级与抢占当多个计算任务共享一个DMA引擎时调度策略就很重要了。简单的做法是FIFO先来先服务。但实际场景中不同任务的紧急程度不同有些任务的计算单元在等数据有些则还在处理上一批数据。这时候如果能给紧急任务更高的DMA优先级整体利用率会更好。更高级的做法是支持DMA任务抢占。当一个高优先级任务需要搬运时可以打断正在进行的低优先级搬运等紧急任务完成后再恢复。这需要DMA引擎保存和恢复上下文硬件复杂度更高但在多任务场景下收益明显。注意DMA抢占虽然好用但会带来上下文切换开销。如果抢占太频繁反而会降低整体效率。建议只在任务优先级差异明显、且高优先级任务确实紧急时才启用。5. 多bank Shared Memory片上数据的高速公路5.1 bank划分的底层逻辑Shared Memory之所以要分bank核心目的是支持并行访问。如果所有数据都在一个大块里同一时刻只能服务一个访问请求带宽就上不去。分成多个bank之后只要访问落在不同bank上就可以并行进行。bank的划分方式通常有两种按地址低位交织interleaving和按地址高位分块blocking。交织方式下连续地址会轮流落到不同bank适合顺序访问分块方式下一段连续地址固定在一个bank适合随机访问。实际设计中往往两种方式结合使用。bank数量不是越多越好。bank多了每个bank的容量就小可能放不下一个完整的数据块而且bank的译码逻辑和互连复杂度也会上升。常见的bank数量在8到32之间具体取决于计算单元的并行度和数据块大小。5.2 bank冲突性能的隐形杀手bank冲突是Shared Memory性能的头号敌人。当多个访问请求落到同一个bank时硬件只能串行处理带宽直接打折。冲突的根源通常是访问模式设计不当。举个典型例子假设有16个bank计算单元需要同时读取一个16x16矩阵的某一列。如果矩阵按行存储每一列的元素地址间隔是16正好都落在同一个bank上因为16是bank数的整数倍这就是最坏情况的bank冲突。解决办法通常是padding——在每行末尾加几个填充元素打破地址的规律性。比如16x16矩阵按行存储时每行实际分配17个元素的空间这样列访问的地址间隔变成17就不会都落到同一个bank了。代价是浪费了一些存储空间但换来的带宽提升非常值得。5.3 多bank下的数据布局设计数据在Shared Memory里的布局直接决定了计算单元能不能高效访问。布局设计要考虑两个因素计算单元的访问模式和bank的并行能力。对于矩阵乘法这类算子常见的做法是把矩阵分块tiling每个tile放到不同的bank上。计算单元处理一个tile时只需要访问对应的bank不会和其他tile的访问冲突。同时tile内部的数据布局也要考虑确保计算单元读取时不会产生bank冲突。这里有个经验布局设计最好和DMA的搬运格式协同考虑。如果DMA搬进来的数据布局就已经是计算单元需要的格式就不需要在Shared Memory里再做一次重排省时省力。这就要求在写DMA描述符时就把目标布局考虑进去。6. 完整数据流实操从HBM到计算单元的端到端走一遍6.1 场景设定与参数计算假设我们要实现一个矩阵乘法算子输入矩阵A是MxK矩阵B是KxN输出矩阵C是MxN。NPU的Shared Memory总容量是256KB分成16个bank每个bank 16KB。计算单元一次能处理16x16的输出tile。先算一下数据分块。为了计算一个16x16的输出tile需要A的16xK行和B的Kx16列。假设K256那么A的tile是16x256B的tile是256x16。以fp16存储A的tile是8KBB的tile是8KB。一个输出tile需要16KB的输入数据。256KB的Shared Memory如果全部用来做双缓冲可以放8组这样的tile。但实际不会全用因为还要留空间给输出和其他临时数据。假设我们用4组buffer做输入双缓冲每组16KB共64KB剩下的空间给输出和其他用途。6.2 DMA搬运描述符的编写DMA需要把A的tile和B的tile从HBM搬到Shared Memory。A的tile在HBM里是原始矩阵A的一部分地址不是连续的因为A是MxK取16行的话每行之间间隔K个元素。所以DMA描述符需要支持步长搬运。一个可能的描述符配置// 搬运A的16x256 tile dma_desc_t desc_a { .src_addr A_base row_start * K * sizeof(fp16), .dst_addr smem_base buffer_idx * TILE_SIZE, .length 256 * sizeof(fp16), // 每行搬运256个元素 .src_stride K * sizeof(fp16), // 行间步长 .dst_stride 256 * sizeof(fp16), // 目的侧连续 .num_rows 16, // 搬运16行 .flags DMA_FLAG_CHAIN };B的tile类似但B是KxN取256行16列步长是N。6.3 计算与搬运的重叠调度调度逻辑大致是这样的DMA搬运第0组A、B tile到buffer 0DMA搬运第1组A、B tile到buffer 1与上一步可以并行如果DMA支持多任务计算单元等待buffer 0就绪开始计算计算单元计算buffer 0的同时DMA搬运第2组到buffer 2计算单元计算完buffer 0切到buffer 1同时DMA搬运第3组到buffer 0复用如此循环这个调度里buffer的数量决定了能重叠多深。2组buffer只能重叠一级3组可以重叠两级以此类推。但buffer越多同步逻辑越复杂而且Shared Memory占用越大。6.4 实测数据与调优记录在一个实际项目里我用类似的配置跑矩阵乘法初始版本的DMA利用率只有45%计算利用率60%。分析后发现两个问题一是DMA描述符的搬运粒度太小每个描述符只搬4KB描述符解析开销占比高二是bank冲突严重A的tile按行存储时计算单元读取列方向数据产生大量冲突。调整方案把搬运粒度加大到16KB减少描述符数量在A的tile每行末尾加8个元素的padding打破bank冲突。调整后DMA利用率提升到78%计算利用率提升到85%。整体吞吐提升了约60%。这个案例说明数据流的调优往往不是单一因素而是多个细节叠加的结果。搬运粒度、bank布局、buffer数量、同步策略每一个都值得仔细推敲。7. 常见问题与排查技巧实录7.1 DMA带宽上不去怎么定位先看DMA描述符的搬运粒度。如果单个描述符搬运的数据量小于1KB描述符开销很可能就是瓶颈。试着加大粒度看带宽是否提升。再看HBM访问模式。如果源地址跨步很大或者访问不连续HBM的实际带宽会远低于理论值。可以用性能计数器看HBM的row hit rate如果命中率低说明访问模式对HBM不友好。最后看DMA引擎本身是否成为瓶颈。有些DMA引擎的并发能力有限多个任务排队时会出现拥塞。这时候需要调整任务优先级或者把大任务拆分成多个小任务并行提交。7.2 Shared Memory bank冲突怎么发现和解决bank冲突通常表现为计算单元利用率下降但DMA利用率正常。因为数据已经搬到了Shared Memory但计算单元读的时候被bank冲突卡住了。发现方法用性能计数器统计bank冲突次数。如果冲突次数占总访问次数的比例超过10%就值得优化。解决方法调整数据布局加入padding或者改变计算单元的访问顺序让同一时刻的访问分散到不同bank。有时候把数据在Shared Memory里做一次转置就能大幅减少冲突。7.3 双缓冲不生效计算和搬运没有重叠最常见的原因是同步做得太保守。比如计算单元在开始计算前等待所有DMA任务完成而不是只等当前buffer就绪。这会导致DMA和计算串行执行重叠完全失效。检查方法看时间线上DMA活动和计算活动是否有交叠。如果没有交叠就是同步粒度太粗。解决把同步细化到每个buffer级别。计算单元只等待它即将消费的那个bufferDMA也只等待它即将写入的那个buffer空闲。其他buffer的搬运和计算可以自由并行。7.4 问题速查表现象可能原因排查方向解决思路DMA带宽低搬运粒度小看描述符大小加大单次搬运量DMA带宽低HBM访问不连续看row hit rate重排数据布局计算利用率低bank冲突看冲突计数器加padding或改布局计算利用率低数据未就绪看同步等待时间细化同步粒度无重叠同步太粗看时间线按buffer同步无重叠buffer不足看buffer数量增加buffer提示排查数据流问题时时间线工具是最好的朋友。把DMA活动、计算活动、同步事件都画在一条时间线上瓶颈一目了然。很多问题不需要复杂分析看一眼时间线就清楚了。8. 一些踩坑之后的个人体会数据流调优这件事最忌讳的就是一上来就调参数。我见过不少人听说buffer数量影响性能就把buffer从2加到4结果Shared Memory不够用反而引入了更多换入换出。正确的做法是先搞清楚瓶颈在哪再针对性地调。另一个体会是DMA描述符的设计比想象中重要。很多人把DMA当成一个简单的memcpy引擎描述符写得很随意。但实际上描述符的粒度、链式结构、地址对齐都会显著影响DMA的实际效率。花时间把描述符设计好往往比调其他参数收益更大。还有一点多bank Shared Memory的布局设计最好在算子开发初期就考虑进去而不是等到性能出问题了再回头改。因为布局一变DMA描述符、计算单元的访问代码都要跟着改返工成本很高。前期多花点时间设计后期省很多事。最后分享一个小技巧如果bank冲突实在难以避免可以考虑用Shared Memory的广播机制。当多个计算单元需要读取同一个地址的数据时硬件可以广播给所有请求者只占一次bank访问。这在某些特定算子比如卷积里的权重共享中非常有用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑