跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 14 章 算子实战 II:GEMM 全家族

/code/14-operators-gemm/

02-tilelang-ladder/gemm_ladder.py7029 字节
#!/usr/bin/env python3
"""第 14 章第 2 节的阶梯:同一个 GEMM 的 ①–④ 级实现。

每一级只改一件事,方便对照「为什么这一级比上一级快」:

  RUNG_0  朴素分块:直接读 global 做乘加
  RUNG_1  shared memory + T.gemm(不流水)
  RUNG_2  T.Pipelined(num_stages=N)
  RUNG_3  RUNG_2 + swizzle / layout 注解

第 ⑤ 级(intrinsic)在 gemm_intrinsic.py。

运行前提:NVIDIA GPU + CUDA toolkit + PyTorch(CUDA) + TileLang v0.1.15。
写作机上没有 GPU,本文件只做过静态核对(py_compile + API 比对),**未运行**。

用法::

    python3 gemm_ladder.py --rung 2 --m 4096 --n 4096 --k 4096
    python3 gemm_ladder.py --rung 3 --num-stages 3 --bench
"""

from __future__ import annotations

import argparse

import tilelang
import tilelang.language as T

try:  # RUNG_3 需要 swizzle layout 工具;v0.1.15 里它在 cuda.intrinsics 下
    from tilelang.cuda.intrinsics import make_mma_swizzle_layout
except ImportError:  # pragma: no cover - 仅为让文件在无 TileLang 时也能被阅读
    make_mma_swizzle_layout = None


@tilelang.jit
def gemm_rung0(A, B, block_M, block_N, dtype=T.float16, accum_dtype=T.float32):
    """① 朴素分块:每个输出元素沿 K 直接读 global。"""
    M, N, K = T.const("M, N, K")
    A: T.Tensor((M, K), dtype)
    B: T.Tensor((K, N), dtype)
    C = T.empty((M, N), dtype)

    with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
        C_local = T.alloc_fragment((block_M, block_N), accum_dtype)
        T.clear(C_local)
        for i, j in T.Parallel(block_M, block_N):
            for k in T.serial(K):
                C_local[i, j] = C_local[i, j] + A[by * block_M + i, k] * B[k, bx * block_N + j]
        T.copy(C_local, C[by * block_M, bx * block_N])

    return C


@tilelang.jit
def gemm_rung1(A, B, block_M, block_N, block_K, dtype=T.float16, accum_dtype=T.float32):
    """② shared memory + T.gemm:先搬上片,再调 tile 级矩阵乘(串行循环)。"""
    M, N, K = T.const("M, N, K")
    A: T.Tensor((M, K), dtype)
    B: T.Tensor((K, N), dtype)
    C = T.empty((M, N), dtype)

    with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
        A_shared = T.alloc_shared((block_M, block_K), dtype)
        B_shared = T.alloc_shared((block_K, block_N), dtype)
        C_local = T.alloc_fragment((block_M, block_N), accum_dtype)

        T.clear(C_local)
        for ko in T.serial(T.ceildiv(K, block_K)):
            T.copy(A[by * block_M, ko * block_K], A_shared)
            T.copy(B[ko * block_K, bx * block_N], B_shared)
            T.gemm(A_shared, B_shared, C_local)

        T.copy(C_local, C[by * block_M, bx * block_N])

    return C


@tilelang.jit
def gemm_rung2(A, B, block_M, block_N, block_K, num_stages=3, dtype=T.float16, accum_dtype=T.float32):
    """③ 加 T.Pipelined:搬运与计算重叠。"""
    M, N, K = T.const("M, N, K")
    A: T.Tensor((M, K), dtype)
    B: T.Tensor((K, N), dtype)
    C = T.empty((M, N), dtype)

    with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
        A_shared = T.alloc_shared((block_M, block_K), dtype)
        B_shared = T.alloc_shared((block_K, block_N), dtype)
        C_local = T.alloc_fragment((block_M, block_N), accum_dtype)

        T.clear(C_local)
        for ko in T.Pipelined(T.ceildiv(K, block_K), num_stages=num_stages):
            T.copy(A[by * block_M, ko * block_K], A_shared)
            T.copy(B[ko * block_K, bx * block_N], B_shared)
            T.gemm(A_shared, B_shared, C_local)

        T.copy(C_local, C[by * block_M, bx * block_N])

    return C


@tilelang.jit
def gemm_rung3(A, B, block_M, block_N, block_K, num_stages=3, panel_size=10,
               dtype=T.float16, accum_dtype=T.float32):
    """④ 加 swizzle / layout 注解:改物理布局与 block 遍历顺序。"""
    M, N, K = T.const("M, N, K")
    A: T.Tensor((M, K), dtype)
    B: T.Tensor((K, N), dtype)
    C = T.empty((M, N), dtype)

    with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
        A_shared = T.alloc_shared((block_M, block_K), dtype)
        B_shared = T.alloc_shared((block_K, block_N), dtype)
        C_local = T.alloc_fragment((block_M, block_N), accum_dtype)

        if make_mma_swizzle_layout is not None:
            T.annotate_layout({
                A_shared: make_mma_swizzle_layout(A_shared),
                B_shared: make_mma_swizzle_layout(B_shared),
            })
        T.use_swizzle(panel_size=panel_size)

        T.clear(C_local)
        for ko in T.Pipelined(T.ceildiv(K, block_K), num_stages=num_stages):
            T.copy(A[by * block_M, ko * block_K], A_shared)
            T.copy(B[ko * block_K, bx * block_N], B_shared)
            T.gemm(A_shared, B_shared, C_local)

        T.copy(C_local, C[by * block_M, bx * block_N])

    return C


def build(rung: int, m: int, n: int, k: int, block_m: int, block_n: int, block_k: int,
          num_stages: int):
    if rung == 0:
        return gemm_rung0.compile(M=m, N=n, K=k, block_M=block_m, block_N=block_n)
    if rung == 1:
        return gemm_rung1.compile(M=m, N=n, K=k, block_M=block_m, block_N=block_n, block_K=block_k)
    if rung == 2:
        return gemm_rung2.compile(M=m, N=n, K=k, block_M=block_m, block_N=block_n,
                                  block_K=block_k, num_stages=num_stages)
    if rung == 3:
        return gemm_rung3.compile(M=m, N=n, K=k, block_M=block_m, block_N=block_n,
                                  block_K=block_k, num_stages=num_stages)
    raise ValueError(f"未知的 rung: {rung}(第 ⑤ 级见 gemm_intrinsic.py)")


def main() -> int:
    parser = argparse.ArgumentParser(description="TileLang GEMM 阶梯 ①–④")
    parser.add_argument("--rung", type=int, default=2, choices=[0, 1, 2, 3])
    parser.add_argument("--m", type=int, default=4096)
    parser.add_argument("--n", type=int, default=4096)
    parser.add_argument("--k", type=int, default=4096)
    parser.add_argument("--block-m", type=int, default=128)
    parser.add_argument("--block-n", type=int, default=128)
    parser.add_argument("--block-k", type=int, default=32)
    parser.add_argument("--num-stages", type=int, default=3)
    parser.add_argument("--bench", action="store_true", help="跑 do_bench(需要 GPU)")
    parser.add_argument("--show-source", action="store_true", help="打印生成的 CUDA 源码")
    args = parser.parse_args()

    import torch

    kernel = build(args.rung, args.m, args.n, args.k, args.block_m, args.block_n,
                   args.block_k, args.num_stages)
    a = torch.randn(args.m, args.k, device="cuda", dtype=torch.float16)
    b = torch.randn(args.k, args.n, device="cuda", dtype=torch.float16)
    c = kernel(a, b)

    ref = a @ b
    torch.testing.assert_close(c, ref, rtol=1e-2, atol=1e-2)
    print(f"rung {args.rung}: 正确性通过(rtol=1e-2, atol=1e-2)")

    if args.show_source:
        print(kernel.get_kernel_source())

    if args.bench:
        profiler = kernel.get_profiler()
        latency = profiler.do_bench(warmup=25, rep=100, return_mode="mean")
        tflops = 2 * args.m * args.n * args.k / (latency / 1e3) / 1e12
        print(f"latency={latency:.4f} ms  ({tflops:.2f} TFLOP/s, 口径 2MNK)")
        print("报数时请一并写明 warmup/rep/backend/return_mode 与是否 flush L2")
    return 0


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

← 浏览本章全部代码