跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

第 33 章 TileKernels(上):一份算法,两个后端

/code/33-tilekernels-two-backends/

01-topk-gate-minimal/topk_gate_cuda_min.py4057 字节
#!/usr/bin/env python3
"""第 33 章 §4.2:top-k gate 的 CUDA 版最小可读实现。

改写自 corpus 快照
`.workspace/research/tilelang-corpus/14-deepseek-ascend-stack/raw/TileKernels/tile_kernels/moe/topk_gate_cuda.py`
(commit 66258df6175d):保留「一行一个 block、warp 内归约、重复取 max、
tie-break 取最小下标」的全部结构,删掉非有限值检查与 PDL,并加上逐段注释。

与原文件的差异(都已在本章正文里说明):
  * 省略 `T.reducer_init/update/finalize` 构成的非有限值检查(原文 :40-46);
  * 省略 `use_pdl` 与 `T.pdl_sync()` / `T.pdl_trigger()`(原文 :23-24,65-66);
  * `num_experts` / `num_topk` 由调用方给出(原文件是同名参数,语义相同)。

运行前提:NVIDIA SM90/SM100 + CUDA Toolkit >= 13.1 + TileLang v0.1.15 + torch。
验证级别:仅静态核对(py_compile 通过;API 名与签名在 _vendor/tilelang-v0.1.15/ 中比对)。
"""

from __future__ import annotations

import tilelang
from tilelang import language as T


def align(x: int, y: int) -> int:
    """把 x 向上对齐到 y 的整数倍(与 tile_kernels/utils.py:8 相同)。"""
    return (x + y - 1) // y * y


@tilelang.jit(
    pass_configs={
        # 这个 kernel 只有一个 warp 在做一件事,不需要 producer/consumer 拆分。
        tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
    },
)
def get_topk_gate_kernel_cuda(num_experts: int, num_topk: int):
    num_tokens = T.dynamic('num_tokens')  # 运行时才知道的行数
    num_threads = 32  # 一个 warp
    num_aligned_experts = align(num_experts, num_threads)  # 向上补齐到 warp 宽度

    @T.prim_func
    def topk_gate_kernel(
        scores: T.Tensor[(num_tokens, num_experts), T.float32],
        topk_idx: T.Tensor[(num_tokens, num_topk), T.int64],
    ):
        # 一行一个 block:所有跨线程通信都退化成 warp 内通信。
        with T.Kernel(num_tokens, threads=num_threads) as pid:
            # fragment = 按线程切分的寄存器数组(第 10 章)。
            scores_fragment = T.alloc_fragment((num_aligned_experts,), T.float32)
            amax_fragment = T.alloc_fragment((1,), T.float32)
            idx_fragment = T.alloc_fragment((num_aligned_experts,), T.int32)
            # reducer = 一等公民的「延迟归约」句柄;物理通信方案由编译器选。
            idx_reducer = T.alloc_reducer((1,), T.int32, op='min')
            idx_result = T.alloc_fragment((1,), T.int32)
            # 只有输出需要跨线程协作,所以放 shared。
            topk_idx_shared = T.alloc_shared((num_topk,), T.int32)

            # 载入本行的分数;越界位置填 -inf,保证它永远不可能是最大值。
            for i in T.Parallel(num_aligned_experts):
                if i < num_experts:
                    scores_fragment[i] = scores[pid, i]
                else:
                    scores_fragment[i] = -T.infinity(T.float32)

            # 记住每个位置对应哪个专家下标,tie-break 时要用。
            for i in T.Parallel(num_aligned_experts):
                idx_fragment[i] = i

            # 重复取 max,每轮输出一个下标。num_topk <= 32 由外层 API 检查。
            for k in T.unroll(num_topk):
                T.reduce_max(scores_fragment, amax_fragment)
                # 在所有「等于最大值」的位置里取最小下标 —— tie-break 契约的落点。
                T.reducer_init(idx_reducer)
                for i in T.Parallel(num_aligned_experts):
                    if scores_fragment[i] == amax_fragment[0]:
                        T.reducer_update(idx_reducer[0], idx_fragment[i])
                T.finalize_reducer(idx_reducer, idx_result)
                topk_idx_shared[k] = idx_result[0]
                # 把中奖位置置 -inf,下一轮不再考虑它。
                for i in T.Parallel(num_aligned_experts):
                    if idx_fragment[i] == idx_result[0]:
                        scores_fragment[i] = -T.infinity(T.float32)

            # int32 shared -> int64 global:T.copy 在两侧 dtype 不同时逐元素转换;
            # 索引非负且 < 2**31,所以转换精确。disable_tma:小拷贝不值得建 TMA 描述符。
            T.copy(topk_idx_shared, topk_idx[pid, 0], disable_tma=True)

    return topk_gate_kernel


def topk_gate(scores, num_topk: int):
    """与 tile_kernels.moe.topk_gate 相同的契约:返回 [T, k] 的 int64 下标。"""
    import torch

    assert scores.dim() == 2 and scores.is_contiguous() and scores.dtype == torch.float32
    num_tokens, num_experts = scores.shape
    assert num_topk <= num_experts and num_topk <= 32

    out = torch.empty((num_tokens, num_topk), dtype=torch.int64, device=scores.device)
    if num_tokens == 0:
        return out
    get_topk_gate_kernel_cuda(num_experts, num_topk)(scores, out)
    return out

← 浏览本章全部代码