Prhub

#35760 [Perf] Tune the W4AFP8 DeepEP low-latency requant launch geometry

原始 PR 作者 alexnails 合并时间 2026-08-30 07:03 文件变更 6 提交数 10 评论 18 代码增减 +618 / -75

执行摘要

重调 W4AFP8 requant 启动几何,decode 内核提速最高 12x

PR body 明确指出:fp8_per_token_to_per_tensor_quant_triton() 是纯流式内核(每元素两次乘法、移动两个字节),但启动形状的三处属性比算术本身更贵——1024 元素 tile 配 8 warps 只有 4 B/thread,正好停在 Triton 停止向量化的阈值上,编译产物只有 ld.global.b32 / st.global.b32;scale 按元素轴寻址,在 DeepEP column-major 布局下形成每行重复 128 次的 8 地址 gather;m-grid 固定每专家 32 个 program,低延迟 decode 行数稀疏时大部分 program 只做 masked load 就退出——256 个本地专家、每专家 1 行时内核花 35 µs 移动约 3 µs 的数据。

值得精读。三个设计决策有普适价值:

1) tile 大小以 bytes-per-lane 而非元素数定义,天然兼容 wave32/wave64 厂商差异;
2) 行估计必须向下取整到 2 的幂,与 dispatch_a 的上取整形成互补而不是叠加(否则每个 2 的幂边界 m-grid 翻倍);
3) 两阶段行调度(per-expert strided + 全局溢出共享)解决“平均值掩盖峰值”的经典调度问题,并用 row_cap slack 2x 在均匀负载代价(13-20%)与偏斜回归之间取平衡。阅读时建议对照 PR body 的性能表和提交历史中的 “share overflow rows”、“tile per lane” 两个关键演进提交。

讨论亮点

BBuf:Could we benchmark with the actual expected_m from the dispatcher? An exact average of 8 rows produces 9, which changes m_grid from 8 to 16.

benchmark 的 _expected_m 改为模拟 dispatch_a 的 (rows + num_experts) // num_experts 上取整上报,使基准与生产 launch 对齐。

BBuf:_legacy still runs the rewritten kernel, so it only restores the old launch parameters, not the old scale-loading behavior.

alexnails 接受并重命名,最终以 legacy-geometry 命名并在文档串中说明其只隔离 launch geometry,不包含 scale 加载方式的重写。

BBuf:This case does not actually exercise the expert cap. With 128 experts and expected_rows=8, both paths choose m_grid=8.

测试参数改为 (40, 32)(128, 16),让 cap 真正绑定:40 专家封顶 16、128 专家封顶 8,同时把每专家行数改成互不相同。

BBuf:expected_rows=None only preserves the old m-grid. The group block size and num_warps still change.

alexnails 承认描述过度,测试更名为 test_unknown_row_count_keeps_the_previous_m_grid,PR body 中同类表述一并修正——expected_rows=None 只保留旧 m-grid。

BBuf:These constants are tuned on H200 and B200 but apply to all SM90+ GPUs. Do we have an H100 or H20 result as well?

alexnails 补测 H100,并刻意在 torch 2.11 / triton 3.6torch 2.13 / triton 3.7.1 两套工具链上验证(收益依赖 Triton codegen);H20 无设备,给出单命令复现入口。

BBuf:expected_m is an average, so with skewed routing a hot expert may be left with m_grid=4/8 and serialize many rows.

benchmark 增加 skew 轴:一个 hot expert 拿 skew * rows,其余专家均分剩余,并保持 dispatch 总量(即 expected_m)不变。

BBuf:The CI subset only runs E8, so it does not exercise the num_experts >= 32 tile/cap path.

CI 子集默认加入 (3584, 56, 256) 形状与 rows=32skew=16 等代表组合。

实现拆解

1. 内核 tile 重构:从扁平元素到整 scale 组

python/sglang/kernels/ops/moe/ep_moe_kernels.py_fp8_per_token_quant_to_per_tensor_quant_kernel 把 payload 从扁平元素段改为 [G_BLOCK_SIZE, 128] 的二维分块,K_BLOCK_SIZE 更名为 G_BLOCK_SIZE(constexpr 单位从隐藏元素数变成 128 宽 scale 组数)。每组的 scale 由一次标量 load 获得,快轴保持连续,部分 k 改为整组掩码。warps 由 8 降至 4,使每线程在飞字节从 4 B 升到 8 B,恢复 v2.b32 向量访问。

2. 两阶段行调度:row_cap + 全局溢出共享

新抽出 _requant_row 逐行写函数,phase 1 与 phase 2 共用,保证两条路径写出的行逐位一致。Phase 1 中每个专家按 m-grid 步长处理 min(masked_m, row_cap) 以下的行;Phase 2 通过 tl.cumsum 前缀和把扁平溢出索引映射回 (expert, row),让 hot expert 的溢出行共享整个 launch 的 program,避免均值估计(expected_m)掩盖偏斜路由、把单个专家串行化。均匀负载时溢出行数为 0,只付出一次归约开销。

3. 启动几何启发式:requant_launch_geometry

该函数返回 (G_BLOCK_SIZE, m_grid, row_cap) 三个 launch hint。关键设计:tile 以元素表达再除以 group 宽度,保证“每线程字节数”(真正的调优量)不被 group 宽度悄悄改变;expected_rows 向下取整到 2 的幂,因为 dispatch_a 上报 (rows + E) // E 已向上取整,二次上取整会在每个 2 的幂边界把 m-grid 翻倍;m-grid 上限保持历史 32、下限 4,行数低于 64 且专家轴未填满时按 1024 program 目标收缩;requant_warp_size 从 device props 读真实 warp 宽度(lru_cache 缓存),以适配 AMD wave64。

4. 调用链打通

expected_m 早已在 host 侧 dispatch_a 中算好、且对已捕获的 CUDA graph 恒定。本 PR 把它从 w4afp8.pyapply_deepep_llcutlass_w4a8_moe_deepep_ll 透传到 fp8_per_token_to_per_tensor_quant_tritonexpected_rows,仅作 launch hint,正确性不依赖它。

5. 测试、基准与 CI 配套

  • 新增 CPU 单元测试 test_w4afp8_requant_geometry.py(12 个用例):锁定 m-grid 上界/单调性/向下取整、行 cap 契约、每线程字节数跨 warp 宽度不变、tile 不超 payload。
  • 扩展 GPU 单元测试 test_fp8_per_token_to_per_tensor_quant.py:覆盖 column-major scale、40/128 专家 cap 生效、全 launch 几何组合逐位一致、空专家跳过。
  • 新增 base-b-kernel-benchmark 条目 bench_fp8_per_token_to_per_tensor_quant.py:tuned 与 legacy-geometry 同图对比,含 skewed 路由轴。
文件 模块 状态 重要度
python/sglang/kernels/ops/moe/ep_moe_kernels.py MoE 内核 modified 7.51
test/registered/unit/layers/moe/test_w4afp8_requant_geometry.py 单元测试 added 7.29
test/registered/kernels/ops/moe/test_fp8_per_token_to_per_tensor_quant.py 单元测试 modified 7.08
test/registered/kernels/benchmark/moe/bench_fp8_per_token_to_per_tensor_quant.py 基准测试 added 7.24
python/sglang/srt/layers/quantization/w4afp8.py 量化层 modified 4.73
python/sglang/srt/layers/moe/cutlass_w4a8_moe.py MoE 算子 modified 4.67

关键符号

fp8_per_token_to_per_tensor_quant_triton _fp8_per_token_quant_to_per_tensor_quant_kernel _requant_row requant_launch_geometry requant_warp_size _floor_pow2 cutlass_w4a8_moe_deepep_ll apply_deepep_ll

关键源码片段

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

核心变更文件:重构 requant Triton 内核的 tile 形状、warp 数与 m-grid,新增两阶段行调度和 launch geometry 启发式,是本 PR 全部性能收益的来源。

# 调优单位是“每线程字节数”而非元素数:NVIDIA wave32 与 AMD wave64 的 warp 宽度不同,
# 固定元素数会在 wave64 部件上把每线程字节数减半。
_REQUANT_BYTES_PER_LANE = 16 # 专家数 >= 32 时每个线程搬 16 B
_REQUANT_BYTES_PER_LANE_FEW_EXPERTS = 8 # 专家少、grid 填不满时 tile 减半
_REQUANT_MANY_EXPERTS = 32
_REQUANT_NUM_WARPS = 4 # 4 warps 让每线程 8 B,恢复 v2.b32 向量化
_REQUANT_DEFAULT_WARP_SIZE = 32
_REQUANT_M_GRID_MAX = 32 # 历史固定值,新几何只缩不放
_REQUANT_M_GRID_MIN = 4 # 下限避免单行 batch 把 expert 串行化
_REQUANT_TARGET_PROGRAMS = 1024 # 行稀缺时 (m-grid x expert) 平面上的 program 目标
_REQUANT_ROWS_SATURATED = 64 # 超过此行数,cap 掉的 program 会有真实工作
_REQUANT_ROW_CAP_SLACK = 2 # row_cap = 2x 期望行,容忍专家间普通波动
​
​
def _floor_pow2(value: int) -> int:
    return 1 << (max(1, value).bit_length() - 1)
​
​
@lru_cache(maxsize=None)
def requant_warp_size(device: torch.device) -> int:
    """每个 warp 的 lane 数,决定 requant 的 tile 宽度。"""
    return torch.cuda.get_device_properties(device).warp_size
​
​
def requant_launch_geometry(
    num_groups: int,
    num_experts: int,
    group_size: int = 128,
    expected_rows: Optional[int] = None,
    warp_size: int = _REQUANT_DEFAULT_WARP_SIZE,
    max_rows: int = 1 << 30,
) -> Tuple[int, int, int]:
    """返回 (每组 program 的 group 数, m_grid, row_cap) 三个 launch hint。    任何取值都产生相同的字节输出,几何只影响性能。行估计必须向下取整到 2 的幂:
    dispatch_a 上报 (rows + num_experts) // num_experts,在恰好平均时多报 1 行,
    若这里再向上取整,会在每个 2 的幂边界把 m-grid 翻倍。
    """
    # payload 是 fp8,一个元素就是一个字节,所以每线程字节数等于每线程元素数。
    bytes_per_lane = (
        _REQUANT_BYTES_PER_LANE
        if num_experts >= _REQUANT_MANY_EXPERTS
        else _REQUANT_BYTES_PER_LANE_FEW_EXPERTS
    )
    tile_elems = bytes_per_lane * _REQUANT_NUM_WARPS * warp_size
    # tile 用元素表达、除以 group 宽度得到 G_BLOCK_SIZE:不同 group 宽度不会
    # 悄悄改变实测的 tile 大小;再向下取整并 clamp 到 payload。
    g_block = min(_floor_pow2(tile_elems // group_size), _floor_pow2(num_groups))
​
    if expected_rows is None:
        # 没有行估计就没有 cap 的依据,保持历史 m-grid,所有行留在本专家。
        return g_block, _REQUANT_M_GRID_MAX, max_rows
​
    m_grid = min(
        _REQUANT_M_GRID_MAX,
        max(_REQUANT_M_GRID_MIN, _floor_pow2(expected_rows)),
    )
    if expected_rows < _REQUANT_ROWS_SATURATED:
        # 行稀缺时用 program 目标限制 m-grid,避免专家轴还没填满机器就空转。
        m_grid = min(
            m_grid, _floor_pow2(_REQUANT_TARGET_PROGRAMS // max(1, num_experts))
        )
    row_cap = min(max_rows, max(1, expected_rows * _REQUANT_ROW_CAP_SLACK))
    return g_block, m_grid, row_cap
test/registered/unit/layers/moe/test_w4afp8_requant_geometry.py test-coverage

新增 CPU 启发式契约测试,12 个用例锁定 m_grid 上界 / 单调性 / 向下取整、row_cap 边界、每线程字节数跨 warp 宽度不变等关键不变量,是评审中反复打磨的对象。

class TestRequantLaunchGeometry(CustomTestCase):
    def test_m_grid_never_exceeds_the_previous_fixed_grid(self):
        """行估计只会让 m-grid 收缩,任何 batch 都不会比旧固定网格更差。"""
        for expected_rows in (1, 8, 32, 33, 1024):
            for num_experts in (8, 56, 256):
                _, m_grid, _ = requant_launch_geometry(
                    DSV3_GROUPS, num_experts, expected_rows=expected_rows
                )
                self.assertLessEqual(m_grid, PREVIOUS_FIXED_M_GRID) # 历史值 32
​
    def test_dispatcher_round_up_does_not_bump_the_grid(self):
        """dispatch_a 上报 (rows + E) // E,恰好整除时多报 1 行;启发式若也向上
        取整,会在每个 2 的幂边界把 m-grid 翻倍,所以必须向下取整。"""
        for rows in (4, 8, 16, 32):
            exact = requant_launch_geometry(DSV3_GROUPS, 8, expected_rows=rows)[1]
            reported = requant_launch_geometry(
                DSV3_GROUPS, 8, expected_rows=rows + 1
            )[1]
            self.assertEqual(reported, exact, f"rows={rows}")
​
    def test_tile_holds_bytes_per_lane_across_warp_widths(self):
        """调优单位是每线程字节数:warp 32 的 2048 元素在 warp 64 上必须变成
        4096,否则 wave64 部件拿到一半的每线程字节数。"""
        for warp_size, want_elems in ((32, 2048), (64, 4096)):
            for group_size in (64, 128, 256, 512):
                g_block, _, _ = requant_launch_geometry(
                    num_groups=7168 // group_size,
                    num_experts=56,
                    group_size=group_size,
                    expected_rows=16,
                    warp_size=warp_size,
                )
                self.assertEqual(
                    g_block * group_size, want_elems, f"warp_size={warp_size}"
                )

评论区精华

benchmark 应使用 dispatcher 实际上报的 expected_m 正确性

BBuf:平均 8 行时 dispatch_a 上报 9,m_grid 从 8 变 16,直接传 rows 不匹配生产 launch。

结论:_expected_m 改为 (dispatched_rows + num_experts) // num_experts,模拟 dispatch_a 的上取整上报。 · 已解决

legacy 基准的命名与基线有效性 测试

BBuf:legacy 仍跑重写后的内核,只恢复旧 launch 参数而非旧 scale 加载行为,建议改名或使用 PR 前内核。

结论:重命名为 legacy-geometry,并在文件 docstring 说明其只隔离 launch geometry 变化。 · 已解决

test_many_experts 未真正触发 expert cap 测试

BBuf:128 专家 + expected_rows=8 时两条路径都选 m_grid=8,cap 未生效;建议 expected_rows=16。

结论:参数化为 (40, 32) 与 (128, 16),使 cap 真正绑定,并把 m 提高到 48、每专家行数互不相同。 · 已解决

expected_rows=None“保持旧几何”的措辞误导 documentation

BBuf:None 只保留旧 m-grid,G_BLOCK_SIZE 与 num_warps 仍会变,称 previous geometry 不准确。

结论:测试改名 test_unknown_row_count_keeps_the_previous_m_grid,PR body 中同类表述一并修正。 · 已解决

调优常量的硬件覆盖(H100/H20) 性能

BBuf:常量在 H200/B200 上调出,是否覆盖所有 SM90+ GPU?

结论:H100 在 torch 2.11/triton 3.6 与 2.13/3.7.1 两套工具链补测,收益成立;H20 无设备,留 benchmark 单命令复现入口。 · 已解决

K_BLOCK_SIZE 更名 G_BLOCK_SIZE 的原因 question

BBuf 询问为什么重命名这个 constexpr。

结论:因为单位从隐藏元素数变为 128 宽 scale 组数,tile 现在按“组”计量。 · 已解决

skewed 路由缺乏性能基准覆盖 性能

BBuf:expected_m 是均值,skewed 路由下 hot expert 可能只剩 m_grid=4/8 而串行化大量行,建议加 skewed masked_m perf case。

结论:benchmark 增加 skew 轴:一个 hot expert 拿 skew*rows、其余均分,并保持 dispatch 总量(即 expected_m)不变。 · 已解决

CI benchmark 子集未覆盖大专家数路径 测试

BBuf:CI 只跑 E8,未覆盖 num_experts >= 32 的 tile/cap 路径,而那是收益最大的区域。

结论:CI 子集加入 (3584, 56, 256) 代表形状,rows=32、skew=16 等组合进入默认参数表。 · 已解决

风险与影响

  1. 性能回归风险(主要风险):m-grid 启发式为经验拟合,PR 自述在低延迟 decode 区间距每格最优 0-5%,超高专家数 sub-4 µs 形状有 8-12% 离群,B200 在 8 行场景还有 0.03 µs 的微逆单元格;“只缩不放”设计(m-grid ≤ 历史 32)兜底了大部分 batch,但 tile 宽度与 warp 数对所有调用者无条件改变(即使 expected_rows=None),test_w4afp8_requant_geometry.py 明确锁定了这一契约。H20 未测,其余架构(尤其非 SM90 部件)常量可能非最优。
  2. 正确性风险低但有边界:Phase 2 前缀和映射是最可能 off-by-one 的地方,测试已覆盖空专家、全零 masked_m、cap 与 payload 边界,三厂商输出 bitwise 一致;但 PR 自述多节点端到端 decode 仍未跑。
  3. Triton codegen 版本敏感:收益依赖 8 B/thread 触发 v2.b32 的代码生成行为,triton 3.6 与 3.7.1 在 7168 形状上已出现 +12% → 持平的方向性差异,未来 Triton 升级可能改变当前最优几何(PR 自述 closing the remaining few percent 需要 autotuning)。

用户侧:W4AFP8 + DeepEP low-latency 部署(DeepSeek-V3/R1、Kimi-K3 等 W4A8 模型)decode 阶段显著受益:E2E A/B 显示 4x H200、EP4 场景吞吐 +0.83~1.48%、median TPOT -0.84~1.44%;并发越低、本地专家越多收益越大(kernel 级单行 12.1x)。无接口、flag 或精度变化,输出 bitwise 一致,升级无需配置改动。
系统侧:新增 3 个测试/基准入口——CPU 几何契约测试(base-a-test-cpu,约 5 s)、GPU 单元测试(base-b-kernel-unit,est 60 s)、kernel benchmark(base-b-kernel-benchmark,est 45 s,1-gpu-large)。
团队侧:沉淀了可复用的 kernel 调优方法学——bytes-per-lane 调优单位、双向上取整抵消、均值估计与溢出共享调度组合;所有常量集中定义并带注释,为后续 autotune 留了明确接口。

核心内核路径变更 启发式经验拟合 Triton codegen 版本敏感 H20 未覆盖 多节点 E2E 未验证

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论