跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 13 章 算子实战 I:逐元素、归一化与 RoPE

/code/13-operators-elementwise-norm-rope/

05-fusion/README.md2955 字节
# 05 · RMSNorm + RoPE (+ FP8 量化) 的融合

## 目的

本章的 capstone:把 2–3 个算子融进一个 kernel,并说明融合带来的收益与代价。

| kernel | 融合了什么 | 归约次数 | 输出 |
| --- | --- | --- | --- |
| `rms_norm_rope_fused` | RMSNorm + 尾部 RoPE | 1 | `Y` |
| `rms_norm_rope_quant_fused` | RMSNorm + 尾部 RoPE + per-token FP8 | 2 | `Y`、`Q`、`Scale` |

账本上的收益(`../01-ledger/ledger.py` 可复算):

```text
RMSNorm 单独 + RoPE 单独 + 量化单独 :185.6 MB
三者融合成一个 kernel              : 51.4 MB   (3.61×)
```

代价是**寄存器与 UB 压力**:三个算子共享同一块 fragment 和 shared,
`blk_m` 只能取 1,而 `x_f32` 一个 `(1, 4096)` 的 fp32 fragment 就是 16 KiB。
这就是「融合不是免费的」的地方。

## 运行前提

* NVIDIA GPU + CUDA + PyTorch(CUDA)
* TileLang **v0.1.15**
* 输入 fp16/bf16;`Q` 是 `float8_e4m3fn`

## 运行命令

```bash
cd 05-fusion
python3 -m py_compile rmsnorm_rope_quant.py   # 本机可做(无需 GPU)
python3 rmsnorm_rope_quant.py                # 真机:跑两组正确性
```

## 预期输出

**正确性判据**:

* `rms_norm_rope_fused` 与 `torch_reference_fused` 的第一个输出在 `rtol=atol=1e-2`
  内一致(fp16);
* `rms_norm_rope_quant_fused` 的 `Scale` 与参考实现在 `rtol=atol=1e-2` 内一致,
  `Q` 反量化回来(`Q.float() * Scale`)与参考在 `rtol=atol=1e-1` 内一致
  ——fp8 只有 3 位尾数,比较时务必按量化误差口径。

**性能口径**:这个 kernel 的账本下界是「读 x + 读 w + 读 cos/sin + 写 y + 写 q + 写 scale」。
注意写了两份输出(`Y` 与 `Q`),所以访存比只做归一化大。要比较增益,
应该把「N 个算子各自的 bytes 之和」与「融合后一个 kernel 的 bytes」对比。

## 真机验证步骤(按这个顺序做,别跳步)

1. `python3 -m py_compile` 先过语法。
2. **先只编译,不运行**:`kernel = rms_norm_rope_fused(m, n, rope_dim)`。
   这一步最可能失败的是 layout inference——因为它要在同一个 kernel 里同时使用
   fragment 归约、shared 上的跨 lane 配对、以及第二个 fragment 归约。
   报错信息通常直接给出「src_layout / orig_dst_layout」的冲突对象。
3. 打印生成代码,确认三件事:`sq_f32` 那一步有没有被向量化;RoPE 那一步的两条
   store 是不是各自连续;fp8 的 cast 有没有被拆成标量。
4. 用小形状(`M=8, N=256, rope_dim=32`)先跑正确性,再上到目标形状。
5. 最后才谈性能,并且和 `../01-ledger/ledger.py` 算出来的下界比,而不是和另一个
   实现比。

## 已知的边界与备选方案

* **`blk_m = 1` 是默认值,不是最优值**。想加大 `blk_m` 就要么按 K 切行
  (对照 `../03-norm` 的 `rms_norm_splitk`),要么改用 per-thread partial 归约
  (TileOPs 的 RMSNorm 用 `per_thread_partial` 那条分支,见
  `../tilelang-corpus/12-operator-libraries/raw/TileOPs/src/tileops/kernels/norm/rms_norm.py:44-52,72-84`)。
* **第二个归约的写法**:这里把一行 reshape 成 `(n_groups, group_size)` 后用
  `T.reduce_max(..., dim=2)` 降维。如果想手工写 read-modify-write,
  `loop_vectorize` 会因为「store 的索引不随最内层循环推进」而把这一处标量化的
  (`_vendor/tilelang-v0.1.15/src/transform/loop_vectorize.cc:1050-1060`)。
* **RoPE 那一步为什么要回到 shared**:NeoX 约定要求第 `p` 个和第 `p + H` 个元素
  同时可见,它们在 fragment 里可能属于不同线程,寄存器之间不能互相访问。
* **想同时输出 Y 与 Q**:如果下游只用 fp8,可以删掉 `Y` 以减少一次整行写回。
  这里保留 `Y` 是为了能直接和参考实现对比数值。

## 与上游示例的关系

* 整体结构(RMSNorm → 尾部 RoPE → 写回)对照适配器仓库
  `_vendor/tilelang-ascend-v0.1.2.000/examples/pos_embedding/rms_rope_fused.py`。
* 分组量化的写法与 dtype 处理对照
  `_vendor/tilelang-v0.1.15/examples/cast/example_per_token_cast_to_fp8.py:8-41`
  与 `_vendor/tilelang-v0.1.15/examples/ascend/example_per_token_cast_to_fp8.py:36-86`。

## 作者验证级别

**仅静态核对**:`python3 -m py_compile` 通过;用到的 `T.*` 名字经
`../07-api-audit/api_audit.py` 对照过 `_vendor/tilelang-v0.1.15/` 的语言面。
**未在 GPU 上编译或运行过**:上面的「真机验证步骤」是给读者的路线图,
不是作者已经执行过的记录。

← 浏览本章全部代码