搞AI芯片或者写底层算子的人多半都有这种体会硬件手册上写着算力有多少TOPS跑benchmark之前心里预期拉满结果一实测实际吞吐常常只有峰值的一半甚至更低。排查到最后十有八九问题都出在数据流上——不是算不过来而是数据喂不过来。这个现象在NPU上尤其明显因为你面对的是一条从片外HBM到片上多bank Shared Memory的完整搬运链路中间每一环的吞吐和延迟都不一样任何一环掉链子整条计算流水线都得等数据。这篇文章就专门把NPU的data flow这条链路翻出来讲透从HBM到底层DMA怎么搬数据再到Shared Memory为什么非要拆成多个bank以及这些硬件设计反过来如何决定你写卷积和矩阵乘时该怎么切分数据。看完你就能理解所谓NPU软件栈里的tiling调度本质上就是一群工程师在配合数据流的物理规律做妥协。1. 先盘一盘整条链路HBM、DMA、Shared Memory各管哪一段1.1 三个角色的分工与协作如果把一颗NPU芯片比作一家餐厅计算单元MAC阵列就是后厨的锅灶而数据通路就是传菜生。HBM相当于后厨外面的食材大仓库距离远、容量大、但是单次取货麻烦Shared Memory相当于后厨门口的备料台离锅灶最近容量小但取用极快DMA就是那个专门负责从仓库往备料台搬货的传菜生。它不负责做菜但是所有食材能不能在开火前就位全看它。这个类比里有几个数字值得记住HBM的访问延迟通常是几十ns到上百ns的级别而Shared Memory的访问延迟通常在几个cycle。一个cycle在多GHz主频下可能只有零点几ns也就是说两者差了至少一个量级甚至两个量级。更重要的是带宽差异HBM的带宽虽然高但它是被所有计算核心共享的而Shared Memory是每个计算核心私有的带宽高得多。所以NPU的架构设计者才必须在中间加一层DMA提前把数据从大仓库搬到备料台让计算单元在计算时永远面对的是低延迟、高带宽的片上存储。1.2 算力是纸面参数数据流才是真实性能我见过不少人在选型或者做算子优化时第一眼只看算力——TOPS越高越好仿佛算力上去了性能就一定上去。但芯片领域的常识是系统的稳态吞吐等于资源瓶颈端的吞吐。如果数据流只能每秒搬运100GB那你就算有再高的TOPS也是空转。反过来如果数据流转得足够快计算单元的利用率才能被拉满。NPU的data flow设计是否合理直接影响三件事一是计算单元的空闲率也叫pipeline bubble气泡二是功耗的浪费程度——搬数据本身要耗电搬重复数据更耗电三是软件编写的复杂度。硬件上把多bank Shared Memory的冲突消除做得好软件上就不需要费尽心思去padding、去改变数据布局开发效率高很多。所以不要觉得data flow只是硬件工程师操心的事任何一个写高性能算子的人都必须把这条链路刻在脑子里。2. HBM为什么NPU不用DDR而要叠DRAM2.1 HBM的堆叠结构把仓库建成楼房我们平时熟悉的DDR内存是平铺在一个PCB面上的颗粒走线长单位面积能提供的带宽有限。而HBMHigh Bandwidth Memory的逻辑完全不同它把多颗DRAM裸片通过硅通孔TSV垂直堆叠起来再用宽接口与底层的逻辑die相连。形象点说DDR是郊区的一排平房从仓库取一次货要走很长的路HBM是把仓库盖成了高楼并且每层楼都修了一条直达传送带货容量不小取货速度还快。这个堆叠方式直接带来了一个关键特性HBM的接口位宽非常大。普通DDR一条channel一般是64-bit而HBM的接口总位宽可达1024-bit甚至更高。你想想位宽翻十几倍即使频率不提高理论带宽也直接翻十几倍。所以HBM能够在一片相对小的面积内堆出TB/s级别的带宽这是DDR望尘莫及的。2.2 接口位宽、频率与读写粒度HBM的读写请求也很有特点——很高的位宽意味着每次读写的固定开销非常大。你随便读一个字节和读一大块连续数据在HBM上的效率差异非常明显。只有当访问地址连续、burst长度够长时HBM才能维持一个接近峰值的有效带宽。如果地址跳来跳去bank row切换和读写翻转的惩罚会把带宽拉到一半以下这在实际项目中很常见。所以你在很多NPU的DMA设计中会看到cacheline对齐建议64B/128B连续搬运之类的要求。这不是硬件的洁癖而是HBM这种高带宽介质的物理特性决定的。HBM适合大块连续数据不适合细碎随机访问。这也是为什么数据布局和tiling在AI计算中如此重要——你要让DMA尽量发出连续的、大批量的读请求而不是一会儿读A的尾巴一会儿读B的中间。2.3 从HBM到片上网络带宽被分摊后的现实HBM的带宽是整个芯片共享的资源不是某一个core独占的。一颗NPU里往往有好几个计算cluster、多个DMA通道大家都在跟同一个HBM控制器要数据。为了保证公平和效率芯片里通常还会有一层片上网络NoC把各个模块连起来。这意味着即使HBM的标称带宽有800GB/s你的某个计算核心真正能分到的带宽还要打折扣。我曾经测过一个模拟场景某个cluster的DMA持续读HBM的需求是300GB/s但实际系统里其他模块同时也在访问内存最终这个cluster的可用带宽只有标称的一半。所以在设计算子的tiling大小和双缓冲策略时一定要按照实际可用带宽去估算不要拿理论峰值去套。3. DMA数据流的搬运工但绝不只是搬运工3.1 描述符队列与搬运动作DMADirect Memory Access引擎干的活看起来简单把HBM里的数据挪到片上Shared Memory。但它怎么知道搬什么、从哪搬到哪这里就靠描述符descriptor了。软件事先在内存里写一批描述符记录源地址、目的地址、长度、方向等信息然后告诉DMA引擎你自己去读描述符然后按描述符干活。为什么要搞一个描述符队列而不是一单一单地手动触发因为这样可以批量下发、流水线执行。DMA引擎像一个自动化的装卸工货单描述符已经排好队了它一份份顺着搬搬完一个自动看下一个不需要CPU每次都插手。这个设计大幅减少了CPU的中断和配置开销你可以轻松维持一个很深的搬运流水线。3.2 双缓冲用一块数据的计算时间掩盖另一块的搬运时间DMA的核心价值在于和计算并行。假设我们要处理一个卷积层数据被切成很多个小块。如果先用DMA把第一个块搬进Shared Memory然后计算单元开始算算完之后停下来等DMA搬第二块——那计算单元有一大半时间在干瞪眼。于是就有了双缓冲double buffering把Shared Memory分成两个区DMA往这个区搬第二块数据的同时计算单元正在算第一块数据。两块数据的操作在时间轴上重叠计算单元几乎不需要等待。这块的工程细节在于双缓冲能不能完美重叠取决于搬运时间和计算时间是否大致匹配。如果一块数据搬运需要30微秒计算只用了10微秒那计算单元还是要等这时候说明tile切太大了反之如果搬运只要5微秒计算要30微秒那DMA其实早就空了浪费了带宽资源。真正理想的状态是两块时间接近让DMA和计算引擎同时处在繁忙状态。3.3 地址对齐、burst长度与bank交错我列出几个DMA调优中非常实际的门道地址对齐DMA搬运的源地址和目的地址最好对齐到cacheline常见64B或128B边界。错开的地址会造成burst被切碎搬同样的字节数要多花几乎一倍的时间。burst长度每次DMA请求尽量发出长burst16拍、32拍甚至更长。短burst在HBM上频繁发起请求固定开销占比高带宽利用率上不去。bank交错如果连续的大块数据恰好映射到同一个HBM bank连续访问会撞在同一个row上触发昂贵的bank预充电。所以HBM地址映射上通常做交错channel interleaving把连续地址散到不同channel和bank上。软件层面对此能做的就是保持访问模式是顺序的尽量不要人为制造跨bank的大跳跃。这些点都指向同一个原则DMA是被物理存储介质约束的软件必须顺着它的性子来。写算子时多花五分钟看一看到底是不是做了对齐可能比你在算法层面扣性能还管用。4. 多bank Shared Memory为什么要拆开又为什么会有冲突4.1 单口SRAM的带宽上限如果Shared Memory做成一整块那么一个cycle内只能支持一次读和一次写无法同时喂给几十个甚至上百个并行的MAC单元。NPU的计算单元一个cycle要消费几百上千个数据单块SRAM根本不够用。所以架构上必须把Shared Memory拆成多个bank每个bank是独立的SRAM电路可以并行读写。这就是多bank Shared Memory存在的最根本原因。你可以把多bank理解成备料台的多个货架。货架编号0、1、2…DMA把食材按顺序放在不同货架上。计算单元发出请求时同时去多个货架取货这样每个cycle能取到的总货量才能跟上计算速度。4.2 bank冲突两个请求同时打在一个货架上多bank并行听着美好但有个经典问题——bank conflict。当多个访客同时想从同一个bank的不同地址取数据时这个bank必须串行服务先服务一个再服务另一个于是总吞吐立刻下降。比如bank数量是8你访问stride正好是8的倍数那所有请求都会落在同一个bank上效率相当于退化成了单bank性能损失可能达到8倍。这也是为什么所有共享内存的编程手册都会着重提醒数组的某一维长度不要设置成bank数的整数倍。最常见的解法是padding填充比如明明需要访问8行数据每行32个元素但每行分配33个元素让相邻行错开bank避免行间冲突。这个多出来的1列就是为了打破访问冲突而牺牲的一点点容量。4.3 广播、双bank与硬件层面的消除手段硬件设计者当然不会把消除冲突的任务全部甩给软件他们还会用一些电路上的技巧来缓解问题。广播broadcast当很多lane想读同一个地址时比如读一个共享的缩放因子硬件支持把一份数据同时广播给所有请求者不需要串行处理。双bank读写某些NPU把Shared Memory的每个物理bank再拆成双口bank可以支持同一个cycle内两个不同地址的访问相当于把冲突概率减半。地址重映射用某种散列函数把地址重新映射到bank上让原来连续的数据天然地散开。代价是地址计算逻辑更复杂而且软件很难预测最终落到哪个bank——但总比每个cycle都撞车强许多。这些硬件的存在让软件不用做太多底层的坑道计算。但作为一个算子开发者你仍然需要知道当数据访问模式有规律性时冲突是可以被建模分析的。用一次profiling看到共享内存访问延迟高第一时间就应该怀疑是不是stride撞上了bank数。5. 从参数推演性能一个卷积层的tiling算给你看5.1 先定义一套教学级的NPU配置光讲概念不落地很虚我定义一个模型化的小型NPU配置用于教学推演不代表任何真实芯片参数数值计算核心数32每核心每个cycle的MAC数256主频1.2 GHzHBM总带宽512 GB/s片上Shared Memory总量1 MB总算力 32 × 256 × 2 × 1.2G 19.66 TOPS乘2是因为一个MAC等于两次浮点运算。我们用这个配置跑一个3x3卷积输入输出尺寸都是64×64通道256使用FP16。5.2 不tiling会怎样数据全走HBM带宽直接爆炸如果数据全放HBM、计算时一次性读入——这当然不可能因为仅输入就有64×64×256×2B 2MB已经超出1MB的Shared Memory更别提还有权重和输出。我们假设把它硬塞进Shared Memory分段处理做最粗放的tile策略比如每次只处理8个输出通道输出块16×16。每个tile需要的输入块是(162)×(162)×256×2B ≈ 166KB权重块是8×256×3×3×2B ≈ 36KB输出块4KB总计约206KBShared Memory放得下。但糟糕的是全卷积的计算量是2×64×64×256×3×3×256 ≈ 4.85 GFLOPs理论计算时间约4.85G / 19.66T ≈ 0.25ms。而每个tile搬运约206KBtile总数为(64/16)×(64/16)×(256/8)4×4×32512个总搬运量约105MB。0.25ms内搬105MB需要约420GB/s的带宽接近HBM上限。一旦内容里还有其他模块占用带宽实际分到的可能只有300GB/s左右那计算就真的要停下来等数据了。5.3 换一种切法把tile放大一切豁然开朗如果我把tile改大——每次处理16个输出通道、输出块32×32。输入块就需要(322)×(322)×256×2B566KB权重块16×256×3×3×2B72KB输出块32KB合计约670KB依然放得下。此时每个tile的计算量是2×32×32×256×3×3×16 ≈ 75.5 MFLOPs搬运量约670KBtile级算术强度从之前的9.4M/206K46 FLOPs/B提升到了75.5M/670K≈112 FLOPs/B。整个网络需要搬64个tile总搬运约43MB0.25ms内搬运43MB只需要约172GB/s的带宽HBM绰绰有余双缓冲和计算重叠之后计算引擎可以跑得很饱满。这就是同一个算法的两种切法性能差异可以接近2.5倍。原因不在计算全在data flow。**tiling的切法和算术强度直接挂钩算术强度低于硬件平台所需的FLOPs/B门槛性能就会被带宽锁死。**很多算子库的auto-tune在做的事情本质上就是在一个巨大的切法空间里搜索一组能让DMA搬运量最小、且Shared Memory放得下的tile参数。6. 工程上的几个常见坑对齐、数据布局、profiling怎么看6.1 地址对齐问题你以为省的事最终都会还回来我在实际调试中不止一次遇到这种场景软件工程师写了DMA搬移逻辑源地址是从一个buffer里随手取的偏移结果每次搬运都要比预期慢30%以上。用profiler一查不是burst不够长而是地址没有对齐到128B硬件每次都要多搬一个跨边界的部分或者把burst拆成两段来发起。解决办法其实很简单为DMA分配内存时用对齐分配接口比如对齐到128B或256B并且确保tile的每一维跨距对齐。如果你用的是自研的runtime也可以做一个地址归一化的封装在DMA配置层把源地址、目的地址和长度统一取整。这一步做得好不用改任何计算逻辑带宽利用率能平稳许多。6.2 数据布局NHWC和NCHW不只差一个字母数据布局对DMA效率的影响很大。HBM和DMA都偏爱顺序连续访问可一个feature map的内存布局是NCHW还是NHWC会直接改变卷积滑窗时的访问连续性。NCHW布局下通道维连续跨通道访问很友好但卷积核在空间方向上滑动时每次读到的地址跳跃较大。NHWC布局下空间相邻点的数据在内存里挨着非常适合做行方向上的连续搬运很多深度优化的SDK偏好NHWC正是因为这个原因。你在做数据排布转换layout transform的时候要意识到这不是无用功它是在把DMA的访问模式从低效的分散读变成高效的顺序读。有时你只是在网络输入输出端做了一次transpose后端卷积整体吞吐就高了15%这在项目里很常见。别嫌麻烦该转的layout还是得转。6.3 profiling时怎么看data flow瓶颈最后聊聊怎么用profiling快速定位data flow问题。现在很多NPU的SDK都提供硬件计数器可以统计计算繁忙周期、DMA繁忙周期、流水线stall周期等。拿到这些数之后我一般按三步排查看计算引擎利用率如果超过80%恭喜数据流没有拖后腿如果只有30%-50%继续往下查。看DMA对比计算的繁忙率如果DMA长时间满负荷而计算在等待那就是搬运/计算重叠出了问题优先检查双缓冲深度以及搬运量是否因为tile选小了被放大。看Shared Memory访存延迟把这个计数器掺进来看如果很高去查bank冲突。我把多维访问的stride和bank数一比通常马上就能定位到是不是stride撞了bank。另外还有一个很容易被忽视的点DMA和计算引擎共享同一份HBM带宽。你以为DMA搬运和计算是并行的所以总时间会被覆盖但实际上计算引擎在访问HBM比如某些算子绕过Shared Memory直接读时也会跟DMA抢带宽。所以在profile里看到HBM控制器繁忙率接近100%但计算利用率只有一半这经常不是重叠没做好而是带宽容量本身就不够了。这时候不要盲目加双缓冲深度应该回去调tiling、降低搬运量。我在实际项目里最深的体会是NPU的data flow平时不说话但一旦你压榨性能它就变成了一面镜子把你tiling方案、数据布局、对齐处理上的粗糙之处全部映出来。多数性能问题都不是某个环节坏了而是整个搬运链路的节奏没对齐。把HBM的访问偏好、DMA的搬移粒度、Shared Memory的bank冲突这三件事同时放在脑子里设计算子性能问题往往在第一次写代码时就被规避掉了。相信我这个功夫花得值。
阅读完成 · 觉得有帮助?