跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

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

/code/14-operators-gemm/

04-low-precision/low_precision_gemm.py5585 字节
#!/usr/bin/env python3
"""第 14 章第 5 节:四类低精度 GEMM 的**结构**与调用方式。

  gemm_fp8          e4m3 / e5m2,fp32 累加
  gemm_int4         int4 打包输入,int32 累加
  gemm_blockscaled  fp8 + SFA/SFB,scale 按 K 块施加
  dequantize 的搬运-反量化模式见 README(它没有独立的 T.gemm 变体)

数值正确性(误差传播、舍入、scale 表示)留给第 27 章;这里只讲结构。

运行前提:NVIDIA GPU + CUDA + PyTorch(CUDA) + TileLang v0.1.15。
fp8 需要 sm89+;block-scaled 的公开示例跑在 sm100 上(本文件不依赖它,
只用通用接口,能否编译取决于后端是否支持该 dtype 组合)。
写作机上没有 GPU,本文件只做过静态核对(py_compile + API 比对),**未运行**。
"""

from __future__ import annotations

import tilelang
import tilelang.language as T
from tilelang.language.fp8 import determine_fp8_type


@tilelang.jit
def gemm_fp8(A, B, block_M, block_N, block_K, dtype, accum_dtype=T.float32):
    """fp8:结构上和 fp16 一模一样,只是 dtype 变了、K 对齐更严。"""
    M, N, K = T.const("M, N, K")

    A: T.Tensor((M, K), dtype)
    B: T.Tensor((N, K), 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_N, block_K), dtype)
        C_local = T.alloc_fragment((block_M, block_N), accum_dtype)

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

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

    return C


def matmul_int4(M, N, K, block_M, block_N, block_K, threads=128):
    """int4:A/B 声明成 T.int4,累加器是 T.int32;输入需按两条 nibble 打包。"""

    @T.prim_func
    def main(
        A: T.Tensor((M, K), T.int4),
        B: T.Tensor((N, K), T.int4),
        C: T.Tensor((M, N), T.int32),
    ):
        with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=threads) as (bx, by):
            A_shared = T.alloc_shared((block_M, block_K), T.int4)
            B_shared = T.alloc_shared((block_N, block_K), T.int4)
            C_local = T.alloc_fragment((block_M, block_N), T.int32)

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

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

    return main


def pack_int4(tensor):
    """把逻辑 int4(用 int8 表示)打包成每字节两个 nibble,与官方示例一致。"""
    import torch  # 只在这个 helper 里用到 torch

    if tensor.dtype != torch.int8:
        raise TypeError("期望 torch.int8 表示的逻辑 int4 张量")
    if tensor.ndim == 0 or tensor.shape[-1] % 2 != 0:
        raise ValueError("逻辑 int4 张量的最后一维必须是偶数")
    t16 = tensor.to(torch.int16)
    packed = (t16[..., ::2] & 0x0F) | ((t16[..., 1::2] & 0x0F) << 4)
    return packed.to(torch.int8).contiguous()


@tilelang.jit
def gemm_blockscaled(A, B, SFA, SFB, block_M, block_N, block_K, sf_granularity_k,
                     in_dtype, out_dtype, accum_dtype=T.float32, num_stages=3):
    """block-scaled:多一对 SFA/SFB,scale 沿 K 以块为单位施加。

    参数名与语义对照 `T.gemm_blockscaled(A, B, C, SFA, SFB, k_start=...,
    sf_a_granularity_k=..., sf_b_granularity_k=...)`。
    这里只保留结构:真实实现的 scale 打包方式与粒度由后端决定
    (sm100 的 MXFP8 用 128,昇腾 MX 用 32)。
    """
    M, N, K = T.const("M, N, K")

    A: T.Tensor((M, K), in_dtype)
    B: T.Tensor((N, K), in_dtype)
    C = T.empty((M, N), out_dtype)
    SFA: T.Tensor((M, T.ceildiv(K, sf_granularity_k)), accum_dtype)
    SFB: T.Tensor((N, T.ceildiv(K, sf_granularity_k)), accum_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), in_dtype)
        B_shared = T.alloc_shared((block_N, block_K), in_dtype)
        SFA_shared = T.alloc_shared((block_M, block_K // sf_granularity_k), accum_dtype)
        SFB_shared = T.alloc_shared((block_N, block_K // sf_granularity_k), accum_dtype)
        C_local = T.alloc_fragment((block_M, block_N), accum_dtype)

        T.clear(C_local)
        for k in T.Pipelined(T.ceildiv(K, block_K), num_stages=num_stages):
            T.copy(A[by * block_M, k * block_K], A_shared)
            T.copy(B[bx * block_N, k * block_K], B_shared)
            T.copy(SFA[by * block_M, k * (block_K // sf_granularity_k)], SFA_shared)
            T.copy(SFB[bx * block_N, k * (block_K // sf_granularity_k)], SFB_shared)
            T.gemm_blockscaled(
                A_shared, B_shared, C_local, SFA_shared, SFB_shared,
                transpose_B=True, k_start=k * block_K,
                sf_a_granularity_k=sf_granularity_k,
                sf_b_granularity_k=sf_granularity_k,
            )

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

    return C


def main() -> int:
    import torch

    m, n, k = 1024, 1024, 1024

    # fp8:e4m3
    dtype = determine_fp8_type()
    torch_dtype = T.dtype(dtype).as_torch()
    a = torch.randn(m, k, dtype=torch.float16, device="cuda").to(torch_dtype)
    b = torch.randn(n, k, dtype=torch.float16, device="cuda").to(torch_dtype)
    c = gemm_fp8(a, b, 128, 128, 64, dtype)
    ref = (a.half() @ b.half().T).to(torch_dtype)
    print("fp8 dtype:", dtype, "| 结构检查完成(未评估数值误差,见第 27 章)")
    _ = ref
    _ = c

    # int4:打包
    logical = torch.randint(-8, 8, (m, k), dtype=torch.int8, device="cuda")
    packed = pack_int4(logical)
    print("int4 打包后形状:", tuple(packed.shape), "(最后一维减半)")
    return 0


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

← 浏览本章全部代码