跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 17 章 端到端 I:用 HuggingFace 资产从零训练一个 7B 模型

/code/17-e2e-training-7b/

03-tilelang-norm-torch/rmsnorm_tl.py3916 字节
"""把 Transformer 里的 RMSNorm 换成 TileLang kernel,并接回 PyTorch autograd。

这一段是第 17 章 §7「把热点算子换成 TileLang」的可读版本,回答三个问题:

1. TileLang kernel 怎么写(前向);
2. 怎么让 `kernel(x, w)` 直接吃 `torch.Tensor`、并把输出当 `torch.Tensor` 拿回来;
3. 反向从哪里来——TileLang 不提供 autograd,所以必须由你显式接上。

API 与锁定版本 `_vendor/tilelang-v0.1.15/` 对齐:
`@tilelang.jit(out_idx=[...])`、`T.prim_func`、`T.copy`、`T.Parallel`、
`T.reduce_sum`、`T.rsqrt`、`T.alloc_fragment`。见 references.md R7–R11。

运行前提:本机需要 NVIDIA GPU(或昇腾 NPU,见同目录 README 的昇腾分支)与可用的
`tilelang`。**下面这段代码在写作机器上只做过静态核对,没有运行过**——写作机器没有
GPU/NPU。真实运行请在带卡的机器上执行同目录 `compare_ref.py`。
"""

from __future__ import annotations

import torch
import tilelang
import tilelang.language as T


@tilelang.jit(out_idx=[2])  # 第 3 个参数(Y)是输出,由 adapter 分配并返回
def _rmsnorm_fwd(M: int, N: int, eps: float, blk_m: int = 1, threads: int = 128,
                 dtype: str = "bfloat16"):
    accum = "float"

    @T.prim_func
    def main(
        X: T.Tensor((M, N), dtype),
        W: T.Tensor((N,), dtype),
        Y: T.Tensor((M, N), dtype),
    ):
        with T.Kernel(T.ceildiv(M, blk_m), threads=threads) as bx:
            x_local = T.alloc_fragment((blk_m, N), accum)
            sq_local = T.alloc_fragment((blk_m, N), accum)
            sum_sq = T.alloc_fragment((blk_m,), accum)
            rstd = T.alloc_fragment((blk_m,), accum)

            T.copy(X[bx * blk_m, 0], x_local)
            for i, j in T.Parallel(blk_m, N):
                sq_local[i, j] = x_local[i, j] * x_local[i, j]
            T.reduce_sum(sq_local, sum_sq, dim=1)
            for i in T.Parallel(blk_m):
                rstd[i] = T.rsqrt(sum_sq[i] / N + eps)
            for i, j in T.Parallel(blk_m, N):
                x_local[i, j] = x_local[i, j] * rstd[i] * T.Cast(accum, W[j])
            T.copy(x_local, Y[bx * blk_m, 0])

    return main


def rmsnorm_ref(x: torch.Tensor, w: torch.Tensor, eps: float) -> torch.Tensor:
    """纯 PyTorch 参考实现,用来对齐数值。"""

    rstd = torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + eps)
    return (x * rstd) * w


class RMSNormTileLang(torch.autograd.Function):
    """把前向 kernel 和反向(这里先用 PyTorch 算子)拼成一个 autograd.Function。

    TileLang 只产出**前向**的 device 函数;`torch.autograd` 不知道它,也不会为它
    建图。要让 `loss.backward()` 穿过这个 kernel,只有一条路:把 kernel 包进
    `torch.autograd.Function`,在 `forward` 里调用 kernel,在 `backward` 里自己
    写(或再调一个 TileLang kernel)反向。
    """

    @staticmethod
    def forward(ctx, x: torch.Tensor, w: torch.Tensor, eps: float) -> torch.Tensor:
        # x: (M, N) bf16, w: (N,) bf16;M 必须是 blk_m 的整数倍(这里取 1)
        x = x.contiguous()
        kernel = _rmsnorm_fwd(x.shape[0], x.shape[1], eps)
        y = kernel(x, w)  # 直接吃 torch.Tensor,返回 torch.Tensor
        ctx.save_for_backward(x, w)
        ctx.eps = eps
        return y

    @staticmethod
    def backward(ctx, grad_y: torch.Tensor):
        x, w = ctx.saved_tensors
        eps = ctx.eps
        x32 = x.float()
        w32 = w.float()
        rstd = torch.rsqrt(x32.pow(2).mean(-1, keepdim=True) + eps)
        xn = x32 * rstd
        grad_w = (grad_y.float() * xn).sum(dim=0).to(w.dtype)
        # dx 的标准形式:dx = rstd * (g*w - xn * mean(g*w*xn))
        g = grad_y.float() * w32
        grad_x = (rstd * (g - xn * (g * xn).mean(-1, keepdim=True))).to(x.dtype)
        return grad_x, grad_w, None


def rmsnorm(x: torch.Tensor, w: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
    """对外的算子入口:训练代码里 `model.forward` 直接调用它。"""

    return RMSNormTileLang.apply(x, w, eps)


if __name__ == "__main__":
    # 这段只有解析意义:本机无 GPU,导入 tilelang 会失败。
    # 在带卡的机器上,把 `cuda` 换成 `npu` 即可跑昇腾分支。
    device = "cuda" if torch.cuda.is_available() else "cpu"
    x = torch.randn(256, 512, dtype=torch.bfloat16, device=device, requires_grad=True)
    w = torch.randn(512, dtype=torch.bfloat16, device=device, requires_grad=True)
    y = rmsnorm(x, w)
    (y.float().pow(2).mean()).backward()
    print("y", y.shape, "dx", x.grad.shape, "dw", w.grad.shape)

← 浏览本章全部代码