资讯动态

NPU性能优化:HBM-DMA-Shared Memory数据流链路解析

发布时间:2026/10/18 17:03:22 来源:尧图企业网站定制
提到NPU性能大多数人第一反应是看TOPS、看MAC阵列规模、看主频。我过去也这么干直到有一次带着一个模型去做效率测试理论算力明明够实际帧率却差了三成最后定位到的问题全部集中在数据搬运上HBM带宽利用率不到一半DMA描述符配置错了一行Shared Memory还因为bank冲突白白浪费了20%的访问周期。从那以后我在做架构分析时都会先画一条数据流主线从HBM出发经过DMA到多bank Shared Memory最后才看计算单元。这篇文章就把这条主线完整梳理一遍。我不打算写那种教科书式的架构综述而是按一个NPU实际执行算子时的数据路径来走数据从HBM出来DMA负责把它搬到片上Shared Memory用多bank的并行访问保证计算单元随时能取到数。每一段我都会给参数计算方法、典型坑和实操心得。适合刚接触AI芯片架构、或者在做NPU算子调优时总被访存问题卡住的同学。1. NPU数据流全景先建立一张“搬运算力”的地图1.1 一条完整的数据路径是什么NPU和普通CPU最大的区别在于它默认数据会以“块”为单位流动而不是一条指令一条数据。常见的NPU执行链路是这样的主机或内部的标量核先解析算子生成一组搬运和计算指令计算需要的数据包括权重、激活值、可能的稀疏索引最开始都存在容量大但延迟高的主存里也就是HBM标量核把搬运描述符交给DMA引擎DMA按照指定地址和时间表把数据块搬到片上的一块高速缓存区这块缓存通常叫作Shared Memory或片上SRAM计算阵列再从Shared Memory中取输入和权重算完把结果写回Shared Memory再由DMA搬回HBM。整条链路里真正参与计算的时间往往只占一小部分更多的时间花在了“准备好数据”上。所以看NPU数据流不要只盯着计算单元要把HBM的带宽、DMA的搬运能力、Shared Memory的访问并行度三者放在同一个时钟域里去估算。任何一个环节拖后腿计算单元的吞吐都会跟着掉。1.2 算力是理论值数据流才是真实值Roofline模型在AI芯片调优里非常实用。它把性能上限看成两条线一条是计算上限单位是每周期能执行多少次乘加另一条是访存上限单位是每秒能从存储层次搬多少个字节。执行任何一个算子实际性能都落在两条线之中较低的那条上。判断一个算子是计算密集还是访存密集核心指标是算术强度也就是每搬运一个字节的数据平均能执行多少次乘加操作。我用一个生活化的类比来记忆这条关系。计算单元像一口正在烧菜的大锅HBM是离厨房很远的仓库DMA是传菜员Shared Memory是灶台边的备菜台。锅的火力再猛若传菜员一趟一趟跟不上备菜台上什么都没有锅也只能空烧。很多人做算子调优时只顾着换更大的“锅”也就是提升MAC利用率却忘了先确认备菜台上的菜是不是按时到位。这个顺序反了改来改去都改不到点子上。1.3 数据流设计要回答的三个问题任何NPU数据流方案本质上都在回答三个问题数据放在哪里、数据怎么过去、数据到了之后怎么并行取用。HBM负责解决第一个问题的容量部分DMA负责第二个问题的搬运部分多bank Shared Memory负责第三个问题的并行部分。三个问题互相牵制HBM带宽决定了DMA能搬多快DMA的吞吐决定了Shared Memory需要多高的访问带宽Shared Memory的bank数量和宽度又反过来决定计算单元能不能在一个周期内拿到足够多的数据。这三个问题必须放在一起权衡。比如Shared Memory做得又大又宽看似能缓解bank冲突但片上面积有限留给计算阵列的资源就少了DMA做得很宽看起来搬得快但若HBM地址映射不均匀DMA也只能干等。设计或调优时脑子里要有这根链条否则很容易陷入“改了这头、坏了那头”的死循环。2. HBM第一公里决定了整条通路的最高水位2.1 HBM凭什么比普通DDR快HBM的核心思路是堆叠和加宽。普通DDR走的是“窄而快”的路线单颗颗粒位宽只有几十位靠极高频率把带宽顶上去HBM则用TSV硅通孔把多层DRAM die垂直堆在一起对外一次性提供1024位甚至更宽的接口。你可以把DDR想象成一条单向双车道车虽然跑得快但满载有限HBM像是十六车道的干线单车道频率未必最高可是整体并行度是碾压式的。所以NPU几乎都选HBM做大容量主存。以一套四堆栈的HBM配置来看每堆栈位宽1024bit数据率按3.2Gbps计算理论总带宽是4×1024×3.2/8也就是大约1.6TB/s。这个数字是DMA和后续存储层次设计的重要参考。设计Shared Memory时若片上DMA搬运带宽只有HBM的一半HBM性能再高也没意义反过来若DMA吞吐远超HBM可持续提供的速率那DMA通道配置得再漂亮也只能等数据慢慢从接口上挪出来。2.2 实际带宽和理论带宽之间差了些什么理论带宽只是峰值。实际跑起来至少有四类开销会让可用带宽缩水。第一是刷新。HBM是动态存储必须周期性刷新刷新期间通道不能做正常读写。堆叠越深、容量越大刷新占用的时间占比越高遇到某些刷新策略还可能在带宽图上形成周期性“谷底”。第二是ECC。大容量主存需要错误校验读出一条数据往往要多读ECC位写一条数据也可能产生额外的读-改-写周期。对神经元数据来说个别bit的轻微错误不一定影响最终精度但控制器严格开启ECC后吞吐下降是看得见的这也是有些场景在功耗和稳定性之间权衡的由来。第三是伪通道的粒度。HBM内部被切成多个伪通道若请求地址集中在同一个伪通道的几个行里外部虽然有多通道实际有效并行度却上不去。想要吃满HBM带宽得让请求地址尽量均匀散到各个伪通道和各个bank里。我常见的方式是地址映射做hash把连续的物理地址按一定规则打散避免多路DMA请求全部砸向同一片区域。第四是访问模式本身的突发性。一次burst的长度、是否跨行、读和写是否频繁切换都会影响总线效率。连续大块搬移永远是HBM最喜欢的模式频繁的小块随机读写则是带宽杀手。2.3 应用层面怎么跟HBM打交道从软件和算子层的视角跟HBM相关的主要有三件事。第一是理解地址交织。配置多个DMA通道分别搬运不同tensor时不要把相邻地址块硬分给不同通道而要让它们各自持有地址跨度较大的区间或依赖硬件hash自动完成分散。否则会出现“所有通道都忙但带宽只有一条车道在用的假并发”。第二是多轮复用数据。同一个权重若被多个输出tile使用最佳策略是让它尽量留在片上而不是每次都从HBM重新搬。用大白话说仓库里的菜要一次多拿点放备菜台不要炒一个菜回一趟仓库这点在后面的tile切分里还会细说。第三是预留足够的请求队列深度。HBM控制器都有原子队列若请求队列太浅吞吐会被拥堵式的背压限制住。搭性能测试环境时记得把控制器对读写的仲裁策略测一遍看看混合读写下面谁占了便宜这一项很影响最终模型跑批的表现。3. DMA数据流的血管也是最容易写错的地方3.1 为什么必须有一个专门干粗活的引擎如果让标量核亲自去从HBM把几十上百KB的数据一块块搬进Shared Memory那标量核会完全被搬运工作淹没既没法解析下一条指令也没法做同步调度。所以NPU里必须有独立的DMA引擎。它的任务很纯粹按照描述符里给出的源地址、目的地址、长度、步长和计数自动把数据搬完并在完成后发一个事件。DMA在数据流里的角色有点像物流调度中心。它不用理解数据是什么只需要可靠地按计划发货。一旦计划本身错了比如目标地址重叠、步长算错、粒度不对齐那它也会非常高效地搬出一堆错误数据而且不报错。这是DMA最具迷惑性的地方后面我会专门展开。3.2 从一维搬送到二维描述符的正确打开方式绝大多数NPU的DMA都支持多种搬运模式。一维模式最简单给定源地址、目标地址、总长度一次搬完。但算子里的数据大多是二维或三维的比如一张tensor的某个矩形tile、权重矩阵中按行取的一段这时一维模式就不够用了得用带stride的描述符。我见过的最常见错误就是把二维搬运里的行间步长当成行内长度。举个例子要搬一个32行、每行64个float的tile每个float占4字节行内数据长度应该是256字节。可源矩阵实际是按512字节一行排列的每行后面还存着别的数据。正确的二维描述符应当让DMA每次从源地址出发搬256字节然后地址向后跳512字节作为下一行的起始。所以行间步长要设为512字节而不是256字节。写错步长的表现是搬出来的数据每一行都比预期错开一段越到后面的行错得越离谱。下面给一个常见的描述符结构不限定具体硬件字段基本相同。struct dma_desc { uint64_t src_addr; // 源地址需按DMA对齐要求 uint64_t dst_addr; // 目的地址通常也要求对齐 uint32_t xfer_len; // 单次搬运长度单位字节 uint32_t src_stride; // 源侧相邻两次搬运的地址间隔 uint32_t dst_stride; // 目的侧相邻两次搬运的地址间隔 uint32_t count; // 重复次数 uint32_t flags; // 中断使能、方向等 };需要特别提醒的是stride的计算必须用字节而不是元素个数。int32的数组每元素4字节若一行有64个元素但行间有128字节的填充行内xfer_len是256字节src_stride是256加128等于384字节而不是64或128。我在多个项目中都用同样的检查方法手动推演第一行、第二行的搬运目标地址确认不重叠、不错位再上板跑能省掉大量调试时间。3.3 同步、预取和乒乓缓冲DMA是异步引擎描述符下发后计算单元不能立刻读Shared Memory里的数据必须等DMA完成事件。粗暴的同步方式是发完描述符后原地轮询完成标志这种办法简单但把DMA的并行能力全浪费了。我更推荐乒乓缓冲的做法。Shared Memory或片上缓冲被划分成两个半区计算单元在读半区A时DMA已经把下一块数据往半区B搬等计算完成再让DMA填半区A计算单元切到半区B读。只要单次搬运时间不大于计算时间计算单元就永远不会等数据。这个“用空间换流水”的思路几乎适用于所有访存密集的算子。描述符队列的有效管理也能明显提升效率。DMA引擎通常支持环形描述符队列标量核可以提前把一批描述符连续写入队列DMA自动取指执行。每条描述符做完后通过事件通知不必每条都打断标量核。批量下发还方便硬件做调度优化比如在描述符里提前做地址排序减少总线切换和bank冲突。我习惯把同一个算子的所有搬运描述符一次性下发再启动计算这种批处理方式在硬件上往往能形成更连续的总线访问流。3.4 对齐与burst长度的隐藏门槛DMA描述符里还有一个容易踩坑的字段是地址对齐。很多DMA引擎要求源地址、目的地址按16字节或32字节对齐否则要么报错要么把一次搬运拆成两次低效的burst。更麻烦的是不对齐的搬运在边界上会写入相邻字节破坏其他数据。处理tensor切片时尤其要小心起始地址在中间的情况。一个实用的做法是在算子设计阶段就把tile的起始地址统一按DMA的对齐粒度计算必要时在Shared Memory里做索引偏移而不是直接换行搬运。burst长度直接影响HBM效率。DMA搬长连续块时控制器能把同一行打开用连续burst把一整行读完若burst被限制得很短每个小burst都要重新预充电和激活HBM带宽会白白损失。因此设计描述符时xfer_len尽量取大的连续段而把stride差异留到二维层级去表达这比拆成几百条一维小描述符要高效得多。4. 多bank Shared Memory让计算单元的取数不用排队4.1 片上缓存为什么非建不可HBM延迟高几十个周期起步计算单元直接去读根本等不起而且每读一个数就耗费一次片外带宽。权重若反复利用带宽在第一次循环就会被耗尽。所以在DMA和计算阵列之间放一层Shared Memory本质上是做数据和权重的复用。数据从HBM搬上片一次可以在片上被计算单元访问很多次牺牲一点片上的面积换取带宽的成倍节省。几乎所有的NPU设计里Shared Memory容量都是跟MAC阵列规模和常用算子tile大小绑定设计的。容量太小权重放不下算子每做一遍都得等HBM喂容量太大又浪费面积和功耗留给计算阵列的资源就少了。实际选型时通常会统计目标模型里最大的单层权重尺寸、最大滑窗输入块再乘上一个安全的冗余系数才定下来。4.2 多个bank到底解决了什么问题Shared Memory虽然叫SRAM物理上由一组SRAM子阵列组成。如果只有一个阵列一个周期只能被访问一次即使计算单元同时想读四份数据也只能排队。多bank设计就是把这些子阵列做成多个可独立寻址的单元只要访问请求落在不同bank它们就能在同一周期并行完成。用数字说话。假设Shared Memory容量64KB按每bank宽度4字节划分成32个bank一个32位地址的bank编号可以近似用addr/4再对32取模得到。计算单元同一周期发起四个地址的读请求如果这四个地址分布在四个不同bank一次周期全部返回假如其中两个地址落在同一个bank这一个bank就得承担两次访问等于该周期少返回了一份有效数据这就是bank冲突。bank冲突直接削减有效带宽但不会导致结果错误所以特别容易被忽略。4.3 bank冲突的典型场景和padding解法最容易触发bank冲突的访问模式之一是“按固定stride取连续元素”。比如要处理一个按行存储的矩阵A[32][32]每行32个float某次计算需要同时取A[i][0]、A[i1][0]、A[i2][0]三行同列的数据。它们的地址分别相差32个float也就是128字节。bank编号按4字节切分这三行第0列的bank编号恰好相等一次并行读三份数据就会发生冲突。反过来如果只按顺序访问同一行的连续元素它们天然落在相邻的不同bank反而不会有问题。解决冲突的标准手法是在数组行尾做padding。把分配改成float A[32][33]每行多一个float行间间隔从128字节变成132字节。由于132除以4后对32取模余1相邻行的同列数据bank编号正好错开1三行同列访问便能并行返回。付出的代价只是每行多4字节的存储浪费换来的是访问带宽显著提升。padding不是唯一解法。有些场景可以直接改遍历顺序把原来按行访问改成按列分块有些场景靠硬件在地址映射里增加随机化或位重排。但站在算子开发者的角度padding是最可控、最不需要改硬件的方法。我的经验是先把数组维度列一张表对每一个二维访问点算出它在每个维度下的stride模bank数的值如果某个维度的stride模bank数等于0且该维度确实存在并行访问那就优先对这一维做padding。4.4 Shared Memory里的容量分配与分区原则实际部署算子时Shared Memory要同时装下权重、输入tile、输出tile还得给乒乓双缓冲留空间。通常来说权重占用最大因为它要反复使用输入tile随滑窗移动不断更换输出tile算一块搬走一块不必长期占用。分配时按优先级排序权重常驻输入给足输出够用即可。若容量紧张优先压缩输出tile其次压缩输入tile权重尽量不要反复从HBM拉因为权重通常是被复用次数最多的数据。多bank结构也会影响分配策略。如果两个数组在地址上恰好错开整数倍于bank周期的宽度它们即使独立分配也会形成“地址对齐”的固定冲突。分配时让不同数组的起始地址故意偏移几个bank宽度常能被动规避一批冲突。这类优化很难靠肉眼发现最好留一个性能计数器统计bank conflicts专门用来引导分配调整。我在代码里一般会保留一份当前算子的bank布局表每次改动后重跑统计确认冲突率没有回流。5. 串联起来一个卷积tile的完整数据流核算5.1 先给一个小型算子的配置下面用一个完整的卷积例子把HBM、DMA、Shared Memory串起来核算一遍。假设输入是64×64×64通道数64卷积核3×3×64输出通道64输出也是64×64×64。片上参数按一套紧凑配置来定Shared Memory 256KB、32个bank、每bank 4字节DMA峰值搬运带宽16GB/sMAC阵列512个乘加单元、主频1GHzHBM理论带宽1.6TB/s。权重总量是3×3×64×64个float共36864个数144KB。它是需要反复使用的数据所以设计目标很明确尽量整个留在Shared Memory里。但输入、输出tile也要占空间容量就紧张了。这里把权重按输出通道切成两批每批72KB分两次遍历tile。每次使用一半权重输出也只算一半通道。5.2 单tile的搬运量和计算量到底是多少输入tile取16×16×64一共16384个数64KB。输出tile在第一批权重时取16×16×3232KB第二批同样取32KB。第一批要从HBM搬入输入tile 64KB、权重72KB计算完再把32KB输出搬回HBM合计168KB第二批重复一次也是168KB。整个tile的全部往返搬运量约336KB。计算量方面每批权重只覆盖32个输出通道但两批加起来要对全部64个输出通道完成16×16×64×3×3×64次乘加。这个数算下来约940万次乘加换算成512并行度、1GHz主频两批计算时间合计约18微秒。真实硬件还要考虑填充、边界处理和累加路径这里把公式简化成每个输出像素对应完整的3×3窗口加所有输入通道为的是把数量级和比例关系讲清楚。再看DMA时间。336KB的搬运量除以16GB/s峰值带宽理论耗时约21微秒。一个tile的计算时间约18微秒搬运明显比计算慢所以这个算子在当前tile策略下是访存瓶颈。此时掐表观察会看到计算单元的PE stall信号长期拉高整卡利用率上不去。5.3 优化方向把权重变成“常住户口”同样的算子如果改变tiling策略把144KB权重整体常驻一次搬上片后不再反复读取每个tile只需要搬输入和输出。为了给权重让出空间输入tile从16×16×64缩小为8×8×64这时输入tile是16KB输出tile是16KB。空间占用144加16加16等于176KB完全放得下。重新核算单个tile的时间。8×8×64对应的计算量约236万次乘加计算时间约4.6微秒输入输出合计搬运32KBDMA时间约2微秒。此时计算时间明显大于搬运时间瓶颈稳定移到计算端。做这个优化时我总结出的原则是复用次数越多的数据越要固定住在Shared Memory里临时数据可以频繁换血让数据流从“每次都长途搬运”变成“长途一次、短途多次”。这里还得补一句缩小输入tile并不总是免费的午餐。tile变小后总共需要处理的tile数量会变多边界重叠区域也会有重复计算和重复搬运。在真正落地时要看tile变小带来的冗余开销会不会抵消权重常驻的收益。比较好的做法是用总搬运量和总计算量同时做一次全局核算不要只看单个tile的快慢。6. 常见问题与排查技巧实录6.1 一张速查表解决八成数据流问题我把日常调试中源于数据流的典型问题做成一张速查表现象、根因、排查顺序按表走能少走很多弯路。现象根因排查方法常规解法计算单元长期等待数据效率不到五成没有做预取或乒乓缓冲DMA和计算串行看PE stall计数器对比DMA busy时间上双缓冲把搬运和计算重叠DMA搬出来的数据错位越到后面越乱二维描述符的stride写成了行内长度手推第一行、第二行的目标地址用字节数重算src_stride和dst_strideShared Memory带宽上不去延迟异常高访问stride模bank数为0发生bank冲突打开bank conflict性能计数器行尾padding或重排存储维度多通道DMA都在跑但HBM带宽利用率仍低地址交织不合理请求扎堆在少数伪通道观察各伪通道的活跃度统计调整地址hash分散连续访问块计算结果偶尔不对且难以复现DMA还没完成就去Shared Memory取数查事件同步是否遗漏barrier是否加上在DMA完成事件后再发计算指令同一份数据读出来有时新有时旧缓存或Shared Memory与HBM的一致性策略没理清检查写回时机和invalidate命令在合适位置插数据同步指令6.2 调数据流问题的顺序比技巧更重要查数据流问题我强烈建议按“从近到远”的顺序来。先确认计算单元等的是不是数据也就是PE stall再查Shared Memory的bank冲突计数这一步能排除片上访问瓶颈最后才查DMA和HBM带宽。很多人上来就盯HBM结果查了半天发现瓶颈在片上的bank冲突HBM压根没被压满。顺序反了定位时间会成倍增加。我还会在算子日志里把DMA描述符的源地址、目标地址、stride、长度全部打出来再写一个离线校验脚本用模拟器把每个描述符的地址序列算一遍。不要以为自己人肉推演很可靠二维甚至三维tile的地址索引手一抖就会错一个括号。脚本能在一秒内把几十个描述符全部验证完。我踩过的最深的一次坑是一个看起来完全正常的描述符因为第4行之后的地址落到了一组偏置数据所在的区域搬完数据后把模型权重连带改了结果丢了一整个下午的定位时间。6.3 三组数字比TOPS更能反映NPU健康度我个人在实际调试中养成了一个习惯每个算子落地后固定记录三组数字HBM实际带宽、DMA有效带宽、bank冲突率。这三组数字比TOPS更能反映一个NPU算子的健康程度。HBM实际带宽可以用来判断远端的搬运是否压满DMA有效带宽能看出描述符和队列配得顺不顺bank冲突率则直接度量片上并行访问的效率。下次性能回退时先翻这三组数数据流的问题通常一眼就能被揪出来。比如某次模型迭代后帧率掉了10%其他人都在查算子改动我翻了记录发现bank冲突率从15%涨到30%马上想到是新增的那个tensor布局把某两路访问的stride模bank数变成了0。改完布局帧率立刻回来。这条经验比任何高大上的工具都好使。最后再分享一个小技巧如果是在仿真环境里查问题除了统计计数器还可以在DMA完成事件和计算启动指令之间插一个按周期计数的监测点看看这两者之间到底隔了多少个空转周期。空转越少说明预取和调度做得越好空转一旦明显优先去看是等待HBM数据、等待DMA完成事件还是等Shared Memory访问结果确认等待类型后再对着上面的速查表动手基本就不会瞎忙了。

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

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

免费获取报价 →
↑