GroupedMatmulV4 A5 MXFP8 调度源码导读
本文聚焦
aclnnGroupedMatmulV4在 A5/DAV_3510 上的 MXFP8 主路径:
x/weight=FLOAT8_E4M3FN、scale/perTokenScale=FLOAT8_E8M0、weight 为 ND,
transA=false、transB=false。其他 format、转置方式和量化模式只在必要时用于对比。
1. 先看全局
这条路径可以分为三层:
- Host 侧识别 A5 MXFP8 场景,计算
baseM/baseN/baseK、L1 流水和 tiling key; - Kernel 侧遍历 group,将每个 group 的 M/N 输出 tile 分配给多个 AI Core;
- 每个 AI Core 对自己的 M/N tile 遍历完整 K,通过 L1、L0A/L0B 分块和多次 MMAD 在 L0C 上累加。
主要调用链:
1 | aclnnGroupedMatmulV4GetWorkspaceSize |
关键入口:
| 层级 | 代码 | 职责 |
|---|---|---|
| V4 API | aclnn_grouped_matmul.cpp |
参数校验、构造 executor |
| Host 总入口 | TilingGMM |
架构和 dtype 分流 |
| MX tiling | GroupedQmmBasicApiTiling |
计算 MX Basic API tiling data |
| Kernel 总入口 | grouped_matmul |
按编译宏和 tiling key 选择模板 |
| Kernel 调度 | KernelQGmmMx |
group 遍历和 MN tile 分核 |
| Cube 计算 | BlockMmadMx |
GM/L1/L0 搬运、MX scale、MMAD、写回 |
V4 只影响 ACLNN 入口的校验和 executor 构造。进入 l0op::GroupedMatmul
后,V4 与其他版本共用同一套 GroupedMatmul tiling 和 kernel。
严格说 ACLNN 是两阶段接口:
aclnnGroupedMatmulV4GetWorkspaceSize
完成校验、建图并生成 executor,
aclnnGroupedMatmulV4
再用 workspace 和 executor 发起执行。Host tiling 属于这套 executor/算子执行链路,
不是 aclnnGroupedMatmulV4 函数体里的一次普通 C++ 直接调用。
2. 文中使用的例子
以典型 MoE split-M 场景为例:
1 | groupNum = 4 |
groupList 是累积值,因此四组的 M 为:
1 | M0 = 10 - 0 = 10 |
数学上是四个 grouped GEMM problem:
1 | Group 0: X0[10,4096] @ W0[4096,512] -> Y0[10,512] |
groupNum 可以类比 batch size,但它更准确的含义是“grouped GEMM problem 的数量”。
它不是普通 BMM 中每组 shape 都固定的 batch 维;例如 split-M 时每个
expert 的 M_i 可以不同。
3. Host 侧:如何选中 A5 MXFP8 tiling
3.1 TilingGMM 进入 A5 量化模板
TilingGMM 先取出 x/weight
第一个动态输入的 descriptor,得到 dtype。在 DAV_3510 上,两个输入都是
1 Byte 类型时归入全量化路径:
xDesc 就是 x[0] 的 tensor descriptor,保存 dtype、format、shape 等
Host 侧元数据;它不是 x 的 GM 数据地址。这里只通过
xDesc->GetDataType() 用它做模板分流。
1 | if (compileInfoPtr->npuArch == NpuArch::DAV_3510) { |
{0,1} 对应的注册在
grouped_quant_matmul_tiling.cpp:
1 | REGISTER_OPS_TILING_TEMPLATE(GroupedMatmul, GroupedQmmBasicApiTiling, 0); |
注册表按优先级尝试模板。scale=FLOAT8_E8M0 时,
GroupedQmmBasicApiTiling::IsCapable
识别到 MXFP8,模板 0 返回成功,不再尝试通用模板 1。
3.2 tiling 框架的执行顺序
GroupedQmmBasicApiTiling 继承自 TilingBaseClass。实际流程由
TilingBaseClass::DoTiling 组织:
1 | GetShapeAttrsInfo |
各阶段的职责:
| 阶段 | 主要工作 |
|---|---|
GetShapeAttrsInfo |
解析 dtype、format、transpose、groupType、groupList 和 M/N/K |
GetPlatformInfo |
读取 AIC 核数以及 L1/L0A/L0B/L0C 容量 |
IsCapable |
判断当前模板是否支持这组输入 |
DoOpTiling |
写入 group、量化模式、bias 等算子级参数 |
DoLibApiTiling |
计算 Cube 基础块和 L1 流水,是主要 tiling 逻辑 |
PostTiling |
将结果序列化到 kernel 可读的 raw tiling buffer |
GetTilingKey |
根据 transB/transA/kernelType 选择 kernel 编译分支 |
GetShapeAttrsInfo 的公共实现依次调用:
AnalyzeDtype:解析 X/W/Y/scale 的 dtype 和 format;AnalyzeAttrs:解析 transpose、groupType 等属性;AnalyzeInputs:计算 groupNum、M/N/K,识别量化模式并校验 scale shape。
scaleDtype == FLOAT8_E8M0 使
SetQuantMode 设置:
1 | aQuantMode = MX_PERGROUP_MODE |
3.3 基础块:baseM/baseN/baseK
MX Basic API 的主 tiling 函数是
GroupedQmmBasicApiTiling::DoLibApiTiling:
1 | GroupedQmmTiling::CalBasicBlock(); |
CalBasicBlock 计算:
1 | A 基础块: [baseM, baseK] |
主路径中 baseM/baseN 上限为 256,再根据 dtype、format 和 Cube 要求对齐。
baseK 先按 dtype 和 128 Byte 级别的基础块选择,MX 模式再要求对齐到 64:
1 | if (bQuantMode == MX_PERGROUP_MODE) { |
3.4 L1 流水:kAL1/kBL1/scaleKAL1
CalL1Tiling
在已知 baseM/baseN/baseK 后,根据 L1 容量计算 A/B 和 scale 的流水深度:
1 | CalL1Tiling |
中间参数的关系:
1 | kAL1 = stepKa * baseK |
baseK:一次 L1 → L0 并执行 MMAD 的 K 基础粒度;kAL1/kBL1:A/B 一套 L1 buffer 一次覆盖的 K 元素数;scaleKAL1:一次搬入 L1 的 MX scale 所覆盖的 K 范围。
dbL0C 由两个 FP32 C tile 是否能同时容纳在 L0C 中决定:
1 | dbL0c = baseM * baseN * sizeof(float) * 2 <= l0cSize ? 2 : 1; |
对应实现在
InitCommonL1TilingFields。
3.5 Host 与 kernel 的交界
MX Basic API 最终写入的核心字段:
1 | GMMQuantParams: |
结构定义在
grouped_matmul_tiling_data_apt.h。
PostTiling
将整个结构拷贝到 context 的 raw tiling buffer,kernel 入口再从 GM_ADDR tiling
解出它。
4. Kernel 入口:从 tiling key 到 MX 模板
grouped_matmul_apt.cpp 中的
grouped_matmul 是 A5 kernel 入口。对双 FP8 输入,
grouped_matmul_utils.h 定义:
1 | V310_GMM_QUANT |
V310 是代码中的历史命名,这个分支实际用于当前 A5/arch35 实现。
ND、A/B 非转置时命中
grouped_matmul_apt.cpp 的分支:
1 | GMM_QUANT_MX_BASIC_API_IMPL_CLASS( |
该宏解出 tiling data,调用
GmmCgmctMxKernel。
后者将三个核心部件组装起来:
1 | using BlockScheduler = GroupedMatmulAswtWithTailSplitScheduler; |
职责边界:
KernelQGmmMx:当前是哪个 group,这个 Core 负责哪些 M/N tile;BlockMmadMx:单个 M/N tile 如何遍历 K,如何搬运并调用 MMAD。
5. group 调度:groupNum_ 与 groupList
KernelQGmmMx::Run 遍历所有 group:
1 | for (uint32_t loopIdx = 0; loopIdx < groupNum_ - 1; ++loopIdx) { |
GetSplitValueFromGroupList
将 cumsum 或 count 形式的 groupList 转成当前组的长度。在 split-M 中,
SetMNK 只更新 M:
1 | problemShape_.M = splitValue; |
K 和 N 对所有 group 保持不变。空 group 会被跳过。
每个 AI Core 都执行同样的 group 循环,但 scheduler 根据本核的
blockIdx 判断这个 group 中哪些 tile 归它。这不是把 group 直接静态分给某一个核。
6. M/N 分核:Scheduler 到底在拆什么
6.1 输出平面的 tile 数
BlockSchedulerGmmAswtWithTailSplit::UpdateNextProblem
为当前 group 计算:
1 | mCnt_ = CeilDiv(M, baseM); |
这是判断“当前路径没有跨 Core split-K”的最直接证据:调度任务数只有
mCnt*nCnt,没有再乘 kCnt,调度坐标中也没有 kTileIdx。
6.2 blockIdx + roundIdx * blockNum
Scheduler 中:
1 | blockNum_ = AscendC::GetBlockNum(); |
GetTileIdx
将本核第 roundIdx_ 轮的逻辑任务号计算为:
1 | index = blockIdx_ + roundIdx_ * blockNum_; |
再将 index 映射为 (mTileIdx,nTileIdx)。实际实现还有 window 和 N 方向蛇形遍历,
用于减少同地址冲突并改善 cache 局部性,但任务唯一性来自上面的确定性映射。
roundIdx_ 是 scheduler 对象的普通成员,而 scheduler 对象在每个 AI Core 上局部创建。
所以每个 Core 有自己的 roundIdx_,不共享、不需要 atomic 或跨核同步。
例如 4 个 Core、12 个逻辑 tile,忽略蛇形映射后可简化为:
1 | Core 0: index 0, 4, 8 |
6.3 ProcessSingleGroup 的 do-while
ProcessSingleGroup 的结构:
1 | if (!bs.GetTileIdx(tileIdx)) { |
因此这个 do-while 既不是 group 循环,也不是 K 循环,而是:
当前 AI Core 在当前 group 中被分配的所有 M/N 输出 tile 的循环。
GetBlockShape 会返回尾块的实际 M/N 大小。GetQuantOffset 结合 tile index
和 tail 子块偏移,得到 A、B、Y、scale 和 bias 的地址偏移。
6.4 最后一组的 tail split
如果最后一组 tile 数较少,大量 Core 空闲,
UpdateTailTile
可将尾部 M/N tile 进一步分成子块。拆分维度仍然是 M/N,不是 K。
7. 一个 Core 内:为什么整个 K 都在同一个 Cube 上
KernelQGmmMx::Iterate
将 scheduler 分配的 M/N 大小和完整 K 一起传入 BlockMmadMx:
1 | BlockShape blockShape{ |
所以一次 mmadOp_ 逻辑上负责:
1 | A[singleCoreM, K] @ B[K, singleCoreN] |
K 不跨 Core 分,但会在这个 Core 内部分为三个层次:
1 | 完整 K |
7.1 L1 循环
BlockMmadMx 根据 kAL1 >= kBL1 选择 A 或 B 作为外层复用对象。
例如 IterAL1BL1:
1 | for (kOuter = 0; kOuter < K; kOuter += kAL1) { |
反向复用的实现是
IterBL1AL1。
7.2 L0/MMAD 循环
BlockMmadMx::Iterate
将当前 L1 K 范围继续按 baseK 拆分:
1 | for (kL0Offset = kL1Offset; |
每次计算:
1 | L0A[baseM, baseK] @ L0B[baseK, baseN] |
cmatrixInitVal 和 unitFlag 控制完整 K 的累加语义:
1 | mmadParams.unitFlag = |
- 第一个 K 块初始化 L0C,或在有 bias 时从 bias 初始化;
- 中间 K 块继续累加;
- 最后一个 K 块标记完整 C tile 完成。
代码中 mmadParams.k = Align64(curKL0) 的
Align64
定义在 CGMCT 公共工具头文件中,作用是将 K 向上对齐到 64;
它是 C++ inline 辅助函数,不是额外的调度层次。
当前 MX kernel 中没有 kTileIdx、partial-sum workspace、atomic add 或跨核
reduction,因此不是并行 split-K。
7.3 groupType=2/SPLIT_K 不等于并行 split-K
groupType=2 时,groupList 描述分组轴上的 K 长度,
SetMNK 更新当前 group 的 K。
但每个 group 的结果写入独立的 groupIdx*M*N 输出区域,并没有将多个
Core 对同一 C tile 的 K partial sum 合并起来。
8. 片上存储和 MX scale 数据流
一个 M/N tile 的主要数据路径:
1 | X/W/scale in GM |
8.1 MatmulWithScale 只是模板策略
MatmulWithScale 是本仓库定义的
compile-time policy:
1 | template <class SingleCoreShape, uint64_t FULL_LOAD_MODE_ = 0> |
它用于选中 BlockMmadMx 的模板特化,自身不执行计算,也不是硬件指令。
8.2 scale 在哪里进入计算
scale 先通过
CopyInScaleA 和
CopyInScaleB 从 GM 搬到 L1。
真正将 scale 与数据一起送入 MX 计算流水的是:
1 | AscendC::LoadData( |
以及 B 侧对应调用,位置分别是:
AscendC::LoadData 是 AscendC 硬件抽象/intrinsic 接口。在这个 overload 中,
L1 中的 FP8 数据、E8M0 scale 和 LoadData2DMxParams 同时传入。后续
Mmad 不再显式接收 scale。
这条 MXFP8 路径不会先在 GM 生成一份完整 FP16/FP32 反量化 tensor;
scale/dequant 语义融合在 MX LoadData/MMAD 流水中。最后的 CopyOut 主要负责
L0C FP32 到 FP16/BF16/FP32 输出的 FixPipe 转换。
9. Ping-pong 分别发生在哪一层
9.1 L0A/L0B:相邻 baseK 块之间
在核内 K 循环中:
1 | l0Offset = HALF_L0_SIZE * (l0PingPong_ & 1); |
所以相邻 baseK 块交替使用 L0A/L0B Ping 和 Pong,用来重叠下一个 K 块的
MTE1 LoadData 与当前 K 块的 Cube MMAD。
9.2 L0C:相邻 M/N 输出 tile 之间
RunMmadLoop 在完整 K 循环开始前选择:
1 | l0cOffset = (l0cPingPong_ & 1) * HALF_L0C_SIZE; |
这个 l0cOffset 在当前 M/N tile 的整个 K 累加过程中不变。完成后:
1 | CopyOut(cGlobal, c1Local_[l0cOffset], ...); |
下一个 M/N tile 才切换 L0C 的另一半,从而可以重叠:
1 | 前一个 tile: FixPipe 正在从 L0C Ping 写回 |
如果某个 Core 在当前 kernel 中只分到一个 M/N tile,它只会使用 L0C Ping,
Pong 没有机会被使用,这与 tiling 允许 dbL0C=2 并不矛盾。
10. 用一个完整例子串起来
将第 2 节的数据简化到 Group 1:
1 | M = 20 |
10.1 M/N 分核
1 | mCnt = ceil(20 / 128) = 1 |
该 group 只有两个输出 tile:
1 | Tile 0: Y[0:20, 0:256] |
若两个 Core 参与:
1 | Core A: X[20,4096] @ W[4096,256] -> Tile 0 |
每个 Core 负责各自 N 区间的完整 K。
10.2 一个 Core 内的 K 层次
1 | 完整 K = 4096 |
从 MMAD 粒度看,当前 Core 需要:
1 | 4096 / 128 = 32 次 baseK MMAD |
简化时间线:
1 | K[0:128] |
如果同一 Core 还有下一个 M/N tile,下一个 tile 的 32 次 K MMAD 会全部累加到
L0C Pong,然后再切回 Ping。
11. 源码阅读地图
建议按下列顺序阅读:
TilingGMM:先看 A5 量化分流;GroupedQmmBasicApiTiling::DoLibApiTiling:看主 tiling 调度;CalBasicBlock:看baseM/N/K;GroupedQmmBasicApiTiling::CalL1Tiling:看 L1 深度和 scale 覆盖;grouped_matmul:看 kernel 编译分支;GmmCgmctMxKernel:看组件组装;KernelQGmmMx::Run:看 group 循环;ProcessSingleGroup:看每核 MN tile 循环;BlockSchedulerGmmAswtWithTailSplit:看 tile 与 Core 映射;BlockMmadMx::RunMmadLoop:看一个 MN tile 的完整 K;IterAL1BL1和Iterate:看 L1/baseK 循环;CopyInL0A、CopyInL0B和Mmad:看 MX scale 和真正计算。
12. 容易混淆的概念
| 容易误解的说法 | 更准确的理解 |
|---|---|
groupNum 就是 batch size |
它是 grouped GEMM 数量,可类比 batch,但每组分组维度可不同 |
| 一个 group 只在一个 Cube 上算 | 一个 group 的 M/N 输出平面会分成多个 tile,分给多个 Core/Cube |
baseK 代表跨 Core 拆 K |
baseK 是同一 Core 内 L1 → L0/MMAD 的 K 粒度 |
groupType=SPLIT_K 就是并行 split-K |
它是 groupList 沿 K 的功能分组;当前 MX 路径没有跨 Core partial-sum reduction |
roundIdx_ 是所有 Core 共享的计数器 |
它是每个 Core 的 scheduler 局部状态,无需跨核同步 |
| L0C ping-pong 用于同一 tile 的 K 分块 | 同一 tile 的所有 K 分块必须累加到同一 L0C;L0C 在相邻 MN tile 间切换 |
MatmulWithScale 是一条指令 |
它是仓库定义的模板策略;硬件抽象接口是带 MX 参数的 AscendC::LoadData |
| FP8 输入会先整体转成 FP16 | 当前路径不生成完整高精度中间 tensor,scale/dequant 融合在 MX 计算流水中 |
最后可以用一句话概括这条调度路径:
Host 选定 M/N/K 基础块和片上流水;Kernel 按 group 遍历问题,按 M/N tile 将输出平面分给多个 Cube;每个 Cube 在核内遍历自己 tile 的完整 K,并通过 L1/L0 分块、MX scale LoadData、MMAD 和 L0C 累加完成计算。