跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 19 章 昇腾实战:在昇腾上开发与调优算子

/code/19-ascend-hands-on/

05-gemm-dialect/gemm_relu_dialect.py3658 字节
#!/usr/bin/env python3
"""第 19 章 §7:主干(M 树)昇腾方言的 GEMM + ReLU 混核 kernel。

来源:`_vendor/tilelang-v0.1.15/tilelang/ascend/README.md:66-108` 的 quickstart。
它同时演示了三件 M 树独有的事:
  1. T.alloc_l1 / T.alloc_l0c 的小写方言 API;
  2. T.Persistent + T.Pipelined 的自动调度;
  3. T.dual_copy 把 L0C 结果劈给两个 AIV,再在 T.SimtVF 里做 ReLU。

运行前提:Ascend 950 NPU + 兼容 CANN(含 bisheng)+ torch_npu + 主干 TileLang v0.1.15。
验证级别:仅静态核对(py_compile + API 比对),未在 NPU 上运行。
"""

from __future__ import annotations

import argparse

import torch
import torch_npu  # noqa: F401 - 注册 torch.npu
import tilelang
import tilelang.ascend.language as T


@tilelang.jit(target="ascend")
def matmul_relu(A, B, block_M: int = 256, block_N: int = 224, block_K: int = 128):
    """C = relu(A @ B^T),bf16 输入,fp32 累加与输出。

    A:(M, K),B:(N, K)(对应 transpose_B=True)。
    所有维度都能被 tile 整除;本片段不处理 partial tile。
    """
    M, N, K = T.const("M, N, K")
    A: T.Tensor((M, K), T.bfloat16)
    B: T.Tensor((N, K), T.bfloat16)
    C = T.empty((M, N), T.float32)
    num_blocks = 32
    n_tiles = N // block_N

    with T.Kernel(num_blocks) as bx:
        A_l1 = T.alloc_l1((block_M, block_K), T.bfloat16)
        B_l1 = T.alloc_l1((block_N, block_K), T.bfloat16)
        C_l0c = T.alloc_l0c((block_M, block_N), T.float32)
        # Cube 与 Vector 不能直接交换数据;这里用 UB 承接 L0C 的输出。
        C_ub = T.alloc_shared((block_M // 2, block_N), T.float32)

        # Persistent:固定 32 个 block,轮转处理所有输出 tile。
        for tile in T.Persistent([M // block_M * n_tiles], num_blocks, bx):
            m = tile // n_tiles * block_M
            n = tile % n_tiles * block_N
            for k in T.Pipelined(K // block_K, num_stages=2):
                T.copy(A[m, k * block_K], A_l1)
                # L2 提示:权重复用度高,保留在 L2(NOTALLOC_KEEP)。
                T.copy(B[n, k * block_K], B_l1, l2_cache_ctrl="NOTALLOC_KEEP")
                T.gemm(A_l1, B_l1, C_l0c, transpose_B=True, clear_accum=(k == 0))
            # L0C -> UB:硬件双目的拷贝,M-split 给两个 AIV。
            T.dual_copy(C_l0c, C_ub)
            with T.SimtVF(threads=128):
                for i, j in T.Parallel(block_M // 2, block_N):
                    C_ub[i, j] = T.max(C_ub[i, j], 0)
            # UB -> GM:L2 提示 NOTALLOC_PW(写出后不需要保留)。
            T.dual_copy(C_ub, C[m : m + block_M, n : n + block_N], l2_cache_ctrl="NOTALLOC_PW")

    return C


def ref_program(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
    """参考实现:relu(A.float() @ B.float().T)。"""
    return torch.relu(a.float() @ b.float().T)


def main() -> int:
    parser = argparse.ArgumentParser(description="Ascend 950 GEMM+ReLU (main tree dialect)")
    parser.add_argument("--m", type=int, default=256)
    parser.add_argument("--n", type=int, default=7168)
    parser.add_argument("--k", type=int, default=2048)
    parser.add_argument("--bench", action=argparse.BooleanOptionalAction, default=False)
    args = parser.parse_args()

    m, n, k = args.m, args.n, args.k
    a = torch.randn((m, k), device="npu", dtype=torch.bfloat16)
    b = torch.randn((n, k), device="npu", dtype=torch.bfloat16)

    c = matmul_relu(a, b)
    torch.testing.assert_close(c, ref_program(a, b), rtol=1e-2, atol=1e-2)
    print("GEMM + ReLU passed.")

    if args.bench:
        from tilelang.profiler import do_bench

        # 注意:matmul_relu 已经被 @tilelang.jit 装饰并直接返回输出张量,
        # 所以这里直接把它当 callable 计时,不要再 compile 一次。
        latency = do_bench(lambda: matmul_relu(a, b), backend="msprof", _n_warmup=30, _n_repeat=50)
        tflops = 2.0 * m * n * k / (latency * 1e9)
        print(f"gemm_relu (M={m}, N={n}, K={k}): {latency * 1000:.2f} us | {tflops:.1f} TFLOPS")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

← 浏览本章全部代码