2、数据本地化策略:数据复用原理、Tiling技术详解、数据分块与搬移策略

好,咱们接着聊。上一章讲了存储层次和带宽,说白了都是「硬件给你什么」。但怎么用好这些硬件,才是咱们架构师的本事。这一章,我重点聊聊数据本地化——这是NPU性能优化的核心,没有之一。

你想想看,NPU算力再强,数据搬不过来,全是白搭。我见过太多项目,算力利用率不到30%,问题就出在数据本地化没做好。嗯,咱们今天就把这块掰开揉碎了讲。

2.1 数据复用原理:为什么能省带宽?

数据复用,说白了就是「一份数据,多次使用」。NPU里最常见的是卷积操作——同一个权重,要被成百上千个输入像素共享。如果每次计算都从DDR取数据,带宽早就爆了。

我个人习惯把数据复用分成三类:

  • 时间复用:同一份数据,在不同时间步重复使用。比如权重在多个输入通道间共享。
  • 空间复用:同一份数据,在同一个时间步被多个PE使用。比如输入特征图被多个卷积核共享。
  • 混合复用:时间+空间一起上。这是实际项目中最常用的。

关键指标:数据复用因子

复用因子 = 总计算次数 / 数据加载次数。这个值越高,带宽效率越好。我见过最极致的案例,复用因子做到128倍,带宽需求直接降了两个数量级。

为什么会这样?因为NPU的算力密度越来越高,但DDR带宽的增长速度远跟不上。你算算看,一个1TOPS的NPU,如果数据复用做得差,可能需要100GB/s的带宽;但复用做得好,10GB/s就够了。这差距,就是架构师的价值所在。

2.2 Tiling技术详解:把大问题拆小

Tiling,中文叫「分块」。但我觉得「切蛋糕」更形象——一个大矩阵,你一口吃不下,就得切成小块慢慢嚼。

我在项目中遇到过最典型的场景:一个224x224x64的特征图,加上3x3的卷积核,如果一次性计算,中间结果缓存要几百KB。但NPU的本地SRAM通常只有几十KB到几百KB。怎么办?切!

2.2.1 分块维度

Tiling不是随便切的。你得考虑三个维度:

  • 输入通道分块:把Cin切成多个小块,每次只算一部分通道。
  • 输出通道分块:把Cout切成小块,减少权重加载量。
  • 空间维度分块:把HxW切成小块,控制中间结果大小。

我建议你画个三维图,把这三个维度标出来。每个维度切多少,直接决定了SRAM的利用率。

2.2.2 分块大小计算

这里有个公式,我每次做架构设计都会用:

SRAM需求 = 输入块大小 + 权重块大小 + 输出块大小

输入块 = Tin_h * Tin_w * Cin_tile
权重块 = Kh * Kw * Cin_tile * Cout_tile
输出块 = Tout_h * Tout_w * Cout_tile

嗯,这里要注意:分块不是越小越好。块太小,搬移次数暴增,反而浪费带宽。我见过有人把块切成4x4,结果搬移开销占了总时间的60%。

我的经验法则:分块大小尽量让SRAM利用率达到80%以上。剩下的20%留给double buffer和临时变量。这样既不会溢出,也不会浪费。

2.3 数据分块与搬移策略:怎么搬最划算?

分块方案定好了,接下来就是怎么搬数据。这部分我踩过不少坑,跟大家分享几个关键策略。

2.3.1 Double Buffer:隐藏搬移延迟

说白了就是「一边算,一边搬」。用两块SRAM,一块给计算单元用,另一块同时从DDR加载下一批数据。等计算完了,直接切换。

我曾经在一个项目里,没用double buffer,结果计算单元有40%的时间在等数据。加上double buffer后,利用率直接飙到85%。

// 伪代码示例
buffer_A = load_from_ddr(tile_0)
for i in range(num_tiles):
    buffer_B = load_from_ddr(tile_{i+1})  // 预加载下一块
    compute(buffer_A)                      // 计算当前块
    swap(buffer_A, buffer_B)               // 交换指针

2.3.2 数据搬移粒度

搬数据不是按字节搬的,是按「行」或「块」搬的。我建议:

  • 行搬移:适合空间维度分块较小的情况,每次搬一行数据。
  • 块搬移:适合通道维度分块,一次搬一个完整的通道块。
  • 混合搬移:先搬行,再搬通道。这是最灵活的方式。

避坑指南:我曾经犯过一个错误——搬移粒度太小。每次只搬4个字节,结果DDR的burst效率极低,带宽利用率不到10%。后来改成64字节对齐搬移,带宽利用率到了70%以上。记住:DDR喜欢大块数据,别让它做零碎活。

2.3.3 数据布局优化

数据在DDR里怎么放,直接影响搬移效率。我推荐两种布局:

布局方式 适用场景 优点 缺点
NHWC 空间维度分块为主 空间局部性好,搬移连续 通道维度搬移效率低
NCHW 通道维度分块为主 通道局部性好,适合权重复用 空间搬移需要跨步

我个人习惯用NHWC,因为大多数NPU的卷积操作,空间复用比通道复用更频繁。但如果你做的是depthwise卷积,NCHW可能更合适。具体选哪个,得看你的算子特征。

2.4 实战案例:一个3x3卷积的Tiling设计

咱们拿一个具体例子练练手。假设:

  • 输入特征图:112x112x64
  • 卷积核:3x3x64x128
  • 输出特征图:112x112x128
  • NPU本地SRAM:256KB

第一步,算算一次性计算需要多少SRAM:

输入块:112 * 112 * 64 * 2B = 1.6MB
权重块:3 * 3 * 64 * 128 * 2B = 144KB
输出块:112 * 112 * 128 * 2B = 3.2MB
总计:1.6MB + 144KB + 3.2MB ≈ 5MB

5MB,远超256KB。必须分块。

第二步,确定分块方案。我建议:

  • 空间分块:把112x112切成4x4的小块,每块28x28
  • 输入通道分块:Cin=64,切成4块,每块16通道
  • 输出通道分块:Cout=128,切成4块,每块32通道

第三步,验证SRAM是否够用:

输入块:28 * 28 * 16 * 2B = 25KB
权重块:3 * 3 * 16 * 32 * 2B = 9KB
输出块:28 * 28 * 32 * 2B = 50KB
总计:25KB + 9KB + 50KB = 84KB

84KB,加上double buffer的2倍,168KB。还有88KB的余量,完美。

最终效果:这个Tiling方案,数据复用因子做到了16倍(输入通道复用)x 4倍(输出通道复用)= 64倍。原来需要5MB的SRAM,现在只用168KB。带宽需求从原来的几十GB/s,降到了不到5GB/s。

嗯,这就是数据本地化的威力。你想想看,同样的硬件,不同的Tiling策略,性能可以差出好几倍。做架构设计,说白了就是在「算力」和「带宽」之间找平衡。而数据本地化,就是那个平衡点。

下一章,咱们聊聊更高级的——数据流调度。到时候我会讲怎么用ping-pong buffer和预取机制,把搬移延迟彻底隐藏掉。到时候见。