跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

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

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

06-ascend/ascend_mainline_kernels.py7394 字节
"""第 13 章 · 昇腾侧同款实现(主干 v0.1.15,`tilelang.ascend.language`)。

需要:昇腾 950 NPU + CANN + torch_npu + 从源码构建的 TileLang v0.1.15
(`USE_ASCEND=ON USE_CUDA=OFF`)。

这个方言和 CUDA 方言的三点不同,在本文件里都能看到:

1. `T.Kernel(N)` 只给**一维核网格**,没有 `threads=`:`with T.Kernel(n) as bx`
   拿到的是 AI Core 的下标(对照 `_vendor/tilelang-v0.1.15/tilelang/ascend/language/kernel.py:139`)。
2. `T.alloc_shared` 在昇腾侧分配的就是 **UB(Unified Buffer)**
   (`.../tilelang/ascend/language/allocate.py:15-23`),没有单独的 `T.alloc_ub`。
3. 线程级并行要显式套一层 `T.SimtVF(threads=...)`;在它内部 `T.Parallel` 才会被
   向量化成 Vector 单元上的 SIMD 指令。不想用线程抽象时可以用 `T.SimdVF()` 直接
   写向量寄存器(见 `examples/ascend/example_vecadd.py` 的 `mode="simd"` 分支)。

对照的官方示例:
  _vendor/tilelang-v0.1.15/examples/ascend/example_vecadd.py
  _vendor/tilelang-v0.1.15/examples/ascend/example_rmsnorm.py
  _vendor/tilelang-v0.1.15/examples/ascend/example_per_token_cast_to_fp8.py

本文件只做**静态核对**:API 名逐个对照过 `_vendor/`,`python3 -m py_compile` 通过。
**没有在 NPU 上编译或运行过**(本机没有昇腾设备),不声明任何性能数字。
"""

from __future__ import annotations

import tilelang
import tilelang.ascend.language as T


# ---------------------------------------------------------------------------
# ① 逐元素:向量加(SIMT 路径)
# ---------------------------------------------------------------------------


def vector_add(N: int, mode: str = "simt", num_blocks: int = 64, tile: int = 8192, num_stages: int = 2):
    """`C = A + B`,结构简化自 `examples/ascend/example_vecadd.py`。

    与 CUDA 版逐元素 kernel 的三个关键差异:

    * **没有线程块内的 `T.Parallel(tile)`**:数据先 DMA 进 UB,再在 `T.SimtVF`
      里并行计算,最后 DMA 出去。搬运与计算是两条独立的流水线(MTE2 / Vector)。
    * **`T.annotate_buffer_versions` 声明 UB 的多缓冲份数**,配合 `T.Pipelined`
      让「搬下一块」和「算这一块」重叠。
    * **tile 大小由 UB 容量决定**:UB 是有限的一整块片上存储,x/y/z 三个 buffer
      乘以份数不能超过它。这和 CUDA 上「shared memory 用完可以靠 L1 兜底」不同。
    """
    if N <= 0 or N % (tile * num_blocks) != 0:
        raise ValueError(f"N must be a positive multiple of tile*num_blocks, got N={N}")
    if mode not in ("simt", "simd"):
        raise ValueError("mode must be simt or simd")
    iterations = N // (tile * num_blocks)

    @T.prim_func
    def main(
        A: T.Tensor((N,), "float32"),  # type: ignore[valid-type]
        B: T.Tensor((N,), "float32"),  # type: ignore[valid-type]
        C: T.Tensor((N,), "float32"),  # type: ignore[valid-type]
    ):
        with T.Kernel(num_blocks) as bx:
            a = T.alloc_shared((tile,), "float32")
            b = T.alloc_shared((tile,), "float32")
            c = T.alloc_shared((tile,), "float32")
            T.annotate_buffer_versions({a: num_stages, b: num_stages, c: num_stages})

            for step in T.Pipelined(iterations, num_stages=num_stages):
                begin = (step * num_blocks + bx) * tile
                T.copy(A[begin : begin + tile], a)
                T.copy(B[begin : begin + tile], b)
                if mode == "simt":
                    with T.SimtVF(threads=2048):
                        for i in T.Parallel(tile):
                            c[i] = a[i] + b[i]
                else:
                    with T.SimdVF():
                        mask = T.simd.pset(32)
                        for i in range(tile // 64):
                            a_reg = T.simd.vld(a[i * 64])
                            b_reg = T.simd.vld(b[i * 64])
                            T.simd.vsts(c[i * 64], T.simd.vadd(a_reg, b_reg, mask), mask)
                T.copy(c, C[begin : begin + tile])

    return main


# ---------------------------------------------------------------------------
# ② 归一化:RMSNorm(T.SimtVF + reducer)
# ---------------------------------------------------------------------------


@tilelang.jit(out_idx=[1, 3])
def rms_norm_ascend(
    batch: int,
    d: int,
    eps: float = 1e-6,
    dtype: str = "float32",
    n_cores: int = 64,
    rows: int = 4,
    threads: int = 256,
):
    """RMSNorm,结构简化自 `examples/ascend/example_rmsnorm.py`(rows 固定、去掉
    `bulk_rstd` 分流)。

    为什么要在 `T.SimtVF` 里用 `T.alloc_reducer` / `T.reducer_update` /
    `T.finalize_reducer`,而不是直接 `T.reduce_sum`:

    * SimtVF 的线程域是可以被编译器**物理复制**的(同一个逻辑输出可能由多个线程
      各自贡献一部分),one-shot 的 reduce 语义在这里没有定义;
    * reducer 是**有 epoch 的**:`reducer_init` 开一轮、`reducer_update` 每次逻辑
      迭代恰好贡献一次、`finalize_reducer` 收口。物理复制多少次都不影响逻辑结果。

    语义与接口见 `_vendor/tilelang-v0.1.15/tilelang/language/reduce_op.py:358-460`。
    """
    if batch % (n_cores * rows) != 0:
        raise ValueError(f"needs batch % (n_cores*rows) == 0, got batch={batch}")
    tile = tilelang.next_power_of_2(d)
    iterations = batch // (n_cores * rows)

    @T.prim_func
    def main(
        X: T.Tensor((batch * d,), dtype),  # type: ignore[valid-type]
        Y: T.Tensor((batch * d,), dtype),  # type: ignore[valid-type]
        W: T.Tensor((d,), dtype),  # type: ignore[valid-type]
        RSTD: T.Tensor((batch,), "float32"),  # type: ignore[valid-type]
        eps_v: T.float32,
    ):
        with T.Kernel(n_cores) as core_id:
            w_ub = T.alloc_shared((d,), dtype)
            x_ub = T.alloc_shared((rows * d,), "float32")
            y_ub = T.alloc_shared((rows * d,), "float32")
            rstd_ub = T.alloc_shared((rows,), "float32")
            T.annotate_buffer_versions({x_ub: 2, y_ub: 1, rstd_ub: 2})

            T.copy(W[:d], w_ub[:d])
            for t in T.Pipelined(iterations, num_stages=2):
                row_id = (t * n_cores + core_id) * rows
                base = row_id * d
                T.copy(X[base : base + rows * d], x_ub)

                with T.SimtVF(threads=threads):
                    x_frag = T.alloc_fragment((rows, tile), "float32")
                    # d 不一定是 2 的幂:多出来的列读 0,对平方和无影响。
                    for r, i in T.Parallel(rows, tile, coalesced_width=T.int32(1)):
                        x_frag[r, i] = T.if_then_else(i < d, x_ub[r * d + i], T.float32(0))

                    sum_sq = T.alloc_reducer((rows,), "float32", op="sum")
                    T.reducer_init(sum_sq)
                    for r, i in T.Parallel(rows, tile, coalesced_width=T.int32(1)):
                        if i < d:
                            T.reducer_update(sum_sq[r], x_frag[r, i] * x_frag[r, i])
                    sum_result = T.alloc_fragment((rows,), "float32")
                    T.finalize_reducer(sum_sq, sum_result)

                    for r in T.Parallel(rows):
                        sum_result[r] = T.rsqrt(sum_result[r] / d + eps_v)
                        rstd_ub[r] = sum_result[r]

                    w_frag = T.alloc_fragment((tile,), "float32")
                    for i in T.Parallel(tile, coalesced_width=T.int32(1)):
                        w_frag[i] = T.if_then_else(i < d, T.Cast("float32", w_ub[i]), T.float32(0))
                    for r, i in T.Parallel(rows, tile, coalesced_width=T.int32(1)):
                        if i < d:
                            y_ub[r * d + i] = x_frag[r, i] * sum_result[r] * w_frag[i]

                T.copy(y_ub, Y[base : base + rows * d])
                T.copy(rstd_ub, RSTD[row_id : row_id + rows])

    return main


if __name__ == "__main__":
    # 需要昇腾 NPU + torch_npu。本机从未执行过。
    import torch
    import torch_npu  # noqa: F401

    n = 1 << 20
    a = torch.randn(n, device="npu")
    b = torch.randn(n, device="npu")
    kernel = tilelang.compile(vector_add(n, mode="simt"), target="ascend", out_idx=-1)
    torch.testing.assert_close(kernel(a, b), a + b)
    print("vector_add (ascend, simt): correctness passed")

← 浏览本章全部代码