跳到主要内容
TileLang 系统教程

tilelang-tutorial.huandu.me

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

/code/33-tilekernels-two-backends/

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

改写自 corpus 快照
`.workspace/research/tilelang-corpus/14-deepseek-ascend-stack/raw/TileKernels/tile_kernels/moe/topk_gate_asc.py`
(commit 66258df6175d):保留「UB 缓冲 + 显式缓冲版本 + SIMD 归约 + int64 位视图」
的全部结构,只保留单组(num_groups == 1)快路径,并加上逐段注释。

与原文件的差异(都已在本章正文里说明):
  * 只保留 num_groups == 1 的快路径(原文 :73-105),省略多组慢路径(原文 :106-151);
  * 保留非法值打标(原文 :66-71)与循环外断言(原文 :155),因为它是「缓冲版本 = 1」
    这个设计的直接后果;
  * 省略 `T.copy(..., l2_cache_ctrl=...)` 之类的可选提示。

运行前提:Ascend 950 NPU + CANN >= 9.2.0 + torch_npu + TileLang v0.1.15
(用 `tilelang.ascend.language` 方言,即本章所说的 M 树)。
验证级别:仅静态核对(py_compile 通过;API 名与签名在 _vendor/tilelang-v0.1.15/ 中比对)。
"""

from __future__ import annotations

import tilelang
from tilelang.ascend import language as T
from tilelang.ascend.language import simd as S


def ceil_div(x: int, y: int) -> int:
    return (x + y - 1) // y


def align(x: int, y: int) -> int:
    return ceil_div(x, y) * y


@tilelang.jit
def get_topk_gate_kernel_asc(num_experts: int, num_topk: int, num_vec_cores: int):
    num_stages = 6
    vector_length = 64  # 一条向量寄存器(vreg)装 64 条 fp32 SIMD lane

    # 一组最多用 6 个 vreg。num_experts 不大时只开够用的几个,减少寄存器压力。
    max_vregs_per_group = 6
    num_vregs_per_group = ceil_div(min(num_experts, max_vregs_per_group * vector_length), vector_length)
    experts_per_group = num_vregs_per_group * vector_length
    num_groups = ceil_div(num_experts, experts_per_group)
    num_aligned_experts = num_groups * experts_per_group

    # 对齐到 vector_length:既方便 UB 访存,也顺便给下面的 int64 位视图留出空间
    # (2 * num_topk <= 64 <= num_aligned_topk)。
    num_aligned_topk = align(num_topk, vector_length)
    num_tokens = T.dynamic('num_tokens')

    assert num_groups == 1, '本最小示例只保留单组快路径;多组路径见原生实现'

    @T.prim_func
    def topk_gate_kernel_asc(
        scores: T.Tensor[(num_tokens, num_experts), T.float32],
        topk_idx: T.Tensor[(num_tokens, num_topk), T.int64],
    ):
        # 昇腾的 grid 就是 Vector 核的编号;这一层没有 threads= 参数。
        with T.Kernel(num_vec_cores) as core_id:
            # 在昇腾方言里 T.alloc_shared 指向 UB,不是 CUDA 的 shared memory。
            scores_ub = T.alloc_shared((num_aligned_experts,), T.float32)
            out_ub = T.alloc_shared((num_aligned_topk,), T.int32)
            invalid_logits_ub = T.alloc_shared((vector_length,), T.int32)
            # 把 int64 输出重新解释成 int32:低位写下标,高位保持 0。
            topk_idx_view = T.view(topk_idx, (num_tokens, num_topk * 2), T.int32)
            # 显式声明缓冲版本:前两个是流水数据(num_stages 份),
            # 第三个是跨所有迭代共享的累积器(只有 1 份)。
            T.annotate_buffer_versions({
                scores_ub: num_stages,
                out_ub: num_stages,
                invalid_logits_ub: 1,
            })

            with T.SimdVF():
                T.clear(out_ub)             # 高位必须为 0,int64 视图才正确
                T.clear(invalid_logits_ub)

            # 流水:每核处理 ceil_div(num_tokens, num_vec_cores) 行,按核交错分配。
            for w in T.Pipelined(
                T.ceildiv(num_tokens, num_vec_cores),
                num_stages=num_stages,
                annotations={'multi_buffer_eligible': [scores_ub, out_ub]},
            ):
                row = w * num_vec_cores + core_id
                if row < num_tokens:
                    T.copy(scores[row, :], scores_ub[:num_experts])

                    with T.SimdVF():
                        mask_32b = S.pset(32, 'PAT_ALL')
                        one = S.pset(32, 'PAT_VL1')
                        neg_inf = S.vdup(-T.infinity(T.float32), T.float32, mask_32b)
                        int_max = S.vdup(T.max_value(T.int32), T.int32, mask_32b)

                        scores_vec = S.alloc_local((num_vregs_per_group,), T.float32)
                        index_vec = S.alloc_local((num_vregs_per_group,), T.int32)
                        tmp_idx_vec = S.alloc_local((num_vregs_per_group,), T.int32)

                        lanes = S.vci(0, T.int32)
                        zero = S.vdup(0.0, T.float32)
                        zeros = S.vdup(0, T.int32)

                        # 非有限值检测:x - x != 0 对 NaN 与 +/-inf 都成立。
                        # 结果用 max 累积进一个 64 宽的向量寄存器,循环外统一断言。
                        invalid = T.alloc_var(T.int32x64, init=S.vld(invalid_logits_ub[0]))
                        for j in T.unroll(ceil_div(num_experts, vector_length), explicit=True):
                            valid = S.vcmps(S.vadds(lanes, j * vector_length), num_experts, op='lt')
                            logit = S.vsel(S.vld(scores_ub[j * vector_length]), zero, valid)
                            nonfinite = S.vcmps(S.vsub(logit, logit), 0.0, op='ne')
                            invalid = S.vmax(invalid, S.vsel(S.vdup(1, T.int32), zeros, nonfinite))
                        S.vsts(invalid_logits_ub[0], S.vcmax(invalid), one)

                        # 载入 vreg,并把越界 lane 掩成 -inf(而不是预先补齐 UB)。
                        for i in T.unroll(num_vregs_per_group, explicit=True):
                            scores_vec[i] = S.vld(scores_ub[i * vector_length])
                            index_vec[i] = S.vci(T.int32(i * vector_length), T.int32)
                        num_exp = S.vdup(T.int32(num_experts), T.int32, mask_32b)
                        for i in T.unroll(num_vregs_per_group, explicit=True):
                            scores_vec[i] = S.vsel(
                                scores_vec[i], neg_inf, S.vcmp(index_vec[i], num_exp, mask_32b, 'lt')
                            )

                        acc_max = S.alloc_var(T.float32)
                        acc_min_idx = S.alloc_var(T.int32)
                        for k in T.serial(num_topk):
                            # 1) 组内 vmax 折叠,再 vcmax 做跨 lane 归约,得到全局最大值。
                            acc_max = scores_vec[0]
                            for i in T.unroll(1, num_vregs_per_group, explicit=True):
                                acc_max = S.vmax(acc_max, scores_vec[i], mask_32b)
                            mx = S.vdupv(S.vcmax(acc_max, mask_32b), mask_32b)

                            # 2) 非并列位置换成 INT_MAX,再 vmin + vcmin 取最小下标(tie-break)。
                            for i in T.unroll(num_vregs_per_group, explicit=True):
                                tmp_idx_vec[i] = S.vsel(
                                    index_vec[i], int_max, S.vcmp(scores_vec[i], mx, mask_32b, 'eq')
                                )
                            acc_min_idx = tmp_idx_vec[0]
                            for i in T.unroll(1, num_vregs_per_group, explicit=True):
                                acc_min_idx = S.vmin(acc_min_idx, tmp_idx_vec[i], mask_32b)
                            idx = S.vdupv(S.vcmin(acc_min_idx, mask_32b), mask_32b)

                            # 3) 单 lane 写第 k 个结果;然后把中奖 lane 掩成 -inf。
                            S.vsts(out_ub[2 * k], idx, one, 'ONEPT_B32')
                            for i in T.unroll(num_vregs_per_group, explicit=True):
                                scores_vec[i] = S.vsel(
                                    neg_inf, scores_vec[i], S.vcmp(index_vec[i], idx, mask_32b, 'eq')
                                )

                    # 低位 + 高位(0)一起搬过去,就是 int64 下标。
                    T.copy(out_ub[: num_topk * 2], topk_idx_view[row, :])

            # 因为 invalid_logits_ub 只有 1 份、被所有迭代共享,断言放在循环之外。
            T.device_assert(invalid_logits_ub[0] == 0, msg='topk_gate input contains nan or inf!')

    return topk_gate_kernel_asc


def topk_gate(scores, num_topk: int, num_vec_cores: int):
    """与 tile_kernels.moe.topk_gate 相同的契约;num_vec_cores 由调用方探测后传入。"""
    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_asc(num_experts, num_topk, num_vec_cores)(scores, out)
    return out

← 浏览本章全部代码