Prhub

#33903 [Inkling] silu_and_mul: replace helion kernels with plain Triton

原始 PR 作者 sshleifer 合并时间 2026-08-08 15:00 文件变更 13 提交数 4 评论 2 代码增减 +368 / -700

执行摘要

Inkling MoE 激活内核迁移纯 Triton,移除 helion 依赖

PR body 开宗明义:"Remove the helion dependency from the Inkling MoE activation path"。动机有三层:一是依赖收敛,将两个 helion silu_and_mul 内核移植为纯 Triton(输出逐位一致),删除 helion AOT autotune 机制与 per-arch 配置表,并移除 helion==1.4 包依赖;二是消除缺陷类别,InklingBatchDenseMLP._swiglu 注释记录 helion 内核 "can produce NaNs for small shared-expert batches"(EP+DP 配置),调查显示该报告早于仓库可达历史且无法复现,作者推断根因是 helion AOT wrapper 按 (width, dtype, size-1 pattern, int64) 键在运行时编译不同内核变体、生成代码面过大,疑似 codegen/dispatch 缺陷,移植为两个静态 mask 内核可从根本上移除该类别;三是性能与可维护性,GB300 基准显示多数形状有收益,且删除配置表后新架构接入不再需要重新 autotune。

值得精读。三个看点:一是 bitwise 级内核移植方法论(复刻运算顺序、仅 store 一次 cast、用独立 harness 做 88/88 逐位对比);二是性能工程细节(BLOCK_M==1 时 mask 省略、evict_last/evict_first 缓存提示、N-fastest 光栅化、tile/warp 扫描直至编译后 PTX 指令混合与 helion 一致、63 寄存器 0 spill);三是用两个静态 mask 内核加小 launch 启发式替代整套 AOT autotune 配置表体系的设计决策,是消除第三方运行时 codegen 依赖的典型范例。同时注意其仅 sm_100 实测的验证边界,若团队在非 Blackwell 架构上运行 Inkling,建议补充一次基准对比。

讨论亮点

本 PR 没有任何代码级 review 评论,唯一 review 是维护者 ispobock 的 APPROVED(正文为空)。仅有的 2 条 issue 评论都是 CI 命令:sshleifer 的 /tag-and-rerun-ci 与 ispobock 的 /rerun-failed-ci;提交历史中也有两次重跑 CI 的记录(空提交与加 run-ci 标签),最终 PR Test 与 PR Test Extra 均通过。实质性的技术论证全部由 PR body 承载,核心声明包括:"The report predates all reachable history. The failure does not reproduce with the pinned helion and the checked-in configs on GB300." 以及 "The suspected failure class is a codegen or dispatch defect in the helion version used at report time… the AOT wrapper compiles a distinct kernel variant per (width, dtype, size-1 pattern, int64) key at runtime, which is a large generated-code surface." 结论是与其排查无法复现的第三方编译器问题,不如把整个运行时按 key 生成代码的机制移除。

实现拆解

  1. 内核移植(python/sglang/kernels/ops/moe/inkling_moe.py):把 _silu_and_mul_helion_interleaved_kernel 与 _silu_and_mul_helion_non_interleaved_kernel 改写为纯 Triton 的 silu_and_mul_interleaved_kernel / silu_and_mul_non_interleaved_kernel(公开命名,便于 profiler 追踪)。运算顺序完全复刻 helion:gate/up 转 fp32 → gate * sigmoid(gate) * up → 可选每行权重最后乘 → store 时一次取整 cast。外层 wrapper silu_and_mul_helion 重命名为 silu_and_mul,签名与行为不变,内部用 launch 启发式选择 tile。silu_and_mul_triton(InklingBatchDenseMLP._swiglu 使用的 persistent-grid 内核,含 sm_121 num_stages 门控)保持原样。随移植删除 AOT 专用的 silu_and_mul_key 与 silu_and_mul_inputs。
  2. 调用点更新(python/sglang/srt/models/inkling_common/moe.py):唯一调用点 activation() 的导入与调用从 silu_and_mul_helion 改为 silu_and_mul。生产路径上 routed MoE 恒传 topk_weights=None,带权重分支主要由测试覆盖。
  3. 删除 helion AOT 机制(python/sglang/srt/layers/moe/moe_runner/triton_utils/helion_utils.py 与 7 个 JSON 配置表):helion_utils.py 296 行整体删除(含 helion_aot_autotune、bind_and_compile_kernel、load_autotune_data_from_json、AOTAutotuneMode 等),其唯一导入方就是 inkling_moe.py;silu_and_mul{,interleaved}_sm{90,95,100,121}.json 共 7 个配置表一并移除。
  4. 依赖移除(python/pyproject.toml、python/pyproject_other.toml):删除 helion==1.4,代码树中不再出现 import helion。
  5. 测试配套(test/registered/kernels/ops/moe/test_inkling_silu_and_mul.py):新增 159 行、47 项测试,注册到 base-b-kernel-unit 的 1-gpu-large runner。覆盖参考数值(bf16 允许 1 ulp)、两布局逐位一致、确定性、out_dtype、奇数宽度、零行、numel > 231 的 int64 偏移路径,以及小共享专家批次回归族(M = n_shared * tokens ∈ 1..16,N = 768/1024/1536)。
    配套验证:作者用独立 harness(保留被删 helion 内核与配置的副本)在 GB300 上对比 88/88 用例逐位一致,包括 numel = 2
    31 + 16384 的大张量;test_moe_preprocess.py 17 项照常通过。代表性基准(CUDA-graph 流式、min of 3):M=6 N=1024 从 2.69 us 降到 1.31 us(2.05x);M=96 N=1536 从 2.47 us 降到 1.44 us(1.71x);M=384 N=1024 带权重从 3.34 us 降到 2.15 us(1.56x);M=49152 N=768 从 42.01 us 降到 33.39 us(1.26x);M=49152 N=3072 从 154.97 us 降到 129.04 us(1.20x);non-interleaved 带权重 N=768 的 2 个形状为 0.97x(慢约 3%)。
文件 模块 状态 重要度
python/sglang/kernels/ops/moe/inkling_moe.py 激活内核 modified 7.51
python/sglang/srt/layers/moe/moe_runner/triton_utils/helion_utils.py 调优框架 removed 9.08
test/registered/kernels/ops/moe/test_inkling_silu_and_mul.py 内核测试 added 7.61
python/sglang/srt/models/inkling_common/moe.py 专家前向 modified 5.11
python/sglang/srt/layers/moe/moe_runner/triton_utils/configs/silu_and_mul_interleaved_sm_100.json 调优配置 removed 5.01
python/pyproject.toml 依赖声明 modified 2.93

关键符号

silu_and_mul_interleaved_kernel silu_and_mul_non_interleaved_kernel silu_and_mul activation helion_aot_autotune bind_and_compile_kernel test_matches_reference test_small_shared_expert_batches test_int64_offsets

关键源码片段

python/sglang/kernels/ops/moe/inkling_moe.py core-logic

本 PR 的核心:两个 helion silu_and_mul 内核在此逐位移植为纯 Triton 静态 mask 内核,并重命名 wrapper 为 silu_and_mul,同时删除 AOT 专用辅助符号。

# python/sglang/kernels/ops/moe/inkling_moe.py
# 由 helion 内核逐位移植的 silu_and_mul 激活内核(interleaved 布局):
# 输入 gateup 形状为 [M, 2N],行内按 [g0, u0, g1, u1, ...] 交错存放,
# 计算 down_inp[i] = silu(gate[i]) * up[i],可选再乘每行权重 topk_weights[i]。
# 移植要点:
# 1) 运算顺序与 helion 完全一致(先转 fp32,乘法后仅 store 时一次取整 cast),
# 因此 bf16 输出逐位一致;
# 2) 每个 load/store 都带显式 mask,不再依赖 helion 的运行时 codegen 与
# per-arch 配置表,从根本上消除 " 小批量共享专家产出 NaN" 的缺陷类别。@triton.jit(do_not_specialize=["M"])
def silu_and_mul_interleaved_kernel(
    gateup_out_ptr, # [M, 2N] 输入,行内 [g0, u0, g1, u1, ...]
    topk_weights_ptr, # [M] 每行权重,仅在 HAS_TOPK_WEIGHTS=True 时读取
    down_inp_ptr, # [M, N] 输出
    M,
    stride_gm,
    N: tl.constexpr,
    HAS_TOPK_WEIGHTS: tl.constexpr,
    INT64_INDEX: tl.constexpr,
    BLOCK_M: tl.constexpr,
    BLOCK_N: tl.constexpr,
):
    # 扁平 grid,N 方向 tile 最快:相邻 CTA 读取同一行的连续片段,
    # 实测比 M 方向优先的光栅化更快。
    pid = tl.program_id(0)
    if INT64_INDEX:
        pid = pid.to(tl.int64) # numel >= 2**31 时改用 int64 寻址
    NUM_BLOCKS_N: tl.constexpr = tl.cdiv(N, BLOCK_N)
    pid_m = pid // NUM_BLOCKS_N
    pid_n = pid % NUM_BLOCKS_N
​
    offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
    offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
    mask_n = offs_n < N
    offs_2n = pid_n * (2 * BLOCK_N) + tl.arange(0, 2 * BLOCK_N)
    mask_2n = offs_2n < 2 * N
    if BLOCK_M == 1:
        # grid 恰好覆盖 M,省略行 mask 在带宽受限形状上有可测收益
        mask_mn = mask_n[None, :]
        mask_m2n = mask_2n[None, :]
    else:
        mask_m = offs_m < M
        mask_mn = mask_m[:, None] & mask_n[None, :]
        mask_m2n = mask_m[:, None] & mask_2n[None, :]
​
    # 按 [g, u] 对连续加载整段数据,用 tl.split 在寄存器内去交错;
    # 逐元素 stride gather 会让有效带宽减半,必须避免。
    gateup = tl.load(
        gateup_out_ptr + offs_m[:, None] * stride_gm + offs_2n[None, :],
        mask=mask_m2n,
        other=0.0,
    )
    gate, up = tl.split(tl.reshape(gateup, (BLOCK_M, BLOCK_N, 2)))
    gate = gate.to(tl.float32)
    up = up.to(tl.float32)
​
    # 运算顺序与 helion 相同:全流程在 fp32 中计算
    down_inp = gate * tl.sigmoid(gate) * up
    if HAS_TOPK_WEIGHTS:
        if BLOCK_M == 1:
            weight_scale = tl.load(
                topk_weights_ptr + offs_m, eviction_policy="evict_last"
            ).to(tl.float32)
        else:
            weight_scale = tl.load(
                topk_weights_ptr + offs_m,
                mask=offs_m < M,
                eviction_policy="evict_last",
            ).to(tl.float32)
        down_inp = down_inp * weight_scale[:, None]
​
    # 仅在此处做一次 fp32 -> 输出 dtype 的取整 cast,与 helion 对齐
    offs_mn = offs_m[:, None] * N + offs_n[None, :]
    tl.store(
        down_inp_ptr + offs_mn,
        down_inp.to(down_inp_ptr.dtype.element_ty),
        mask=mask_mn,
    )
test/registered/kernels/ops/moe/test_inkling_silu_and_mul.py test-coverage

新增 159 行、47 项测试,注册到 base-b-kernel-unit:以同序 fp32 参考验证 1 ulp 数值、布局逐位一致、确定性、int64 偏移与小共享专家批次 NaN 回归。

# test/registered/kernels/ops/moe/test_inkling_silu_and_mul.py
# 参考实现刻意采用与内核一致的运算顺序(整体转 fp32、weight 最后乘、
# 末尾一次 cast),因此 bf16 输出与参考只差 tl.sigmoid / torch.sigmoid
# 的最后一个 fp32 ulp,容差收紧到 1 个 bf16 ulp。def _reference(gateup, weights, out_dtype, interleaved):
    xf = gateup.float()
    if interleaved:
        gate, up = xf[:, 0::2], xf[:, 1::2]
    else:
        n = gateup.shape[1] // 2
        gate, up = xf[:, :n], xf[:, n:]
    out = gate * torch.sigmoid(gate) * up
    if weights is not None:
        out = out * weights.float()[:, None] # 每行权重最后乘
    return out.to(out_dtype or gateup.dtype) # 末尾一次取整 cast
​
​
def _ulp_diff_bf16(a, b):
    # bf16 逐位比较:剥离符号位后按 int16 取差,得到 ulp 距离
    mask = (1 << 15) - 1
    ai = a.contiguous().view(torch.int16).to(torch.int64)
    bi = b.contiguous().view(torch.int16).to(torch.int64)
    ai = torch.where(ai < 0, -(ai & mask), ai)
    bi = torch.where(bi < 0, -(bi & mask), bi)
    return (ai - bi).abs()
​
​
# 回归旧 helion 内核在 EP+DP 配置下小共享专家批次的 NaN 报告:
# m = n_shared_experts * tokens_on_dp_rank,n 为每 rank 共享专家宽度;
# 要求输出有限且正确——这是本 PR 顺带修复的行为变更。
@pytest.mark.parametrize("m", [1, 2, 4, 8, 16])
@pytest.mark.parametrize("n", [768, 1024, 1536])
def test_small_shared_expert_batches(m, n):
    out = _check(m, n, True, True)
    assert out.isfinite().all()

评论区精华

无代码级 review 评论,论证由 PR body 承载 other

13 个文件、+368/-700 的改动没有任何 review 评论,ispobock 直接 APPROVED(正文为空);唯一 2 条 issue 评论是 CI 命令(/tag-and-rerun-ci、/rerun-failed-ci)。

结论:技术论证全部由 PR body 承载:88/88 逐位证据、基准表、NaN 调查报告;无未解决疑虑。 · closed

CI 重跑与合并前状态 测试

提交历史含两次 CI 重触发(空提交与加 run-ci 标签),标签含 bypass-fastfail 与 run-ci-extra;合并前 PR Test 与 PR Test Extra 均通过。

结论:最终 CI 全绿后由 ispobock 合并,无失败项遗留。 · closed

风险与影响

覆盖验证局限:所有 bitwise 与性能验证仅在 GB300(sm_100)完成,sm_90/sm_95/sm_121 的配置表直接删除且未在对应架构复测;新内核无 arch 特化、所有访问带 mask,正确性风险低,但非 Blackwell 架构上的性能未知,存在隐性回归可能。性能残余:non-interleaved + 每行权重 + N=768 + M≥6144 的两个形状比 helion 慢约 3%(编译后 PTX 指令混合与 helion 一致,残余归因于 allocation 地址敏感),该形状不在生产路径(MoE activation() 恒传 topk_weights=None),影响可控。行为变更:凡旧 helion 栈曾产出 NaN 的输入,新内核会输出正确有限值,是有意 bugfix,但属于语义变化。打包契约:pyproject 删除 helion==1.4 影响所有安装路径,若代码树残留未发现的 import helion 会在运行时直接 ImportError,PR 声明已清理干净但只在 GB300 路径验证。测试环境依赖:test_int64_offsets 需要 ≥ 16 GB 显存否则 skip,CI 实际覆盖取决于 runner 配置。

用户/部署层面:安装与运行 sglang 不再需要第三方 helion 内核编译器依赖,简化环境;Inkling 系模型(如 d42/d66)的 MoE 激活路径在 GB300 上 interleaved 布局实测提速 1.04x-2.05x,大 batch(49152 行)约 1.2x-1.26x,decode 小批量在 N=1024/1536/2048 上收益最大(1.4x-2.05x),non-interleaved 基本持平(0.97x-1.63x)。系统层面:净删约 700 行代码(含 296 行 autotune 框架与 7 个 JSON 配置表),消除启动期配置表查找、运行时 per-key codegen、hash miss 警告等路径,降低维护与分发成本。团队层面:47 项新测试为后续内核修改提供安全网,并固化了小共享专家批次、布局逐位一致性、int64 大张量等边界;该 PR 是仓库 helion 内核逐个迁移到纯 Triton 路线的样板。

仅 sm_100 实测其余架构未复测 打包依赖移除契约变更 NaN 行为变更有意修复 非交错布局小 N 慢约 3% int64 偏移测试依赖大显存 runner

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论