跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 18 章 端到端 II:推理与部署

/code/18-e2e-inference-serving/

07-tilelang-moe-and-quant/skeletons.py9995 字节
#!/usr/bin/env python3
"""第 18 章 · fused MoE 与量化 GEMM 的 TileLang 骨架。

验证级别:**仅静态核对**(本机没有 GPU / NPU,从未编译或运行)。

两个骨架:

1. `moe_routed_kernel`:MoE 的路由专家部分,结构对照
   `_vendor/tilelang-v0.1.15/examples/fusedmoe/example_fusedmoe_tilelang.py`
   的 `moe_forward_tilelang_routed`(`:92-226`)。重点是 **dispatch 契约**:
   kernel 不自己做 sort/bincount,而是从 host 拿四张表,按 block 反查自己
   该处理哪个专家的哪一段行。
2. `w4a8_dequant_gemm`:权重 int4、激活 int8 的反量化 GEMM,结构对照
   `examples/dequantize_gemm/example_dequant_gemm_w4a8.py:86-148`。
   重点是 **反量化发生在什么时候**:把打包的 int4 权重搬进片上,
   在 fragment 里解包并转成计算精度,再喂给 `T.gemm`。

量化推理的三种接法(本章第 5 节)在这里都有落点:

* **weight-only**(W4A16/W8A16):权重解包,激活保持高精度 —— 骨架 2 的
  `in_dtype="int8"` 换成 `"float16"` 就是这一路;
* **weight + activation**(W4A8/W8A8):两侧都量化,kernel 内各自反量化;
* **block-scaled**(MXFP4/MXFP8):`T.gemm_blockscaled(..., SFA, SFB, ...)`
  (`tilelang/language/gemm_op.py:275`),scale 的粒度是新的一个维度。
"""

from __future__ import annotations

import tilelang
import tilelang.language as T
from tvm import tirx


def _tir_u8_to_i4_to_i8(nbit: int, val, pos, dtype: str):
    """把 uint8 里的一个 4 bit 字段解成 int8(带符号扩展)。

    与 `example_dequant_gemm_w4a8.py:10-24` 的 `_tir_u8_to_i4_to_i8` 同构。
    """
    assert nbit == 4 and dtype == T.int8 and val.dtype == T.uint8
    mask = tirx.const((1 << nbit) - 1, T.uint8)
    i4 = (val >> (pos.astype(T.uint8) * tirx.const(nbit, T.uint8))) & mask
    i8_shifted = tirx.reinterpret(T.int8, i4 << tirx.const(4, T.uint8))
    return i8_shifted >> tirx.const(4, T.int8)


@tilelang.jit(pass_configs={"tl.disable_warp_specialized": True})
def moe_routed_kernel(
    d_hidden: int,
    d_expert: int,
    n_routed_experts: int,
    group_sum: int,
    block_token: int = 128,
    block_dhidden: int = 128,
    block_dexpert: int = 128,
    threads: int = 256,
    num_stages: int = 1,
    dtype: str = "float16",
):
    """路由专家的 grouped GEMM(gate/up → SiLU → down → 加权写回)。

    ``group_sum`` 是 (token, expert) 对的总数,也就是
    ``batch * seq_len * n_experts_per_token``——它是**编译期常量**,
    decode 每步都在变,所以真实框架要按 batch 分桶编译(第 7 章的 JIT 缓存)。
    """
    group_count = n_routed_experts
    m_blocks = (group_sum + block_token - 1) // block_token + group_count
    accum_dtype = "float32"
    scale = 1.44269504  # log2(e)

    input_shape = (group_sum, d_hidden)
    gate_shape = (n_routed_experts, d_expert, d_hidden)
    up_shape = (n_routed_experts, d_expert, d_hidden)
    down_shape = (n_routed_experts, d_hidden, d_expert)

    @T.prim_func
    def moe_routed(
        X: T.Tensor(input_shape, dtype),
        W_gate: T.Tensor(gate_shape, dtype),
        W_up: T.Tensor(up_shape, dtype),
        W_down: T.Tensor(down_shape, dtype),
        RowWeights: T.Tensor((group_sum,), dtype),
        group_sizes: T.Tensor((group_count,), T.int32),
        group_offsets: T.Tensor((group_count,), T.int32),
        group_padded_offsets: T.Tensor((group_count,), T.int32),
        group_idx_for_bx: T.Tensor((m_blocks,), T.int32),
        UpLogits: T.Tensor((group_sum, d_expert), dtype),
        Output: T.Tensor(input_shape, dtype),
    ):
        # ---- gate + up,融合 SiLU 与逐元素乘 ----
        with T.Kernel(m_blocks, T.ceildiv(d_expert, block_dexpert),
                      threads=threads) as (bx, by):
            x_shared = T.alloc_fragment((block_token, block_dhidden), dtype)
            w_gate_shared = T.alloc_shared((block_dexpert, block_dhidden), dtype)
            w_up_shared = T.alloc_shared((block_dexpert, block_dhidden), dtype)
            gate_local = T.alloc_fragment((block_token, block_dexpert), accum_dtype)
            up_local = T.alloc_fragment((block_token, block_dexpert), accum_dtype)

            # 这是 dispatch 契约的 kernel 侧:从三张表还原出「我在哪、处理谁」
            cur_group = group_idx_for_bx[bx]
            cur_size = group_sizes[cur_group]
            m_start_padded = bx * block_token
            m_start = (m_start_padded - group_padded_offsets[cur_group]
                       + group_offsets[cur_group])
            actual_rows = T.max(0, T.min(block_token,
                                         cur_size - (m_start_padded
                                                     - group_padded_offsets[cur_group])))
            T.clear(gate_local)
            T.clear(up_local)
            for k in T.Pipelined(T.ceildiv(d_hidden, block_dhidden),
                                 num_stages=num_stages):
                T.copy(X[m_start, k * block_dhidden], x_shared)
                T.copy(W_gate[cur_group, by * block_dexpert,
                              k * block_dhidden], w_gate_shared)
                T.copy(W_up[cur_group, by * block_dexpert,
                            k * block_dhidden], w_up_shared)
                T.gemm(x_shared, w_gate_shared, gate_local, transpose_B=True)
                T.gemm(x_shared, w_up_shared, up_local, transpose_B=True)
            for i, j in T.Parallel(block_token, block_dexpert):
                # SiLU(gate) * up,同一个 kernel 里做完,省一趟 HBM
                gate_local[i, j] = gate_local[i, j] * (
                    1.0 / (1.0 + T.exp2(-gate_local[i, j] * scale)))
                up_local[i, j] = up_local[i, j] * gate_local[i, j]
            for i, j in T.Parallel(block_token, block_dexpert):
                if i < actual_rows:
                    UpLogits[m_start + i, by * block_dexpert + j] = up_local[i, j]

        # ---- down + 按路由权重写回 ----
        with T.Kernel(m_blocks, T.ceildiv(d_hidden, block_dhidden),
                      threads=threads) as (bx, by):
            up_shared = T.alloc_fragment((block_token, block_dexpert), dtype)
            w_down_shared = T.alloc_shared((block_dhidden, block_dexpert), dtype)
            out_local = T.alloc_fragment((block_token, block_dhidden), accum_dtype)

            cur_group = group_idx_for_bx[bx]
            cur_size = group_sizes[cur_group]
            m_start_padded = bx * block_token
            m_start = (m_start_padded - group_padded_offsets[cur_group]
                       + group_offsets[cur_group])
            actual_rows = T.max(0, T.min(block_token,
                                         cur_size - (m_start_padded
                                                     - group_padded_offsets[cur_group])))
            T.clear(out_local)
            for k in T.Pipelined(T.ceildiv(d_expert, block_dexpert),
                                 num_stages=num_stages):
                T.copy(UpLogits[m_start, k * block_dexpert], up_shared)
                T.copy(W_down[cur_group, by * block_dhidden,
                              k * block_dexpert], w_down_shared)
                T.gemm(up_shared, w_down_shared, out_local, transpose_B=True)
            for i, j in T.Parallel(block_token, block_dhidden):
                if i < actual_rows:
                    # combine 的权重乘法放在这里,避免再开一个 kernel
                    Output[m_start + i, by * block_dhidden + j] = (
                        out_local[i, j] * RowWeights[m_start + i])

    return moe_routed


@tilelang.jit(out_idx=[2])
def w4a8_dequant_gemm(
    M: int,
    N: int,
    K: int,
    block_M: int = 128,
    block_N: int = 128,
    block_K: int = 256,
    num_stages: int = 2,
    threads: int = 128,
    num_bits: int = 4,
    in_dtype: str = "int8",
    out_dtype: str = "int32",
    accum_dtype: str = "int32",
):
    """W4A8:权重 int4(打包进 uint8)、激活 int8、int32 累加。

    ``Ct`` 的形状是 ``(N, M)``(与官方示例一致,转置写回)。
    """
    num_elems_per_byte = 8 // num_bits
    storage_dtype = "uint8"
    B_shape = (N, K // num_elems_per_byte)
    A_shared_shape = (block_M, block_K)
    B_shared_shape = (block_N, block_K // num_elems_per_byte)
    B_dequant_shape = (block_N, block_K)

    @T.prim_func
    def matmul_int8xint4(
        A: T.Tensor((M, K), in_dtype),
        B: T.Tensor(B_shape, storage_dtype),
        Ct: T.Tensor((N, M), out_dtype),
    ):
        with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M),
                      threads=threads) as (bx, by):
            A_shared = T.alloc_shared(A_shared_shape, in_dtype)
            B_shared = T.alloc_shared(B_shared_shape, storage_dtype)
            B_local = T.alloc_fragment(B_shared_shape, storage_dtype)
            B_dequant = T.alloc_fragment(B_dequant_shape, in_dtype)
            B_dequant_prev = T.alloc_fragment(B_dequant_shape, in_dtype)
            Ct_local = T.alloc_fragment((block_N, block_M), accum_dtype)
            Ct_shared = T.alloc_shared((block_N, block_M), out_dtype)

            T.annotate_layout(
                {B_shared: tilelang.layout.make_swizzled_layout(B_shared)}
            )
            T.clear(Ct_local)
            for k in T.Pipelined(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 // num_elems_per_byte], B_shared)
                T.copy(B_shared, B_local)
                # 反量化:解包 + 符号扩展,全部在片上做
                for i, j in T.Parallel(block_N, block_K):
                    B_dequant[i, j] = _tir_u8_to_i4_to_i8(
                        num_bits,
                        B_local[i, j // num_elems_per_byte],
                        j % num_elems_per_byte,
                        dtype=in_dtype,
                    )
                # 再拷一份给 GEMM 用:下轮循环会覆盖 B_dequant
                T.copy(B_dequant, B_dequant_prev)
                T.gemm(B_dequant_prev, A_shared, Ct_local, transpose_B=True)
            T.copy(Ct_local, Ct_shared)
            T.copy(Ct_shared, Ct[bx * block_N:(bx + 1) * block_N,
                                 by * block_M:(by + 1) * block_M])

    return matmul_int8xint4


#: 与官方示例 `example_dequant_gemm_w4a8.py` 的默认形状同量级。
SERVING_SHAPES = {
    # decode 单步:M = batch,N/K 是权重矩阵的一行
    "gemv_like": {"M": 1, "N": 3584, "K": 18944},
    # 小 batch:仍然是 memory-bound(见 03-decode-step-share 的账本)
    "small_batch": {"M": 32, "N": 3584, "K": 18944},
}


if __name__ == "__main__":
    raise SystemExit(
        "本文件只做静态核对:请在装有 NVIDIA GPU + TileLang v0.1.15 的机器上\n"
        "参照 code/07-tilelang-moe-and-quant/README.md 运行。"
    )

← 浏览本章全部代码