跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 27 章 低精度、量化与数值正确性

/code/27-low-precision-quantization/

06-tilelang-audit/tilelang_audit.py16700 字节
#!/usr/bin/env python3
"""第 27 章 · 示例 6:把本章的源码结论变成可复现的检查(纯标准库)。

本章引用了大量具体的文件与语义(哪些 dtype 存在、块缩放的粒度是多少、scale 怎么
打包、`T.gemm_blockscaled` 的参数叫什么)。这些结论如果只写在正文里,读者无法
验证;这个脚本把它们变成一组「在锁定版本的工作树上跑一遍」的断言。

每条检查包含:
    id       正文里的引用编号(与 references.md 一致)
    what     这条结论在讲什么
    相对路径  相对 _vendor 或 corpus 根目录
    必须出现  若干子串(全部命中才算通过)
    必须不出现 可选,命中即失败(用于负向断言)

**这是启发式文本检查,不是编译器。** 命中只说明「这句话在锁定版本里确实存在」,
不说明它的语义被正确理解了。

用法(在 code/06-tilelang-audit/ 目录下):

    python3 tilelang_audit.py
    python3 tilelang_audit.py --vendor ../../../../_vendor/tilelang-v0.1.15 \
        --ascend ../../../../_vendor/tilelang-ascend-v0.1.2.000 \
        --corpus ../../../../../tilelang-corpus

找不到任一根目录时,相关检查打印 `skipped` 并以 0 退出。

作者实际做到的验证级别:**CPU 逻辑验证**(在写作机的工作树上实际运行过,
全部检查通过;输出见同目录 README.md)。
"""

from __future__ import annotations

import argparse
from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class Check:
    cid: str
    what: str
    root: str  # "vendor" | "ascend" | "corpus"
    path: str
    must: list[str] = field(default_factory=list)
    must_not: list[str] = field(default_factory=list)
    need_missing: bool = False  # True 表示「这个路径必须不存在」


CHECKS: list[Check] = [
    # ---- dtype 系统 ------------------------------------------------------- #
    Check(
        "S1",
        "tilelang/dtypes.py 只是 language/dtypes.py 的再导出",
        "vendor",
        "tilelang/dtypes.py",
        ["from tilelang.language.dtypes import *", "dtype, AnyDType, get_tvm_dtype"],
    ),
    Check(
        "S2",
        "语言面的 dtype 只认字符串名,且登记了 e4m3/e5m2/e8m0/int4",
        "vendor",
        "tilelang/language/dtypes.py",
        ['"int4": "Int4"', '"float8_e4m3fn": "Float8E4M3FN"', '"float8_e8m0fnu": "Float8E8M0FNU"'],
    ),
    Check(
        "S3",
        "torch 的扩展 dtype 列表里有 e4m3fn/e5m2/e8m0fnu/float4_e2m1fnx2",
        "vendor",
        "tilelang/language/dtypes.py",
        ['("float8_e4m3fn",)', '("float8_e8m0fnu",)', '("float4_e2m1fnx2",)'],
    ),
    Check(
        "S4",
        "int4 落到 torch 时用 int8 存储(torch 没有 int4 dtype)",
        "vendor",
        "tilelang/language/dtypes.py",
        ['logger.debug("torch doesn\'t support int4, using int8 as storage dtype.")'],
    ),
    Check(
        "S5",
        "e8m0fnu 需要 torch >= 2.8(版本门槛写在断言里)",
        "vendor",
        "tilelang/language/dtypes.py",
        ['hasattr(torch, "float8_e8m0fnu")', "torch.float8_e8m0fnu is not supported in this version of torch. Please upgrade torch >= 2.8.0"],
    ),
    Check(
        "S6",
        "FP4 有 packed(4 bit)与 unpacked(8 bit 槽位)两种存储,用 type_code 区分",
        "vendor",
        "tilelang/language/dtypes.py",
        ["_FLOAT4_E2M1FN_TYPE_CODE = 17", "_FLOAT4_E2M1_UNPACKED_TYPE_CODE = 131"],
    ),
    Check(
        "S7",
        "混合 dtype 的 GEMM 只允许 f8f6f4 家族",
        "vendor",
        "tilelang/language/dtypes.py",
        ["def is_f8f6f4_family", "Mixed-dtype GEMM only supports f8f6f4 family operands currently"],
    ),
    # ---- fp8 ------------------------------------------------------------- #
    Check(
        "S8",
        "determine_fp8_type:CUDA 用 e4m3fn/e5m2,ROCm 默认 FNUZ、gfx950 例外",
        "vendor",
        "tilelang/language/fp8.py",
        ["def determine_fp8_type", 'T.float8_e4m3fn if fp8_format == "e4m3" else T.float8_e5m2', 'gcn_arch.startswith("gfx950")'],
    ),
    Check(
        "S9",
        "fp8 的负零有一个专门的清理函数",
        "vendor",
        "tilelang/utils/tensor.py",
        ["def fp8_remove_negative_zeros_", "Input tensor must be of float8 dtype"],
    ),
    # ---- 示例:fp8 / int4 ------------------------------------------------ #
    Check(
        "S10",
        "gemm_fp8 示例:fp32 累加、block_K=64、用相似度而非逐元素比较",
        "vendor",
        "examples/gemm_fp8/example_tilelang_gemm_fp8.py",
        ["accum_dtype=T.float32", "def calc_diff", "assert diff < 1e-3"],
    ),
    Check(
        "S11",
        "gemm_int4 示例:T.int4 张量 + T.int32 累加 + 精确相等断言",
        "vendor",
        "examples/gemm_int4/example_tilelang_gemm_int4.py",
        ["T.Tensor((M, K), T.int4)", "C: T.Tensor((M, N), T.int32)", "torch.testing.assert_close(C.cpu(), ref_c, rtol=0, atol=0)"],
    ),
    Check(
        "S12",
        "int4 打包:两个 int8 的低 4 位合成一个字节",
        "vendor",
        "examples/gemm_int4/example_tilelang_gemm_int4.py",
        ["packed = (tensor_i16[..., ::2] & 0x0F) | ((tensor_i16[..., 1::2] & 0x0F) << 4)"],
    ),
    Check(
        "S13",
        "dequantize GEMM:搬入时用位运算解码再送进 T.gemm",
        "vendor",
        "examples/dequantize_gemm/README.md",
        ["_tir_packed_to_unsigned_convert", "B_dequantize_local[i, j] = ", "T.gemm(B_dequantize_local, A_shared, Ct_local, transpose_B=True)"],
    ),
    Check(
        "S14",
        "w4a8 的 int4->int8 符号扩展技巧(先左移 4 位再算术右移 4 位)",
        "vendor",
        "examples/dequantize_gemm/example_dequant_gemm_w4a8.py",
        ["i8_shifted = tirx.reinterpret(T.int8, i4 << tirx.const(4, T.uint8))", "i8 = i8_shifted >> tirx.const(4, T.int8)"],
    ),
    Check(
        "S15",
        "fine-grained 版本按 128 位事务决定每线程处理多少元素",
        "vendor",
        "examples/dequantize_gemm/example_dequant_gemm_fine_grained.py",
        ["MAX_TRANSACTION_SIZE_IN_BITS = 128", "local_size = MAX_TRANSACTION_SIZE_IN_BITS // DataType(in_dtype).bits"],
    ),
    # ---- block-scaled:示例与 API --------------------------------------- #
    Check(
        "S16",
        "SM100 MXFP8 示例:blockscale (1,1,128)、uint32 打包、sf_load_period 公式",
        "vendor",
        "examples/blockscaled_gemm_sm100/gemm_mxfp8_blockscaled_1d1d.py",
        [
            "# Blockscale size: (M, N, K) = (1, 1, 128)",
            "sf_load_period = sf_granularity_k * 4 // block_K",
            "sf_k_groups = T.ceildiv(T.ceildiv(K, sf_granularity_k), 4)",
            "SFA: T.Tensor[[sf_k_groups * M], T.uint32]",
        ],
    ),
    Check(
        "S17",
        "打包公式:4 个字节拼成一个 uint32,再转置成 group-major 扁平数组",
        "vendor",
        "examples/blockscaled_gemm_sm100/gemm_mxfp8_blockscaled_1d1d.py",
        ["packed = (words[:, 0::4] | (words[:, 1::4] << 8) | (words[:, 2::4] << 16) | (words[:, 3::4] << 24))", "return packed.T.contiguous().reshape(-1)"],
    ),
    Check(
        "S18",
        "E8M0 由 fp32 的指数位直接得到,且向上取整到 2 的幂",
        "vendor",
        "examples/blockscaled_gemm_sm100/gemm_mxfp8_blockscaled_1d1d.py",
        ["def ceil_to_ue8m0(x):", "exp = ((bits >> 23) & 0xFF) + (bits & 0x7FFFFF).ne(0).to(torch.int32)", "sf = ceil_to_ue8m0(x_amax / 448.0)"],
    ),
    Check(
        "S19",
        "scale 填充值是 127(即 2^0 = 1),不是 0",
        "vendor",
        "examples/blockscaled_gemm_sm100/gemm_mxfp8_blockscaled_1d1d.py",
        ["torch.full((mn, sf_k_padded), 127, device=x.device, dtype=torch.uint8)"],
    ),
    Check(
        "S20",
        "参考实现按 sf_granularity_k 分块,逐块乘 scale",
        "vendor",
        "examples/blockscaled_gemm_sm100/gemm_mxfp8_blockscaled_1d1d.py",
        ["def blockscaled_gemm_ref(", "a_block = a_f32[:, k_start:k_end] * sfa_scales[:, bi : bi + 1]"],
    ),
    Check(
        "S21",
        "2CTA 变体把 sf_granularity_k 固定为 128",
        "vendor",
        "examples/blockscaled_gemm_sm100/gemm_mxfp8_blockscaled_1d1d.py",
        ["assert sf_granularity_k == 128"],
    ),
    Check(
        "S22",
        "T.gemm_blockscaled 的签名带 k_start / sf_a_granularity_k / sf_b_granularity_k",
        "vendor",
        "tilelang/language/gemm_op.py",
        ["def gemm_blockscaled(", "k_start: int | tirx.PrimExpr", "sf_a_granularity_k: int", "sf_b_granularity_k: int"],
    ),
    Check(
        "S23",
        "T.tcgen05_gemm_blockscaled:A/B 是 FP8/FP6/FP4,SFA/SFB 是 TMEM 里的 E8M0",
        "vendor",
        "tilelang/cuda/language/gemm_op.py",
        ["def tcgen05_gemm_blockscaled(", "SFA/SFB are E8M0 scale factors already", "resident in tensor memory"],
    ),
    Check(
        "S24",
        "SF ID 的推导公式:(k_start / granularity) % 4",
        "vendor",
        "tilelang/cuda/intrinsics/macro/tcgen05_macro_generator.py",
        ["runtime_sf_a_id = ((_sf_k_start + ki * micro_size_k) // int(sf_a_granularity_k)) % 4"],
    ),
    Check(
        "S25",
        "tcgen05_cp_warpx4:每个 128 word 的块映射到 4 个 TMEM 列",
        "vendor",
        "tilelang/language/builtin.py",
        ["def tcgen05_cp_warpx4(", "each 128-word chunk maps to", "4 TMEM columns"],
    ),
    Check(
        "S26",
        "没有块缩放实现的后端会直接编译失败,而不是退化成普通 GEMM",
        "vendor",
        "src/op/gemm_blockscaled.cc",
        ["a dense GEMM lowering would silently drop the SFA/SFB", "scale factors."],
    ),
    Check(
        "S27",
        "MX 指令描述符里 e4m3=0 / e5m2=1 / e2m1=5",
        "vendor",
        "src/op/tcgen5_meta.h",
        ["return 0u; // E4M3", "return 1u; // E5M2", "return 5u; // E2M1"],
    ),
    Check(
        "S28",
        "TCGEN5MMA 的 fp16/bf16 路径要求 fp32 累加器,且 K 是 16 的倍数",
        "vendor",
        "src/op/tcgen5_meta.h",
        ["if ((ab_dtype.is_bfloat16() || ab_dtype.is_float16()) &&", "(c_dtype.is_float() && c_dtype.bits() == 32)) {", "if (K % 16 != 0)"],
    ),
    Check(
        "S29",
        "TCGEN5MMA 的 f8f6f4 家族要求 K 是 32 的倍数,累加器可以是 fp32 **或** fp16",
        "vendor",
        "src/op/tcgen5_meta.h",
        [
            "} else if ((ab_dtype.is_float8() || ab_dtype.is_float6_e2m3fn() ||",
            "ab_dtype.is_float6_e3m2fn() || ab_dtype.is_float4()) &&",
            "(c_dtype.is_float16() && c_dtype.bits() == 16))) {",
            "if (K % 32 != 0)",
        ],
    ),
    Check(
        "S30",
        "WGMMA 的 fp8 路径要求 A 不转置、B 转置,且 K 是 32 的倍数",
        "vendor",
        "src/cuda/op/gemm.cc",
        ["if (op.a_->dtype.is_float8() && op.b_->dtype.is_float8())", "return (!op.transA_) && op.transB_ && op.k_ % 32 == 0;"],
    ),
    # ---- 昇腾 ------------------------------------------------------------ #
    Check(
        "A1",
        "昇腾方言的 T.gemm_blockscaled 把 MX 粒度写死为 32",
        "vendor",
        "tilelang/ascend/language/gemm_op.py",
        ['annotations["sf_a_granularity_k"] = 32', 'annotations["sf_b_granularity_k"] = 32', "MX scales cover 32 K elements per factor"],
    ),
    Check(
        "A2",
        "昇腾侧 fp8 的 dtype 名是 e4m3_float8 / e5m2_float8",
        "ascend",
        "tilelang/language/ascend.py",
        ['"e4m3_float8": "float8_e4m3_t"'],
    ),
    Check(
        "A3",
        "昇腾 quant GEMM 示例:int8 输入、int32 累加、epilogue 里乘 scale",
        "ascend",
        "examples/quant_batch_matmul/example_quant_batch_matmul.py",
        ["T.alloc_L0C([block_M, block_N], accum_dtype)", "c_scale[bm_v, bn_v] *= scale_ub[bn_v]"],
    ),
    Check(
        "A4",
        "昇腾 quant GEMM 的对拍判据是 rtol=1e-2 / atol=1e-2",
        "ascend",
        "examples/quant_batch_matmul/example_quant_batch_matmul.py",
        ["torch.testing.assert_close(C.cpu(), ref_C.cpu(), rtol=1e-2, atol=1e-2)"],
    ),
    Check(
        "A5",
        "昇腾 FP8 GEMM 需要 A5(Ascend 950)的 Cube 核",
        "ascend",
        "examples/gemm/example_gemm_fp8_pto.py",
        ["FP8 GEMM (TMATMUL accepting float8_e4m3 / float8_e5m2) requires the A5"],
    ),
    # ---- corpus:CATLASS 与 PTX ------------------------------------------ #
    Check(
        "R1",
        "CATLASS 的 MX 粒度常量是 32",
        "corpus",
        "06-huawei-ascend/raw/catlass/include/catlass/catlass.hpp",
        ["constexpr uint32_t MX_SCALE_GROUP_NUM = 32;", "Data count for one MX-scale factor per group"],
    ),
    Check(
        "R2",
        "CATLASS MX 模板以 L1_SCALE_FACTOR_K 参数化 L1 上的 scale 深度",
        "corpus",
        "06-huawei-ascend/raw/catlass/include/catlass/gemm/block/block_mmad_mx_tla.hpp",
        ["static constexpr uint32_t L1_SCALE_FACTOR_K = DispatchPolicy::L1_SCALE_FACTOR_K;"],
    ),
    Check(
        "R3",
        "A8W4 MX 示例用的 scale 类型是 float8_e8m0_t,公式为 (MxScaleA*A)*(MxScaleB*B)",
        "corpus",
        "06-huawei-ascend/raw/catlass/examples/59_ascend950_a8w4_mx_matmul/README.md",
        ["float8_e8m0", "C = (MxScaleA x A) * (MxScaleB x B) + Bias"],
    ),
    Check(
        "R4",
        "CATLASS MXFP8 FlashAttention:block size=32、e8m0 scale、在 L1→L0 阶段反量化",
        "corpus",
        "06-huawei-ascend/raw/catlass/experimental/attention/ascend950_fp8_mx_flash_attention_infer/mxfp8_flash_attention_infer.md",
        ["block size=32", "float8_e8m0_t", "在 Cube 矩阵乘的 L1→L0 阶段完成反量化"],
    ),
    Check(
        "R5",
        "PTX:tcgen05.mma 的块缩放把 scale 放在 Tensor Memory,SFB 要复制到 32 个 lane 分区",
        "corpus",
        "05-nvidia-backend/raw/nvidia-ptx-isa.md",
        [
            "Block Scaling for tcgen05.mma",
            "where `scale_A` and `scale_B` are matrices residing in Tensor Memory.",
            "Scale factor for `B` matrix needs to be duplicated to all 32 lane partitions of tensor memory.",
        ],
    ),
    Check(
        "R6",
        "PTX:mxf8f6f4 的 scale 类型是 ue8m0,SFA_ID 选的是 Tensor Memory 里的字节子列",
        "corpus",
        "05-nvidia-backend/raw/nvidia-ptx-isa.md",
        [
            ".kind::mxf8f6f4 | .e4m3 , .e5m2",
            ".kind::mxf8f6f4 | E4M3, E5M2, E2M3",
            "SFA_ID specifies the byte offset in the",
            "Tensor Memory word",
        ],
    ),
    Check(
        "R7",
        "PTX:e4m3 没有 inf、NaN 只有 0x7f/0xff;ue8m0 是「8 位指数 + 0 位尾数」",
        "corpus",
        "05-nvidia-backend/raw/nvidia-ptx-isa.md",
        ["The `e4m3` encoding does not support infinity and `NaN` values are limited to", "This data format is an 8-bit unsigned floating-point format with 8 bits for exponent and 0 bits for", "`ue8m0` encoding does not support infinity"],
    ),
    # ---- 版本边界(负向断言) ------------------------------------------- #
    Check(
        "N1",
        "v0.1.15 主干里没有 tilelang/quantize/ 包(只在示例目录下)",
        "vendor",
        "tilelang/quantize",
        need_missing=True,
    ),
    Check(
        "N2",
        "v0.1.15 主干的 examples/ 下确实有 dequantize_gemm/quantize/(负向断言的对照)",
        "vendor",
        "examples/dequantize_gemm/quantize/nvfp4.py",
        ["def blockscaled_chunk_kmajor_word_offset"],
    ),
]


def find_root(start: Path, name: str) -> Path | None:
    """从 start 向上找 `<dir>/name` 或 `<dir>/_vendor/name`。"""

    current = start.resolve()
    for _ in range(12):
        for candidate in (current / name, current / "_vendor" / name):
            if candidate.exists():
                return candidate
        if current.parent == current:
            break
        current = current.parent
    return None


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--vendor")
    parser.add_argument("--ascend")
    parser.add_argument("--corpus")
    args = parser.parse_args()

    here = Path(__file__).resolve().parent
    roots = {
        "vendor": Path(args.vendor) if args.vendor else find_root(here, "tilelang-v0.1.15"),
        "ascend": Path(args.ascend) if args.ascend else find_root(here, "tilelang-ascend-v0.1.2.000"),
        "corpus": Path(args.corpus) if args.corpus else find_root(here, "tilelang-corpus"),
    }
    for name, path in roots.items():
        print(f"root[{name}] = {path if path is not None else 'NOT FOUND'}")
    print()

    passed = failed = skipped = 0
    for check in CHECKS:
        root = roots[check.root]
        if root is None:
            print(f"skipped  {check.cid:<4}{check.what}(找不到 {check.root} 根目录)")
            skipped += 1
            continue
        target = root / check.path
        if check.need_missing:
            if target.exists():
                print(f"FAIL     {check.cid:<4}{check.what}({target} 存在,但期望不存在)")
                failed += 1
            else:
                print(f"ok       {check.cid:<4}{check.what}")
                passed += 1
            continue
        if not target.is_file():
            print(f"FAIL     {check.cid:<4}{check.what}(文件不存在:{target})")
            failed += 1
            continue
        text = target.read_text(encoding="utf-8", errors="ignore")
        missing = [needle for needle in check.must if needle not in text]
        forbidden = [needle for needle in check.must_not if needle in text]
        if missing or forbidden:
            detail = []
            if missing:
                detail.append("未命中:" + " | ".join(repr(m) for m in missing))
            if forbidden:
                detail.append("不应出现:" + " | ".join(repr(m) for m in forbidden))
            print(f"FAIL     {check.cid:<4}{check.what}({'; '.join(detail)})")
            failed += 1
        else:
            print(f"ok       {check.cid:<4}{check.what}")
            passed += 1

    print()
    print(f"通过 {passed} / 失败 {failed} / 跳过 {skipped}(共 {len(CHECKS)} 条)")
    return 1 if failed else 0


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

← 浏览本章全部代码