跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

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

/code/14-operators-gemm/

03-gemm-variants/gemm_variants.py10482 字节
#!/usr/bin/env python3
"""第 14 章第 4 节:GEMM 的四个变体(split-K / stream-K / grouped / batched)。

每个实现都直接对照官方 example:

  gemm_splitk    <- examples/gemm_splitk/example_tilelang_gemm_splitk.py
  gemm_streamk   <- examples/gemm_streamk/example_tilelang_gemm_streamk.py
  gemm_grouped   <- examples/grouped_gemm/example_grouped_gemm_fwd.py
  gemm_batched   <- 一维 grid 展平 batch(思路同昇腾 batch_gemm)

运行前提:NVIDIA GPU + CUDA + PyTorch(CUDA) + TileLang v0.1.15。
写作机上没有 GPU,本文件只做过静态核对(py_compile + API 比对),**未运行**。
"""

from __future__ import annotations

import math

import tilelang
import tilelang.language as T


# ---------------------------------------------------------------------------
# split-K:输出块数 < 核数时,把 K 切开换并行度
# ---------------------------------------------------------------------------
@tilelang.jit
def gemm_splitk(A, B, C, block_M, block_N, block_K, split_k,
                dtype=T.float16, accum_dtype=T.float32, out_dtype=T.float32):
    M, N, K = T.const("M, N, K")
    splitK = K // split_k

    A: T.Tensor((M, K), dtype)
    B: T.Tensor((K, N), dtype)
    C: T.Tensor((M, N), out_dtype)

    with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), split_k, threads=128) as (bx, by, bz):
        A_shared = T.alloc_shared((block_M, block_K), dtype)
        B_shared = T.alloc_shared((block_K, block_N), dtype)
        C_local = T.alloc_fragment((block_M, block_N), accum_dtype)

        T.clear(C_local)
        for ko in T.Pipelined(T.ceildiv(splitK, block_K), num_stages=0):
            T.copy(A[by * block_M, bz * splitK + ko * block_K], A_shared)
            T.copy(B[bz * splitK + ko * block_K, bx * block_N], B_shared)
            T.gemm(A_shared, B_shared, C_local)

        for i, j in T.Parallel(block_M, block_N):
            T.atomic_add(C[by * block_M + i, bx * block_N + j], C_local[i, j])


# ---------------------------------------------------------------------------
# stream-K:把 K 迭代平均分给固定数量的 program,只有被切开的块要原子合并
# ---------------------------------------------------------------------------
@tilelang.jit
def gemm_streamk(A, B, C, streamk_programs, iters_per_tile, full_tiles, partial_tiles,
                 sm_partition_factor, streamk_tiles, block_M, block_N, block_K,
                 dtypeAB=T.float16, dtypeC=T.float32,
                 accum_dtype=T.float32, num_stages=2, threads=64):
    """注意:`streamk_programs` 等规模参数在 host 侧算好后传入,
    与官方 example 用 Python 变量捕获这些常量的做法一致。"""
    M, N, K = T.const("M, N, K")

    A: T.Tensor((M, K), dtypeAB)
    B: T.Tensor((N, K), dtypeAB)
    C: T.Tensor((M, N), dtypeC)

    with T.Kernel(streamk_programs, threads=threads) as pid:
        A_shared = T.alloc_shared((block_M, block_K), dtypeAB)
        B_shared = T.alloc_shared((block_N, block_K), dtypeAB)
        A_shared_full = T.alloc_shared((block_M, block_K), dtypeAB)
        B_shared_full = T.alloc_shared((block_N, block_K), dtypeAB)
        C_local = T.alloc_fragment((block_M, block_N), accum_dtype)

        start_iter = T.alloc_fragment((1,), T.int32, "local")
        end_iter = T.alloc_fragment((1,), T.int32, "local")

        start_iter[0] = pid * full_tiles + T.min(pid, partial_tiles)
        last_iter = (pid + 1) * full_tiles + T.min(pid + 1, partial_tiles)

        while start_iter[0] < last_iter:
            end_iter[0] = T.min(
                start_iter[0] + (iters_per_tile - (start_iter[0] % iters_per_tile)),
                last_iter,
            )
            tile_id = start_iter[0] // iters_per_tile
            remain = start_iter[0] % iters_per_tile
            pid_m = tile_id // T.ceildiv(N, block_N)
            pid_n = tile_id % T.ceildiv(N, block_N)

            T.clear(C_local)
            for k in T.Pipelined(end_iter[0] - start_iter[0], num_stages=num_stages):
                T.copy(A[pid_m * block_M, (k + remain) * block_K], A_shared)
                T.copy(B[pid_n * block_N, (k + remain) * block_K], B_shared)
                T.gemm(A_shared, B_shared, C_local, transpose_B=True)

            if remain == 0 and (end_iter[0] % iters_per_tile == 0):
                T.copy(C_local, C[pid_m * block_M, pid_n * block_N])
            else:
                for i, j in T.Parallel(block_M, block_N):
                    T.atomic_add(C[pid_m * block_M + i, pid_n * block_N + j], C_local[i, j])

            start_iter[0] = end_iter[0]

        # 剩下的整块(blocking tiles)平铺给固定 grid,不需要原子合并
        for p in T.serial(sm_partition_factor):
            tile_id = pid + streamk_tiles + p * streamk_programs
            pid_m = tile_id // T.ceildiv(N, block_N)
            pid_n = tile_id % T.ceildiv(N, block_N)
            T.clear(C_local)
            for k in T.Pipelined(T.ceildiv(K, block_K), num_stages=1):
                T.copy(A[pid_m * block_M, k * block_K], A_shared_full)
                T.copy(B[pid_n * block_N, k * block_K], B_shared_full)
                T.gemm(A_shared_full, B_shared_full, C_local, transpose_B=True)
            T.copy(C_local, C[pid_m * block_M, pid_n * block_N])


# ---------------------------------------------------------------------------
# grouped GEMM:每个专家的 M 不同(MoE),用偏移表定位当前 block 属于谁
# ---------------------------------------------------------------------------
@tilelang.jit
def gemm_grouped(A, B, batch_sizes, batch_offsets, batch_padded_offsets, batch_sizes_list,
                 block_M, block_N, block_K, num_stages=2, threads=128, dtype=T.float16):
    K, N = T.const("K, N")
    batch_sum = sum(batch_sizes_list)
    batch_count = len(batch_sizes_list)
    accum_dtype = T.float32
    total_m_blocks = sum((size + block_M - 1) // block_M for size in batch_sizes_list)

    A: T.Tensor([batch_sum, K], dtype)
    B: T.Tensor([batch_count, K, N], dtype)
    C = T.empty([batch_sum, N], dtype)
    batch_sizes: T.Tensor([batch_count], T.int32)
    batch_offsets: T.Tensor([batch_count], T.int32)
    batch_padded_offsets: T.Tensor([batch_count], T.int32)

    with T.Kernel(total_m_blocks, T.ceildiv(N, block_N), threads=threads) as (bx, by):
        A_shared = T.alloc_shared([block_M, block_K], dtype)
        B_shared = T.alloc_shared([block_K, block_N], dtype)
        C_local = T.alloc_fragment([block_M, block_N], accum_dtype)
        cur_batch_idx = T.alloc_var(dtype=T.int32)

        m_start_padded = bx * block_M
        cur_batch_idx = 0
        for i in range(batch_count):
            in_cur = m_start_padded >= batch_padded_offsets[i]
            cur_batch_idx = T.if_then_else(in_cur, i, cur_batch_idx)

        cur_batch_size = batch_sizes[cur_batch_idx]
        m_start = m_start_padded - batch_padded_offsets[cur_batch_idx] + batch_offsets[cur_batch_idx]
        actual_rows = T.max(0, T.min(block_M, cur_batch_size + batch_padded_offsets[cur_batch_idx] - m_start_padded))

        T.clear(C_local)
        for k in T.Pipelined(T.ceildiv(K, block_K), num_stages=num_stages):
            T.copy(A[m_start : m_start + block_M, k * block_K : (k + 1) * block_K], A_shared)
            T.copy(B[cur_batch_idx, k * block_K : (k + 1) * block_K,
                     by * block_N : (by + 1) * block_N], B_shared)
            T.gemm(A_shared, B_shared, C_local)

        for i, j in T.Parallel(block_M, block_N):
            if i < actual_rows:
                C[m_start + i, by * block_N + j] = C_local[i, j]

    return C


# ---------------------------------------------------------------------------
# batched GEMM:形状一致的批量,把 (batch, m_block, n_block) 展平成一维 grid
# ---------------------------------------------------------------------------
@tilelang.jit
def gemm_batched(A, B, batch, block_M, block_N, block_K, num_stages=2,
                 threads=128, dtype=T.float16, accum_dtype=T.float32):
    M, N, K = T.const("M, N, K")
    m_num = T.ceildiv(M, block_M)
    n_num = T.ceildiv(N, block_N)

    A: T.Tensor((batch, M, K), dtype)
    B: T.Tensor((batch, K, N), dtype)
    C = T.empty((batch, M, N), dtype)

    with T.Kernel(batch * m_num * n_num, threads=threads) as cid:
        bid = cid // (m_num * n_num)
        rem = cid % (m_num * n_num)
        bx = rem // n_num
        by = rem % n_num

        A_shared = T.alloc_shared((block_M, block_K), dtype)
        B_shared = T.alloc_shared((block_K, block_N), 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[bid, bx * block_M, k * block_K], A_shared)
            T.copy(B[bid, k * block_K, by * block_N], B_shared)
            T.gemm(A_shared, B_shared, C_local)

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

    return C


def streamk_plan(m, n, k, block_m, block_n, block_k, total_sm):
    """算出 stream-K 需要的几个规模参数(纯 Python,可在无 GPU 时单独调用)。"""
    num_block_m = math.ceil(m / block_m)
    num_block_n = math.ceil(n / block_n)
    iters_per_tile = math.ceil(k / block_k)
    total_tiles = num_block_m * num_block_n
    streamk_tiles = total_tiles % total_sm
    if total_tiles - streamk_tiles > total_sm:
        streamk_tiles += total_sm
    blocking_tiles = total_tiles - streamk_tiles
    streamk_iters = streamk_tiles * iters_per_tile
    full_tiles = streamk_iters // total_sm
    partial_tiles = streamk_iters % total_sm
    return {
        "iters_per_tile": iters_per_tile,
        "total_tiles": total_tiles,
        "streamk_tiles": streamk_tiles,
        "blocking_tiles": blocking_tiles,
        "full_tiles": full_tiles,
        "partial_tiles": partial_tiles,
        "sm_partition_factor": max(blocking_tiles // total_sm, 1),
    }


def main() -> int:
    import torch

    torch.manual_seed(42)

    # split-K:M=N=256 太小,K=16384 很大
    m, n, k = 256, 256, 16384
    a = torch.randn(m, k, dtype=torch.float16, device="cuda")
    b = torch.randn(k, n, dtype=torch.float16, device="cuda")
    c = torch.zeros(m, n, dtype=torch.float32, device="cuda")
    gemm_splitk(a, b, c, 128, 128, 32, 4)
    torch.testing.assert_close(c, (a @ b).float(), rtol=1e-2, atol=1e-2)
    print("split-K 正确性通过")

    # batched
    batch = 8
    ab = torch.randn(batch, 512, 512, dtype=torch.float16, device="cuda")
    bb = torch.randn(batch, 512, 512, dtype=torch.float16, device="cuda")
    cb = gemm_batched(ab, bb, batch, 128, 128, 32)
    torch.testing.assert_close(cb, torch.bmm(ab, bb), rtol=1e-2, atol=1e-2)
    print("batched 正确性通过")

    # stream-K 的规模参数(打印即可,不需要真跑)
    print("stream-K plan:", streamk_plan(256, 1024, 512, 16, 128, 32, total_sm=108))
    return 0


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

← 浏览本章全部代码