跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 24 章 NVIDIA 后端:代码生成与硬件特性

/code/24-nvidia-backend-codegen/

02-gemm-inst-selection/gemm_inst_selection.py10173 字节
#!/usr/bin/env python3
"""A tiny, dependency-free model of TileLang's CUDA GEMM instruction selection.

Mirrors Gemm::SelectInst in
_vendor/tilelang-v0.1.15/src/cuda/op/gemm.cc:336-375 and the f16/bf16 branch of
GetTCGEN5MMAMeta in _vendor/tilelang-v0.1.15/src/op/tcgen5_meta.h:22-177.

It is NOT the compiler. See README.md for the exact simplifications.
"""

from __future__ import annotations

from dataclasses import dataclass

MMA = "cuda.mma"
FMA = "cuda.fma"
WGMMA = "cuda.wgmma"
TCGEN05 = "cuda.tcgen05"

F16, BF16, F32, TF32, FP8, INT8 = "f16", "bf16", "f32", "tf32", "fp8", "int8"


def arch_of(target: str) -> int:
    return int(target.removeprefix("sm_").rstrip("af"))


def is_volta(target: str) -> bool:
    return 70 <= arch_of(target) < 75


def is_turing(target: str) -> bool:
    return 75 <= arch_of(target) < 80


def is_hopper(target: str) -> bool:
    return 90 <= arch_of(target) < 100


def is_sm100(target: str) -> bool:
    return 100 <= arch_of(target) <= 110


# --- GetTCGEN5MMAMeta, f16/bf16 -> f32 branch only --------------------------

def tcgen5_meta_f16(m: int, n: int, k: int, *, disable_2cta: bool = False,
                    disable_ws: bool = False) -> tuple | None:
    """Return (atom_m, atom_n, atom_k, enable_ws, enable_2cta) or None."""
    if k % 16 != 0:
        return None
    if not disable_2cta:
        if m % 128 == 0:
            for atom_n in range(256, 15, -16):
                if n % atom_n == 0:
                    return (256, atom_n, 16, False, True)
        elif m % 64 == 0:
            for atom_n in range(256, 15, -16):
                if n % atom_n == 0:
                    return (128, atom_n, 16, False, True)
    if m % 128 == 0:
        for atom_n in range(256, 7, -8):
            if n % atom_n == 0:
                return (128, atom_n, 16, False, False)
        return None
    if m % 64 == 0:
        if not disable_ws:
            for atom_n in (256, 128, 64):
                if n % atom_n == 0:
                    return (64, atom_n, 16, True, False)
        return None
    if m % 32 == 0:
        if not disable_ws:
            for atom_n in (256, 128, 64):
                if n % atom_n == 0:
                    return (32, atom_n, 16, True, False)
        return None
    return None


@dataclass(frozen=True)
class GemmOp:
    a_dtype: str
    b_dtype: str
    c_dtype: str
    a_scope: str  # "shared" | "shared.dyn" | "local.fragment" | "shared.tmem"
    b_scope: str
    c_scope: str
    m: int
    n: int
    k: int
    block_size: int = 256
    trans_a: bool = False
    trans_b: bool = False
    explicit: str | None = None  # None | "wgmma" | "tcgen05"
    use_2cta: bool = False
    disable_wgmma: bool = False
    gemm_blockscaled: bool = False


def is_shared(scope: str) -> bool:
    return scope in ("shared", "shared.dyn")


def is_fragment(scope: str) -> bool:
    return scope == "local.fragment"


def is_tmem(scope: str) -> bool:
    return scope == "shared.tmem"


def check_wgmma(op: GemmOp) -> bool:
    """Mirror CheckWgmma (gemm.cc:40-78)."""
    if not is_shared(op.b_scope):
        return False
    if op.c_dtype == F16:
        if op.a_dtype == F16 and op.b_dtype == F16:
            return op.k % 16 == 0
        if op.a_dtype == FP8 and op.b_dtype == FP8:
            return (not op.trans_a) and op.trans_b and op.k % 32 == 0
        return False
    if op.c_dtype == F32:
        if op.a_dtype == F16 and op.b_dtype == F16:
            return op.k % 16 == 0
        if op.a_dtype == BF16 and op.b_dtype == BF16:
            return op.k % 16 == 0
        if op.a_dtype == TF32 and op.b_dtype == TF32:
            return (not op.trans_a) and op.trans_b and op.k % 8 == 0
        if op.a_dtype == FP8 and op.b_dtype == FP8:
            return (not op.trans_a) and op.trans_b and op.k % 32 == 0
        return False
    if op.c_dtype == "int32":
        ok_dtypes = {(INT8, INT8), (INT8, "uint8"), ("uint8", INT8), ("uint8", "uint8")}
        if (op.a_dtype, op.b_dtype) in ok_dtypes:
            return (not op.trans_a) and op.trans_b and op.k % 32 == 0
        return False
    return False


def allow_wgmma(op: GemmOp, target: str) -> bool:
    if op.disable_wgmma:
        return False
    num_warps = op.block_size // 32
    return (is_hopper(target) and op.m >= 64 and num_warps % 4 == 0
            and check_wgmma(op))


def allow_tcgen5_mma(op: GemmOp, target: str) -> bool:
    """Mirror AllowTcgen5Mma (gemm.cc:80-87)."""
    scope_ok = ((is_shared(op.a_scope) or is_tmem(op.a_scope))
                and is_shared(op.b_scope) and is_tmem(op.c_scope))
    if not is_sm100(target) or not scope_ok:
        return False
    if op.c_dtype != F32 or op.a_dtype not in (F16, BF16):
        return False  # only the f16/bf16 -> f32 branch is modeled
    meta = tcgen5_meta_f16(op.m, op.n, op.k)
    return meta is not None


def allow_volta_mma(op: GemmOp) -> bool:
    """Mirror AllowVoltaMma (gemm.cc:99-117)."""
    scope_ok = (is_shared(op.a_scope) or is_fragment(op.a_scope)) and is_shared(op.b_scope)
    if not scope_ok or op.trans_a:
        return False
    if op.a_dtype != F16 or op.b_dtype != F16:
        return False
    if op.c_dtype not in (F16, F32):
        return False
    return op.m % 16 == 0 and op.n % 16 == 0 and op.k % 4 == 0


def allow_turing_mma(op: GemmOp) -> bool:
    """Mirror AllowTuringMma (gemm.cc:131-145)."""
    if op.a_dtype != op.b_dtype:
        return False
    if op.a_dtype == F16:
        return op.c_dtype in (F16, F32)
    if op.a_dtype in (INT8, "uint8", "int4", "uint4"):
        return op.c_dtype == "int32"
    return False


class GemmSelectionError(Exception):
    pass


def select_inst(op: GemmOp, target: str) -> str:
    """Mirror Gemm::SelectInst (gemm.cc:336-375)."""
    if op.explicit == "wgmma":
        if not allow_wgmma(op, target):
            raise GemmSelectionError("T.wgmma_gemm() requires Hopper WGMMA lowering")
        return WGMMA
    if op.explicit == "tcgen05":
        if not allow_tcgen5_mma(op, target):
            raise GemmSelectionError("T.tcgen05_gemm() requires Blackwell TCGEN5MMA lowering")
        return TCGEN05
    if op.use_2cta:
        if not allow_tcgen5_mma(op, target):
            raise GemmSelectionError(
                "use_2cta=True requires Blackwell TCGEN5MMA lowering; "
                "no one-CTA instruction fallback is valid")
        return TCGEN05
    if allow_tcgen5_mma(op, target):
        return TCGEN05
    if allow_wgmma(op, target):
        return WGMMA
    if is_volta(target) and not allow_volta_mma(op):
        return FMA
    if is_turing(target) and not allow_turing_mma(op):
        return FMA
    return MMA


def describe(op: GemmOp, target: str) -> str:
    try:
        return select_inst(op, target)
    except GemmSelectionError as exc:
        return "ERROR: " + str(exc)


def self_check(rows):
    expected = {
        "hopper big": WGMMA,
        "hopper small M": MMA,
        "hopper wrong warp group": MMA,
        "hopper fp8 mn-major A": MMA,
        "hopper fp8 k-major A": WGMMA,
        "blackwell C in tmem": TCGEN05,
        "blackwell C in fragment": MMA,
        "blackwell M=64 C in tmem": TCGEN05,
        "turing f16": MMA,
        "volta f16->f16": MMA,
        "volta transposed A -> fma": FMA,
        "ampere": MMA,
        "explicit wgmma on hopper": WGMMA,
        "explicit wgmma on ampere": "ERROR",
        "use_2cta on blackwell": TCGEN05,
        "use_2cta on hopper": "ERROR",
    }
    ok = True
    for name, _op, _target, result in rows:
        want = expected[name]
        good = result.startswith("ERROR") if want == "ERROR" else result == want
        if not good:
            print(f"SELF-CHECK FAILED: {name}: expected {want}, got {result}")
            ok = False
    print("self-check:", "ok" if ok else "FAILED")
    return ok


def main() -> int:
    base = dict(a_dtype=F16, b_dtype=F16, c_dtype=F32, a_scope="shared.dyn",
                b_scope="shared.dyn", c_scope="local.fragment",
                m=128, n=128, k=64)
    scenarios = [
        ("hopper big", GemmOp(**{**base, "trans_b": True}), "sm_90a"),
        ("hopper small M", GemmOp(**{**base, "m": 32, "trans_b": True}), "sm_90a"),
        ("hopper wrong warp group", GemmOp(**{**base, "block_size": 160, "trans_b": True}), "sm_90a"),
        ("hopper fp8 mn-major A",
         GemmOp(a_dtype=FP8, b_dtype=FP8, c_dtype=F32, a_scope="shared.dyn",
                b_scope="shared.dyn", c_scope="local.fragment",
                m=128, n=128, k=64, trans_a=True, trans_b=True), "sm_90a"),
        ("hopper fp8 k-major A",
         GemmOp(a_dtype=FP8, b_dtype=FP8, c_dtype=F32, a_scope="shared.dyn",
                b_scope="shared.dyn", c_scope="local.fragment",
                m=128, n=128, k=64, trans_a=False, trans_b=True), "sm_90a"),
        ("blackwell C in tmem", GemmOp(**{**base, "c_scope": "shared.tmem"}), "sm_100a"),
        ("blackwell C in fragment", GemmOp(**base), "sm_100a"),
        ("blackwell M=64 C in tmem", GemmOp(**{**base, "m": 64, "c_scope": "shared.tmem"}), "sm_100a"),
        ("turing f16", GemmOp(**base), "sm_75"),
        ("volta f16->f16", GemmOp(**{**base, "c_dtype": F16}), "sm_70"),
        ("volta transposed A -> fma", GemmOp(**{**base, "trans_a": True}), "sm_70"),
        ("ampere", GemmOp(**base), "sm_80"),
        ("explicit wgmma on hopper", GemmOp(**{**base, "explicit": "wgmma", "trans_b": True}), "sm_90a"),
        ("explicit wgmma on ampere", GemmOp(**{**base, "explicit": "wgmma"}), "sm_80"),
        ("use_2cta on blackwell",
         GemmOp(**{**base, "c_scope": "shared.tmem", "use_2cta": True}), "sm_100a"),
        ("use_2cta on hopper", GemmOp(**{**base, "use_2cta": True}), "sm_90a"),
    ]
    print("=== T.gemm 指令选择(模型) ===")
    rows = []
    for name, op, target in scenarios:
        result = describe(op, target)
        rows.append((name, op, target, result))
        print(f"{target:<8} M={op.m:<4} N={op.n:<4} K={op.k:<4} "
              f"{op.a_dtype}->{op.c_dtype:<5} A={op.a_scope:<14} C={op.c_scope:<14} "
              f"-> {result}")
    print()
    return 0 if self_check(rows) else 1


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

← 浏览本章全部代码