hgemm_mma_m16n8k16_naive_kernel
hgemm_mma_m16n8k16_naive_kernel详解
仓库链接:xlite-dev/HGEMM at eee72be829545bd6bd115a4b252b5068c9f61597
代码链接:HGEMM/kernels/hgemm/mma/hgemm_mma.cu at eee72be829545bd6bd115a4b252b5068c9f61597 · xlite-dev/HGEMM
1 |
|
1. 什么是 MMA 指令?
MMA (Matrix Multiply-Accumulate) 是 NVIDIA Tensor Core 提供的矩阵乘累加指令,可以在一条指令中完成小规模矩阵乘法。
1.1 m16n8k16 指令规格
1 | mma.sync.aligned.m16n8k16.row.col.f16.f16.f16.f16 |
计算: D[16×8] = A[16×16] × B[16×8] + C[16×8]
1.2 性能对比
| 方式 | 一条指令计算量 | 说明 |
|---|---|---|
| CUDA Core (HFMA) | 1×1×1 = 2 FLOPs | 单个 FMA |
| Tensor Core (m16n8k16) | 16×8×16×2 = 4096 FLOPs | 一个 warp 协作 |
加速比: 4096 / (32 × 2) = 64倍 (理论上)
2. Kernel 概述
1 | template<const int MMA_M=16, const int MMA_N=8, const int MMA_K=16> |
2.1 Launch 配置
1 | dim3 block(WARP_SIZE); // 32 threads = 1 warp |
关键: MMA 指令需要整个 warp (32 threads) 协作执行!
2.2 Shared Memory 布局
1 | __shared__ half s_a[MMA_M][MMA_K]; // 16×16 = 512 bytes |
3. 数据加载 (Global → Shared)
3.1 加载 A 矩阵
1 | // s_a[16][16]: 每行 16 个 half,每线程加载 8 个,需要 2 线程/行 |
图示:
1 | Thread 0,1 → s_a[0][0:7], s_a[0][8:15] (第 0 行) |
3.2 加载 B 矩阵
1 | // s_b[16][8]: 每行 8 个 half,每线程加载 8 个,只需 1 线程/行 |
4. ldmatrix 指令详解 ⭐
ldmatrix 是专为 MMA 设计的数据加载指令,从 Shared Memory 加载数据到寄存器,并自动重排数据布局以匹配 MMA 指令的要求。
4.1 ldmatrix.x4 加载 A
1 | // 每个线程提供一个地址,协作加载 4 个 8×8 矩阵 |
地址映射 (32 个线程的地址):
1 | lane_id | row (lane_id % 16) | col ((lane_id/16)*8) | 地址 |
结果: 每个线程获得 4 个 32-bit 寄存器 (RA[0]~RA[3]),包含 8 个 half 值。
1 | s_a[16][16] 在 Shared Memory 中: |
LDMATRIX_X4 做了什么?
- 加载 4 个 8×8 子矩阵(共 16×16)
- 硬件自动重新分发数据到各线程的寄存器
- 输出:每线程得到 RA[0…3],共 4 个 u32(8 个 half)
4.2 ldmatrix.x2.trans 加载 B (带转置)
1 | uint32_t load_smem_b_ptr = __cvta_generic_to_shared(&s_b[lane_id % 16][0]); |
地址计算: s_b[16][8] 是 B 矩阵的 shared memory
| lane_id | lane_id % 16 (行) | 地址 |
|---|---|---|
| 0~15 | 0~15 | s_b[0~15][0] |
| 16~31 | 0~15 | s_b[0~15][0] (重复) |
图示:
1 | s_b[16][8]: (存储是行主序) |
为什么需要转置?
- B 矩阵在 shared memory 中是行主序
s_b[K][N] - MMA 指令要求 B 是列主序
ldmatrix.trans在加载时自动完成转置
4.3 ldmatrix 数据布局图解(硬件重新分发)
输入地址 和 输出寄存器 的对应关系不是直接的:
1 | ❌ 错误理解:T0 提供的地址 → T0 的寄存器 |
具体过程
Step 1: 每个线程提供一个地址
1 | &s_a[lane_id % 16][(lane_id / 16) * 8] |
合计: 32 × 8 = 256 个 half = 整个 16×16 矩阵
Step 2: 硬件收集所有数据
ldmatrix 指令让硬件一次性从 shared memory 读取全部 256 个 half:
1 | ┌─────────────────────────────────────────┐ |
Step 3: 按 MMA 要求重新分发
关键:MMA 指令对输入数据的排布有特定要求!
关键公式(来自官方文档)
1 | groupID = %laneid >> 2 // lane_id / 4 |
A 矩阵 Fragment(RA[0…3],4 个 u32 = 8 个 half)
官方公式
1 | row = groupID for ai where 0 <= i < 2 || 4 <= i < 6 |
硬件按照 NVIDIA 定义的 fragment 布局 将数据分发到各线程:
每线程持有的 A 矩阵元素:
| lane_id | groupID | threadID | a0,a1 位置 | a2,a3 位置 | a4,a5 位置 | a6,a7 位置 |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | A[0][0:1] | A[8][0:1] | A[0][8:9] | A[8][8:9] |
| 1 | 0 | 1 | A[0][2:3] | A[8][2:3] | A[0][10:11] | A[8][10:11] |
| 2 | 0 | 2 | A[0][4:5] | A[8][4:5] | A[0][12:13] | A[8][12:13] |
| 3 | 0 | 3 | A[0][6:7] | A[8][6:7] | A[0][14:15] | A[8][14:15] |
| 4 | 1 | 0 | A[1][0:1] | A[9][0:1] | A[1][8:9] | A[9][8:9] |
| … | … | … | … | … | … | … |
| 31 | 7 | 3 | A[7][6:7] | A[15][6:7] | A[7][14:15] | A[15][14:15] |
图示:
1 | A[16][16] 矩阵的分布: |
官方图示:

B 矩阵 Fragment(RB[0…1],2 个 u32 = 4 个 half)
官方公式:
1 | row = (threadID_in_group * 2) + (i & 0x1) for bi where i < 2 |
每线程持有的 B 矩阵元素:
| lane_id | groupID | threadID | b0,b1 位置 | b2,b3 位置 |
|---|---|---|---|---|
| 0 | 0 | 0 | B[0:1][0] | B[8:9][0] |
| 1 | 0 | 1 | B[2:3][0] | B[10:11][0] |
| 2 | 0 | 2 | B[4:5][0] | B[12:13][0] |
| 3 | 0 | 3 | B[6:7][0] | B[14:15][0] |
| 4 | 1 | 0 | B[0:1][1] | B[8:9][1] |
| … | … | … | … | … |
官方图示:

C/D 矩阵 Fragment(RC[0…1],2 个 u32 = 4 个 half)
官方公式:
1 | row = groupID for ci where i < 2 |
每线程持有的 C/D 矩阵元素:
| lane_id | groupID | threadID | c0,c1 (RC[0]) | c2,c3 (RC[1]) |
|---|---|---|---|---|
| 0 | 0 | 0 | C[0][0:1] | C[8][0:1] |
| 1 | 0 | 1 | C[0][2:3] | C[8][2:3] |
| 2 | 0 | 2 | C[0][4:5] | C[8][4:5] |
| 3 | 0 | 3 | C[0][6:7] | C[8][6:7] |
| 4 | 1 | 0 | C[1][0:1] | C[9][0:1] |
| … | … | … | … | … |
| 31 | 7 | 3 | C[7][6:7] | C[15][6:7] |
图示:
1 | C[16][8] 矩阵的分布: |
官方图示:

5. MMA 指令执行
1 |
|
5.1 寄存器用量
| 矩阵 | 维度 | 每线程元素数 | 寄存器数 |
|---|---|---|---|
| A | 16×16 | 8 half | 4 × 32-bit |
| B | 16×8 | 4 half | 2 × 32-bit |
| C/D | 16×8 | 4 half | 2 × 32-bit |
整个 Warp:
- A: 32 threads × 8 = 256 half = 16×16 ✓
- B: 32 threads × 4 = 128 half = 16×8 ✓
- C: 32 threads × 4 = 128 half = 16×8 ✓
寄存器分布(每线程持有的数据)
1 | A矩阵 (RA[0~3], 每个 u32 = 2 个 half): |
数据流
1 | ┌──────────────────────────────────────────────────────────┐ |
6. 输出矩阵 C 的数据布局 ⭐
MMA 指令的输出分布在 32 个线程的寄存器中,布局如下:
6.1 官方文档布局
根据 PTX ISA:
1 | C/D Matrix [16][8] 在各线程寄存器中的分布: |

6.2 索引公式
1 | // 每个线程的 RC[0] 对应的位置 |
6.3 存储到 Shared Memory
1 | // RC[0] → s_c 的上半部分 (row 0~7) |
线程分布表
| lane_id | lane_id/4 | lane_id%4 | RC[0] 写入位置 | RC[1] 写入位置 |
|---|---|---|---|---|
| 0 | 0 | 0 | s_c[0][0:1] | s_c[8][0:1] |
| 1 | 0 | 1 | s_c[0][2:3] | s_c[8][2:3] |
| 2 | 0 | 2 | s_c[0][4:5] | s_c[8][4:5] |
| 3 | 0 | 3 | s_c[0][6:7] | s_c[8][6:7] |
| 4 | 1 | 0 | s_c[1][0:1] | s_c[9][0:1] |
| 5 | 1 | 1 | s_c[1][2:3] | s_c[9][2:3] |
| 6 | 1 | 2 | s_c[1][4:5] | s_c[9][4:5] |
| 7 | 1 | 3 | s_c[1][6:7] | s_c[9][6:7] |
| … | … | … | … | … |
| 28 | 7 | 0 | s_c[7][0:1] | s_c[15][0:1] |
| 29 | 7 | 1 | s_c[7][2:3] | s_c[15][2:3] |
| 30 | 7 | 2 | s_c[7][4:5] | s_c[15][4:5] |
| 31 | 7 | 3 | s_c[7][6:7] | s_c[15][6:7] |
图示:s_c[16][8] 的填充
1 | s_c[16][8] 矩阵: |
关键理解
1 | 32 个线程,每线程写 2 次(RC[0] 和 RC[1]) |
与 MMA 输出 Fragment 的对应
这个存储模式完全匹配 MMA m16n8k16 的 C/D fragment 布局:
1 | 官方公式: |
两者完全一致! 代码正确地将 MMA 计算结果从寄存器写回 shared memory。
6.4 s_c → Global Memory C 的存储解释
1 | if (lane_id < MMA_M) { // MMA_M = 16 |
代码含义
| 部分 | 含义 |
|---|---|
lane_id < 16 |
只用前 16 个线程(32 个线程中有 16 个空闲) |
LDST128BITS |
一次写 128 bits = 8 个 half |
s_c[lane_id][0] |
第 lane_id 行,从列 0 开始读 8 个元素 |
图示
1 | s_c[16][8] → C[M][N] 的全局位置 |
线程分工
| lane_id | 负责 | 写入全局地址 |
|---|---|---|
| 0 | s_c[0][0:7] → C[byBM+0][bxBN : bx*BN+8] | 一整行 |
| 1 | s_c[1][0:7] → C[byBM+1][bxBN : bx*BN+8] | 一整行 |
| … | … | … |
| 15 | s_c[15][0:7] → C[byBM+15][bxBN : bx*BN+8] | 一整行 |
| 16~31 | 空闲 | 不参与 |
为什么只用 16 个线程?
s_c是 16×8 矩阵,共 16 行- 每线程写一整行(8 个 half = 128 bits)
- 16 线程刚好覆盖 16 行
- 剩余 16 个线程(lane_id 16~31)被
if过滤掉
效率说明
1 | 优点:每次访存 128 bits,合并访问效率高 |
7. 完整执行流程
1 | ┌─────────────────────────────────────────────────────────────────┐ |
8. K 维度循环
1 | for (int k = 0; k < NUM_K_TILES; ++k) { |
计算量:
- 每次迭代: 16 × 8 × 16 × 2 = 4096 FLOPs
- K/16 次迭代
- 总计: 16 × 8 × K × 2 FLOPs
9. 性能分析
9.1 理论计算量
对于 M×K × K×N 的矩阵乘法:
- Grid: (N/8) × (M/16) blocks
- 每 block: K/16 次 MMA
- 总 MMA 数: (N/8) × (M/16) × (K/16) = M×N×K / 2048
9.2 限制因素
| 因素 | 影响 |
|---|---|
| 只有 1 warp/block | 严重限制 SM 利用率 |
| 无 double buffering | 无法隐藏内存延迟 |
| 小 block size | Grid 巨大,调度开销大 |
这就是为什么需要 Level 2 (多 warp) 和 Level 3 (多阶段) 的优化!
10. 总结
10.1 关键概念
| 概念 | 说明 |
|---|---|
| MMA 指令 | 一条指令完成 16×8×16 矩阵乘累加 |
| Warp 协作 | 32 个线程共同执行一条 MMA |
| ldmatrix | 专用加载指令,自动重排数据 |
| ldmatrix.trans | 加载时转置,适配列主序 B |
| C 布局 | 输出分散在 32 个线程的寄存器中 |
10.2 数据分布
1 | ┌──────────────────┐ |
10.3 下一步
学完 naive kernel 后,继续学习 Level 2: hgemm_mma_m16n8k16_mma2x4_warp4x4_kernel,了解如何组织多个 MMA 操作和多个 Warp 协作。
- 标题: hgemm_mma_m16n8k16_naive_kernel
- 作者: 鱿鱼圈
- 创建于 : 2026-03-02 23:50:00
- 更新于 : 2026-06-05 23:02:25
- 链接: https://yuyanqi.com/2026/03/02/hgemm_mma_m16n8k16_naive_kernel/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。