跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

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

/code/24-nvidia-backend-codegen/

03-tma-descriptor/tma_descriptor.py8204 字节
#!/usr/bin/env python3
"""Work out the cuTensorMapEncodeTiled arguments for a TMA tile copy.

The real descriptor is encoded on the host by
_vendor/tilelang-v0.1.15/tilelang/jit/adapter/wrapper.py:111-131 from the
`tma_descriptor_args` attribute that LowerHopperIntrin attaches to the host
function. The constraints checked here mirror CheckBulkLoad / CheckBulkStore /
CheckGlobalStrides in _vendor/tilelang-v0.1.15/src/cuda/op/copy_analysis.cc.

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

from __future__ import annotations

from dataclasses import dataclass, field

# From _vendor/tilelang-v0.1.15/src/cuda/op/tma_layout.h:40.
TMA_MAX_BOX_DIM = 256
TMA_SMEM_ADDR_ALIGN = 128

# Symbolic names for CUtensorMapDataType (numeric values live in cuda.h).
CU_TENSOR_MAP_DATA_TYPE = {
    "u8": "UINT8",
    "u16": "UINT16",
    "u32": "UINT32",
    "s32": "INT32",
    "u64": "UINT64",
    "s64": "INT64",
    "f16": "FLOAT16",
    "f32": "FLOAT32",
    "f64": "FLOAT64",
    "bf16": "BFLOAT16",
    "f32_f16": "FLOAT32_FLOAT32",
    "tf32": "TFLOAT32",
    "f8": "FLOAT8_E4M3",
    "f8_e5m2": "FLOAT8_E5M2",
    "f4_align16b": "16U4_ALIGN16B",  # packed FP4 global -> unpacked FP4 smem
}

F16_BITS = 16


class Rejected(Exception):
    pass


@dataclass(frozen=True)
class TensorSpec:
    """A global tensor plus the tile box we want to move with TMA."""

    name: str
    dtype: str
    elem_bits: int
    shape: tuple[int, ...]
    box: tuple[int, ...]
    strides: tuple[int, ...] = ()  # element strides, innermost last; () = row-major
    element_strides: tuple[int, ...] = ()
    interleave: str = "NONE"
    swizzle: str = "NONE"
    l2_promotion: str = "L2_128B"
    oob_fill: str = "NONE"
    extra: dict = field(default_factory=dict)

    def element_strides_or_default(self) -> tuple[int, ...]:
        return self.element_strides or tuple([1] * len(self.box))

    def strides_or_default(self) -> tuple[int, ...]:
        if self.strides:
            return self.strides
        strides = [1] * len(self.shape)
        acc = 1
        for i in range(len(self.shape) - 1, -1, -1):
            strides[i] = acc
            acc *= self.shape[i]
        return tuple(strides)


def check_global_strides(spec: TensorSpec) -> None:
    """Mirror CheckGlobalStrides (copy_analysis.cc:143-193)."""
    strides = spec.strides_or_default()
    if strides[-1] != 1:
        raise Rejected(f"global 最内层 stride 是 {strides[-1]},不是 1")
    for i, stride in enumerate(strides[:-1]):
        stride_bytes = stride * spec.elem_bits // 8
        if stride_bytes % 16 != 0:
            raise Rejected(f"globalStride[{i}] 字节数 {stride_bytes} 不是 16 的倍数")
        if stride_bytes >= (1 << 40):
            raise Rejected(f"globalStride[{i}] 字节数 {stride_bytes} 超过 2^40")


def check_last_dim(spec: TensorSpec) -> None:
    """Mirror the check_last_dim branch of CheckBulkLoad/Store."""
    boxes_bits = spec.box * spec.elem_bits if isinstance(spec.box, int) else None
    if boxes_bits is None:
        last_bits = spec.box[-1] * spec.elem_bits
    else:
        last_bits = boxes_bits
    if last_bits % 128 != 0:
        raise Rejected(f"最后一维 {spec.box[-1]} * {spec.elem_bits} bits = "
                       f"{last_bits} bits,不是 128 的倍数")


def check_box_dim(spec: TensorSpec) -> None:
    for dim in spec.box:
        if dim > TMA_MAX_BOX_DIM:
            raise Rejected(f"boxDim {dim} 超过上限 {TMA_MAX_BOX_DIM}")


def build_descriptor(spec: TensorSpec) -> dict:
    check_last_dim(spec)
    check_box_dim(spec)
    check_global_strides(spec)
    if spec.dtype not in CU_TENSOR_MAP_DATA_TYPE:
        raise Rejected(f"未建模的 dtype {spec.dtype}")
    if spec.swizzle != "NONE" and (spec.elem_bits // 8 * spec.box[-1]) % TMA_SMEM_ADDR_ALIGN != 0:
        raise Rejected("swizzle 要求 shared 侧每行至少 128 字节对齐")
    rank = len(spec.shape)
    strides = spec.strides_or_default()
    # The wrapper passes `globalStride + 1`, i.e. it drops the innermost stride
    # (which must be 1 anyway) and reports the byte offsets as 64-bit values.
    global_stride_bytes = [strides[i] * spec.elem_bits // 8 for i in range(rank - 1)]
    return {
        "handle": sanitize(spec.name) + "_tmap",
        "dtype": CU_TENSOR_MAP_DATA_TYPE[spec.dtype],
        "tensor_rank": rank,
        "global_dim": list(spec.shape),
        "global_stride": global_stride_bytes,
        "box_dim": list(spec.box),
        "element_strides": list(spec.element_strides_or_default()),
        "interleave": spec.interleave,
        "swizzle": spec.swizzle,
        "l2_promotion": spec.l2_promotion,
        "oob_fill": spec.oob_fill,
    }


def sanitize(name: str) -> str:
    """Turn a scenario label into a valid C identifier fragment."""
    out = []
    for ch in name:
        out.append(ch if ch.isalnum() and ch.isascii() else "_")
    return "".join(out).strip("_")


def render_c(desc: dict) -> str:
    """Render the same shape of code that wrapper.py:111-131 emits."""
    gdim = ", ".join(str(d) for d in desc["global_dim"])
    gstride = ", ".join(str(s) for s in desc["global_stride"])
    box = ", ".join(str(d) for d in desc["box_dim"])
    estride = ", ".join(str(s) for s in desc["element_strides"])
    return (
        f"  CUtensorMap {desc['handle']};\n"
        f"  ... cuTensorMapEncodeTiled(&{desc['handle']}, "
        f"/*dtype=*/{desc['dtype']}, /*rank=*/{desc['tensor_rank']},\n"
        f"      /*gaddr=*/A, /*gdim=*/{{{gdim}}}, /*gstride=*/{{{gstride}}},\n"
        f"      /*box=*/{{{box}}}, /*estride=*/{{{estride}}},\n"
        f"      /*interleave=*/{desc['interleave']}, /*swizzle=*/{desc['swizzle']},\n"
        f"      /*l2=*/{desc['l2_promotion']}, /*oob=*/{desc['oob_fill']});"
    )


def report(spec: TensorSpec) -> str:
    try:
        desc = build_descriptor(spec)
    except Rejected as exc:
        return f"[rejected] {spec.name}: {exc}"
    lines = [
        f"[ok]   {spec.name}  dtype={spec.dtype}  gshape={tuple(spec.shape)}  box={tuple(spec.box)}",
        f"       globalDim      = {desc['global_dim']}",
        f"       globalStride   = {desc['global_stride']}   # 单位是字节,最内层被丢掉",
        f"       boxDim         = {desc['box_dim']}",
        f"       elementStrides = {desc['element_strides']}",
        render_c(desc),
    ]
    strides = spec.strides_or_default()
    for i, stride in enumerate(strides[:-1]):
        lines.append(f"       globalStride[{i}] bytes = {stride * spec.elem_bits // 8} "
                     f"(16 的倍数: {(stride * spec.elem_bits // 8) % 16 == 0})")
    return "\n".join(lines)


def main() -> int:
    specs = [
        TensorSpec("2D K-major f16 tile", "f16", 16, (4096, 4096), (128, 64)),
        TensorSpec("2D N-major f16 tile", "f16", 16, (4096, 4096), (64, 128)),
        TensorSpec("1D contiguous f16 row", "f16", 16, (8192,), (256,)),
        TensorSpec("2D bf16 tile, box 256x64", "bf16", 16, (8192, 8192), (256, 64)),
        # rejections
        TensorSpec("box last dim 30", "f16", 16, (4096, 4096), (128, 30)),
        TensorSpec("stride-2 innermost", "f16", 16, (4096, 4096), (128, 64),
                   strides=(8192, 2)),
        TensorSpec("stride 12 elements (24 B)", "f16", 16, (4096, 4096), (128, 64),
                   strides=(12, 1)),
        TensorSpec("box dim 512 > 256", "f16", 16, (8192, 8192), (512, 64)),
        TensorSpec("fp8 tile, last dim 32", "f8", 8, (4096, 4096), (128, 32)),
    ]
    print("=== TMA 描述符参数与约束(模型) ===")
    for spec in specs:
        print(report(spec))
        print()

    ok = True
    def expect_reject(spec, needle):
        nonlocal ok
        text = report(spec)
        if not text.startswith("[rejected]") or needle not in text:
            print(f"SELF-CHECK FAILED: {spec.name} should be rejected with {needle!r}")
            ok = False

    expect_reject(specs[4], "不是 128 的倍数")
    expect_reject(specs[5], "不是 1")
    expect_reject(specs[6], "不是 16 的倍数")
    expect_reject(specs[7], "超过上限")
    if not report(specs[0]).startswith("[ok]"):
        print("SELF-CHECK FAILED: the plain 2D f16 tile should be accepted")
        ok = False
    print("self-check:", "ok" if ok else "FAILED")
    return 0 if ok else 1


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

← 浏览本章全部代码