先给结论:这里没有统一的“切到多少就不会落HBM”。容量约束决定的是微块的最大尺寸;指令粒度、对齐、带宽利用率又决定它不能太小。
对当前910B/arch22和MegaMoE,可以看到三种不同的实际微块:
Dispatch内部搬运块:8 KiB GMM Cube微块: 128 × 256 × 64 SwiGLU Vector微块: 19 × 512
它们分别由ACL SHMEM预留UB、L0A/B/C容量、SwiGLU活跃Buffer数量决定。
本仓CANN依赖中的arch22平台信息是:
当前CANN平台测试信息可见 test_foreach_sigmoid_tiling.cpp。
不过需要注意:
GetCoreMemSize
对齐约束是:
UB: 32 Byte L1: 32 Byte L0A/L0B: 512 Byte L0C: 64 Byte
GM单次搬运通常达到16 KiB以上才更容易发挥带宽,内轴最好按128/256/512 Byte对齐。Ascend C硬件约束
当前:
baseM = 128 baseN = 256 baseK = 64 输入BF16/FP16:2 Byte L0C累加FP32: 4 Byte
一个输出累加微块占用:
[ 128 \times 256 \times 4 =131072\ \text{Bytes} =128\ \text{KiB} ]
正好占满L0C:
C tile = 128×256 FP32 = 128 KiB L0C容量 = 128 KiB
所以当前无法对L0C进行双缓冲。CANN的MatMul tiling也明确通过:
baseM * baseN * 4 * dbL0C <= l0CSize
判断能否双缓冲,见 batch_mat_mul_v3_base_tiling.cpp。
双缓冲A:
[ 2 \times 128 \times 64 \times 2 =32768\ \text{Bytes} =32\ \text{KiB} ]
低于64 KiB的L0A容量。
双缓冲B:
[ 2 \times 64 \times 256 \times 2 =65536\ \text{Bytes} =64\ \text{KiB} ]
正好占满L0B。
因此这组参数非常典型:
A双缓冲:32 KiB / 64 KiB B双缓冲:64 KiB / 64 KiB C单缓冲:128 KiB / 128 KiB
对于:
[10000,7168] × [7168,4096]
一个Cube微块实际上是:
A:128 × 64 B:64 × 256 C:128 × 256
K方向:
7168 / 64 = 112轮
这112轮的部分和一直累加在同一块L0C中:
K0=0..63 → 累加到L0C K1=64..127 → 继续累加 ... K111 → 得到完整128×256结果
中间112次K分块不会把部分和写回GM。这就是“避免中间结果落HBM”真正发生的位置。
但是K全部规约完以后,128×256结果仍然会通过FixPipe写到GM。
128×256
外层SwiGLU Task是:
128 token × 4096输入 → 拆成左右两个128×2048
但当前生成的 swiglu_tiling.bin 显示:
swiglu_tiling.bin
rowLen = 128 colLen = 2048 baseRowLen = 19 baseColLen = 512 doubleBuffer = 1
也就是实际Vector微块:
19 token × 512对SwiGLU元素
一个128-token Task内部大约会拆成:
[ \lceil128/19\rceil \times \lceil2048/512\rceil =7\times4 =28 ]
个Vector微块。
为什么是 19×512?BF16 SwiGLU双缓冲时,每个配对元素需要:
19×512
3组BF16队列 × 2 Byte × 双缓冲 = 12 Byte 2组FP32临时Buffer × 4 Byte = 8 Byte 总计 = 20 Byte/元素
那么:
[ 192\text{ KiB}/20 =9830\text{个元素左右} ]
实际选择:
[ 19\times512=9728 ]
占用:
[ 9728\times20 =194560\text{ Bytes} =190\text{ KiB} ]
剩余约2 KiB处理对齐和其他开销。
对应的UB计算逻辑见 swi_glu_tiling.cpp。
所以:
128 token:调度Task粒度 19×512: 真正装入UB的Vector计算微块
虽然物理UB是192 KiB,但ACL SHMEM默认只给MTE通信预留:
DEFAULT_MTE_UB_SIZE = 16 * 1024;
见 shmem_init.cpp。
Dispatch使用ping-pong,因此每块:
16 KiB / 2 = 8 KiB
对于一个128-token、hidden=7168、BF16的Task:
总数据 = 128 × 7168 × 2 = 1,835,008 Byte = 1.75 MiB
内部搬运次数:
[ 1835008/8192=224 ]
也就是:
一个128-token Dispatch Task ↓ 224个8 KiB GM→UB→远端GM搬运块 ↓ 全部完成后发一次signal
这里的8 KiB只是通信暂存块,不是新的调度Task。
例如GMM的112个K分块:
A/B:GM→L1→L0A/L0B C部分和:始终留在L0C K归约完成后:L0C→GM
这里确实避免了每个K分块都写HBM。
满足条件主要是:
[ db_A M_bK_bs_A \le L0A ]
[ db_B K_bN_bs_B \le L0B ]
[ db_C M_bN_bs_C \le L0C ]
910B/arch22是AIC、AIV分离架构,官方架构描述中两者通过Global Memory交换数据。NPU架构版本220x
所以当前:
GMM: L0C → FixPipe → GM SwiGLU: GM → UB → Vector
当前GMM和SwiGLU之间并没有把结果直接从L0C交给AIV的UB。严格从编程模型看,中间结果已经写入GM。
但是“写入GM地址”不一定意味着每个字节都立即到物理HBM,因为L2可能命中:
AIC写GM → L2 AIV很快读GM → L2命中
问题是L2只有32 MiB,而且是共享、硬件管理的,无法仅凭tile尺寸保证驻留。
例如一个专家:
10000 × 4096 × 2 Byte = 78.125 MiB
已经远大于32 MiB L2。如果当前必须等整个专家GMM完成后再启动SwiGLU,那么前面的输出大概率无法全部留在L2。
如果改成128-token粒度及时交给SwiGLU:
128 × 4096 × 2 Byte = 1 MiB
这1 MiB切片更可能在被驱逐前由AIV消费,从而减少真实HBM流量。但仍然是“GM地址上的L2复用”,不是严格的L0C→UB片上直连。
选取内部计算块时需要同时满足:
因此所谓“最小块”其实是两个边界之间的折中:
块不能太大:否则装不进片上Buffer 块不能太小:否则搬运、指令和调度效率下降
对当前MegaMoE最直观的三层答案就是:
调度层:Dispatch/SwiGLU按128 token生成Task 算子层:SwiGLU继续切成19×512;Dispatch继续切成8 KiB Cube层:GMM按128×256×64计算,K部分和留在L0C 跨AIC/AIV:仍需经过GM,主要争取L2命中,并非严格不落GM/HBM
先给结论:这里没有统一的“切到多少就不会落HBM”。容量约束决定的是微块的最大尺寸;指令粒度、对齐、带宽利用率又决定它不能太小。
对当前910B/arch22和MegaMoE,可以看到三种不同的实际微块:
它们分别由ACL SHMEM预留UB、L0A/B/C容量、SwiGLU活跃Buffer数量决定。
910B/arch22片上存储容量
本仓CANN依赖中的arch22平台信息是:
当前CANN平台测试信息可见 test_foreach_sigmoid_tiling.cpp。
不过需要注意:
GetCoreMemSize获取目标设备实际容量,不要跨产品硬编码。Ascend C架构说明对齐约束是:
GM单次搬运通常达到16 KiB以上才更容易发挥带宽,内轴最好按128/256/512 Byte对齐。Ascend C硬件约束
为什么GMM是128×256×64
当前:
L0C约束
一个输出累加微块占用:
[
128 \times 256 \times 4
=131072\ \text{Bytes}
=128\ \text{KiB}
]
正好占满L0C:
所以当前无法对L0C进行双缓冲。CANN的MatMul tiling也明确通过:
baseM * baseN * 4 * dbL0C <= l0CSize判断能否双缓冲,见 batch_mat_mul_v3_base_tiling.cpp。
L0A约束
双缓冲A:
[
2 \times 128 \times 64 \times 2
=32768\ \text{Bytes}
=32\ \text{KiB}
]
低于64 KiB的L0A容量。
L0B约束
双缓冲B:
[
2 \times 64 \times 256 \times 2
=65536\ \text{Bytes}
=64\ \text{KiB}
]
正好占满L0B。
因此这组参数非常典型:
对于:
一个Cube微块实际上是:
K方向:
这112轮的部分和一直累加在同一块L0C中:
中间112次K分块不会把部分和写回GM。这就是“避免中间结果落HBM”真正发生的位置。
但是K全部规约完以后,
128×256结果仍然会通过FixPipe写到GM。SwiGLU实际不是128个Token一次装进UB
外层SwiGLU Task是:
但当前生成的
swiglu_tiling.bin显示:也就是实际Vector微块:
一个128-token Task内部大约会拆成:
[
\lceil128/19\rceil \times \lceil2048/512\rceil
=7\times4
=28
]
个Vector微块。
为什么是
19×512?BF16 SwiGLU双缓冲时,每个配对元素需要:那么:
[
192\text{ KiB}/20
=9830\text{个元素左右}
]
实际选择:
[
19\times512=9728
]
占用:
[
9728\times20
=194560\text{ Bytes}
=190\text{ KiB}
]
剩余约2 KiB处理对齐和其他开销。
对应的UB计算逻辑见 swi_glu_tiling.cpp。
所以:
Dispatch当前只使用16 KiB通信UB
虽然物理UB是192 KiB,但ACL SHMEM默认只给MTE通信预留:
DEFAULT_MTE_UB_SIZE = 16 * 1024;见 shmem_init.cpp。
Dispatch使用ping-pong,因此每块:
对于一个128-token、hidden=7168、BF16的Task:
内部搬运次数:
[
1835008/8192=224
]
也就是:
这里的8 KiB只是通信暂存块,不是新的调度Task。
“不落HBM”要区分两种情况
同一计算核内部
例如GMM的112个K分块:
这里确实避免了每个K分块都写HBM。
满足条件主要是:
[
db_A M_bK_bs_A \le L0A
]
[
db_B K_bN_bs_B \le L0B
]
[
db_C M_bN_bs_C \le L0C
]
AIC到AIV之间
910B/arch22是AIC、AIV分离架构,官方架构描述中两者通过Global Memory交换数据。NPU架构版本220x
所以当前:
当前GMM和SwiGLU之间并没有把结果直接从L0C交给AIV的UB。严格从编程模型看,中间结果已经写入GM。
但是“写入GM地址”不一定意味着每个字节都立即到物理HBM,因为L2可能命中:
问题是L2只有32 MiB,而且是共享、硬件管理的,无法仅凭tile尺寸保证驻留。
例如一个专家:
已经远大于32 MiB L2。如果当前必须等整个专家GMM完成后再启动SwiGLU,那么前面的输出大概率无法全部留在L2。
如果改成128-token粒度及时交给SwiGLU:
这1 MiB切片更可能在被驱逐前由AIV消费,从而减少真实HBM流量。但仍然是“GM地址上的L2复用”,不是严格的L0C→UB片上直连。
最终受什么限制
选取内部计算块时需要同时满足:
因此所谓“最小块”其实是两个边界之间的折中:
对当前MegaMoE最直观的三层答案就是: