☰
NPU数据流架构设计:从HBM到计算单元的完整链路与性能调优
2026/10/19 6:38:11 网站建设 项目流程

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的脾气。HBM(High 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的通道结构与传输模式

DMA(Direct 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/4=32,32%32=0)。如果计算单元需要同时读取这三个地址的数据,就会发生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,输出通道数是512,stride为1,padding为1。HBM带宽假设是1TB/s,DMA启动开销假设是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空间,可以支持更大的输入特征图块。

最后分享一个小技巧:在调试数据流时,我习惯先用一个简化的性能模型估算各级的耗时,然后和实测数据对比。如果差距很大,说明模型中有遗漏的因素,需要进一步排查。这个习惯帮我快速定位了不少问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询