Prhub

#50949 [CPU] Optimize routed FP8/MXFP4 MoE GEMM dispatch

原始 PR 作者 tianmu-li 合并时间 2026-08-08 12:56 文件变更 3 提交数 7 评论 8 代码增减 +110 / -5

执行摘要

CPU FP8/MXFP4 MoE 按块选 BRGEMM,吞吐提升约 8.7%

PR body 指出原实现用固定的全局 crossover 决定 routed CPU FP8/MXFP4 MoE 是否使用 BRGEMM:avg_M = std::max(int64_t(1), M * topk / E),只要平均每 expert token 数小,整层都退回 TinyGEMM,集中的 MoE 路由因此无法受益于 Intel AMX 的 BRGEMM。本 PR 的目的即『Remove the fixed global crossover from routed CPU FP8/MXFP4 MoE dispatch so that concentrated moe routing can benefit from brgemm』,并明确与 #49942(通过不同 API 扩展 CPU FP8 W8A8 支持)相互独立。Dense 与 shared-expert 分派保持不变,以控制回归面。

值得精读。核心看三点:一是 per-tile 的 BRGEMM 决策如何与全局 brgemm_release() 配对,这是 kernel 级条件执行资源管理的范本;二是用「空块提前 return + 逐块判定」处理路由稀疏性的做法;三是 review 中维护者坚持与 SGL 上游对齐、简化 API 的决策过程,对维护多份共享代码的团队有参考价值。

讨论亮点

维护者 bigPYJ1151 在复现收益(“Thanks @tianmu-li I can reproduce the improvement”)后提出三点设计意见,均被作者采纳:

  1. 用 bool 取代枚举:关于 PackedGemmBackend,原话是“I think use bool is enough. I want to reduce divergence with SGL side to facilitate code bump-up.”,最终回退为 bool 标志以降低与 SGL 上游同步成本。
  2. 新增双参重载:“I suggest to add new can_use_brgemm overloads to keep consistent manner.”,替代 moe_uses_brgemm 谓词,最终以 can_use_brgemm(int M, int N) 落地。
  3. 条件前移到调用方:“I think these conditions can be checked in fused_experts_fp_kernel_impl, then the shortcuts in brgemm are not required.”,避免内核内部冗余短路分支。

实现拆解

  1. 新增按块判定的 BRGEMM 谓词:csrc/cpu/sgl-kernels/gemm.h 在既有单参 can_use_brgemm<T>(int M) 基础上新增双参重载 can_use_brgemm<T>(int M, int N),语义为“支持 BRGEMM 且(M 超过小 GEMM 阈值,或 N 为不完整的 tail 块)”。这是逐块分派的最小公共谓词,后续所有决策都复用该函数。
  2. 逐块分派下沉到 fused kernel:csrc/cpu/sgl-kernels/moe_fp8.cpp 的 fused_experts_fp_kernel_impl 中删除全局 use_brgemm = can_use_brgemm(avg_M);stage 1(hidden @ w1)与 stage 2(activation @ w2)的 tile 循环内改用 const bool brg = can_use_brgemm<packed_t>(m_size, n_size),其中 m_size 是该 expert 块的实际 token 数、n_size 是实际 N 分块宽度,tinygemm_kernel 的 brg 参数随之逐块传入,小块自然落到固定行 TinyGEMM 微内核。
  3. BRGEMM 资源释放配对:同一线程多个 tile 可能分别走 TinyGEMM 与 BRGEMM,新增局部聚合标志 use_brgemm |= brg,在循环结束后统一调用 at::native::cpublas::brgemm_release(),保证 AMX 状态释放与使用严格配对;同时为 m_size == 0 的空块增加提前 return,避免无意义的 A 行拷贝与 GEMM 调用。
  4. 测试配套:tests/kernels/moe/test_cpu_quant_fused_moe.py 新增 _deterministic_expert_routes(block_sizes) 辅助函数,用连续 token 精确构造每个 expert 的块大小;新增 test_w8a16_block_fp8_cpu_fused_moe_small_expert_blocks 与 test_mxfp4_cpu_fused_moe_small_expert_blocks,block_sizes 取 (4, 5, 33),覆盖阈值 4 两侧(4 走 TinyGEMM、5 与 33 走 BRGEMM)以及跨多个 BLOCK_M 的大块,与浮点参考实现 ref_* 做 1e-2 容差对比。
  5. 演进与收敛:提交历史显示实现经历了“枚举后端 + 集中谓词 → 按 review 简化为 bool + 双参重载”的来回:最初引入 PackedGemmBackend 枚举与 moe_uses_brgemm,维护者 bigPYJ1151 以“减少与 SGL 上游分歧、便于 code bump-up”为由要求改回 bool,最终 commit 恢复了 use_brgemm 命名并删除多余 adapter。
文件 模块 状态 重要度
csrc/cpu/sgl-kernels/moe_fp8.cpp 融合内核 modified 5.95
csrc/cpu/sgl-kernels/gemm.h GEMM 调度 modified 4.96
tests/kernels/moe/test_cpu_quant_fused_moe.py MoE 测试 modified 6.26

关键符号

fused_experts_fp_kernel_impl can_use_brgemm(双参数重载) _deterministic_expert_routes test_w8a16_block_fp8_cpu_fused_moe_small_expert_blocks test_mxfp4_cpu_fused_moe_small_expert_blocks

关键源码片段

csrc/cpu/sgl-kernels/moe_fp8.cpp core-logic

核心分派逻辑所在:将全局平均 M 的 BRGEMM 决策改为逐 expert block 按实际 m_size、n_size 决策,并新增空块提前 return 与 use_brgemm 聚合释放标志。

// stage 1: intermediate_cache0 = hidden_states @ w1(FP8/MXFP4 共用)
// 本 PR 核心改动:BRGEMM 决策从「全局平均 M」下沉到「每个 expert block 的
// 实际 m_size、n_size」,集中路由的大块走 Intel AMX,小块回退 TinyGEMM。
parallel_2d(MB, NB, [&](int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1) {
  int tid = get_thread_num();
  scalar_t* __restrict__ A = A_tmp + tid * BLOCK_M * K;
  // 聚合标志:任一 tile 使用 BRGEMM 后统一 release,保证 AMX 状态配对
  bool use_brgemm = false;
​
  loop_2d<packed_t>(mb0, mb1, nb0, nb1, BLOCK_N * K,
                    [&](int64_t mb, int64_t nb, int64_t nb_offset) {
    int64_t n_size = std::min(2 * N - nb * BLOCK_N, BLOCK_N);
​
    // B 为 [K, n_size] 的 VNNI 打包格式,按 expert 定位权重与量化 scale
    int32_t expert_id = expert_ids[mb];
    const packed_t* __restrict__ B =
        packed_w1 + expert_id * stride_e + nb * BLOCK_N * stride_n;
    const param_t* __restrict__ Bs =
        w1s + expert_id * scale_size_N * scale_size_K +
        scale_offset_per_block(nb) * scale_size_K;
    const float* __restrict__ B_bias =
        with_bias ? w1_bias + expert_id * 2 * N + nb * BLOCK_N : nullptr;
​
    // 每个新 expert 的首块需要解包 VNNI 权重
    int32_t pre_expert_id = mb == 0 ? -1 : expert_ids[mb - 1];
    bool do_unpack = (mb == mb0) || (expert_id != pre_expert_id);
​
    // 空 expert 块直接跳过,避免无意义的 A 行拷贝与 GEMM 调用
    int64_t m_size = offsets[mb + 1] - offsets[mb];
    if (m_size == 0) {
      return;
    }
​
    // 逐块判定:M 超过小 GEMM 阈值 4 或 N 为 tail 块时走 BRGEMM
    const bool brg = can_use_brgemm<packed_t>(m_size, n_size);
    use_brgemm |= brg;
​
    if (nb_offset == 0) {
      // 仅首个 N 块加载 A,按排序后的 id 取原始 token 行
      const int32_t* A_ids = sorted_ids + mb * BLOCK_M;
      for (int64_t m = 0; m < m_size; ++m) {
        int32_t index = A_ids[m] / topk;
        copy_stub(A + m * K, input + index * K, K);
      }
    }
​
    // 同一内核同时承载 TinyGEMM 与 BRGEMM 两条路径,由 brg 逐块切换
    tinygemm_kernel<scalar_t>(
        /* A */ A,
        /* B */ B,
        /* C */ ic0 + offsets[mb] * 2 * N + nb * BLOCK_N,
        /* Btmp */ B_tmp + tid * B_tmp_size_per_thread + nb_offset * BLOCK_N * K,
        /* Ctmp */ C_tmp + tid * 2 * BLOCK_M * BLOCK_N,
        /* Bbias */ B_bias,
        /* scale */ Bs,
        /* M */ m_size,
        /* N */ n_size,
        /* K */ K,
        /* lda */ K,
        /* ldb */ n_size,
        /* ldc */ 2 * N,
        /* brg */ brg,
        /* block_size_K */ block_size_K,
        /* do_unpack */ do_unpack);
  });
​
  if (use_brgemm) {
    at::native::cpublas::brgemm_release();
  }
});
csrc/cpu/sgl-kernels/gemm.h core-logic

新增双参 can_use_brgemm(int M, int N) 重载,是逐块分派决策的公共谓词,并被 moe_fp8.cpp 调用。

// define threshold using brgemm (intel AMX)
// 单参版本:按行数 M 判定,用于 dense 等非 routed 场景;
// 各量化类型共用「支持 BRGEMM 且 M > 4」的阈值。
template <>
inline bool can_use_brgemm<at::Float8_e4m3fn>(int M) {
  return brgemm_supported() && M > 4;
}
​
// 双参版本(本 PR 新增):routed MoE 按每个 expert block 的实际 M、N 决策。
// N 不等于完整 block 的 tail 块也走 BRGEMM,避免 TinyGEMM 的固定行约束;
// 单参判断的 M > 4 阈值依然生效,小块保留 TinyGEMM 微内核。
template <typename T>
inline bool can_use_brgemm(int M, int N) {
  return brgemm_supported() && (can_use_brgemm<T>(M) || N != block_size_n());
}

评论区精华

BRGEMM 选择 API 与 SGL 上游对齐(enum 改 bool) 设计

bigPYJ1151 在 gemm.h 的 PackedGemmBackend 枚举上评论:“I think use bool is enough. I want to reduce divergence with SGL side to facilitate code bump-up.”

结论:作者按建议回退为 bool,并保留逐 tile 的 brg 切换,最终 commit 恢复 use_brgemm 聚合命名。 · 已解决

新增 can_use_brgemm(int M, int N) 双参数重载 设计

bigPYJ1151 建议去掉 moe_uses_brgemm 辅助谓词,改走与既有单参版本一致的风格:“I suggest to add new can_use_brgemm overloads to keep consistent manner.” 并指出 M > 0 的前置判断是多余的。

结论:作者采纳,最终 gemm.h 中新增双参模板重载,作为逐块决策的唯一入口。 · 已解决

条件检查前移到 fused_experts_fp_kernel_impl 设计

bigPYJ1151 在 gemm_fp8.cpp 的 tinygemm_for_small_m 上建议:“I think these conditions can be checked in fused_experts_fp_kernel_impl, then the shortcuts in brgemm are not required.” 即把分派决策收敛到调用方,避免内核内部加短路分支。

结论:作者回复 Done,最终 dispatch 决策集中到 fused_experts_fp_kernel_impl 的循环内完成。 · 已解决

风险与影响

  1. 性能回退点:基准 27 个点中最差为 GPT-OSS 并发 64 的 0.9867x,整体 geomean 虽为正,但高并发下存在约 1% 抖动;合并后若有更多回退报告,阈值 4 与 BRGEMM 选择条件需重新评估。
  2. 阈值硬编码:M > 4 阈值直接写在 gemm.h 的模板特化中,FP8 与 MXFP4 共用,未来调整需同步 SGL 侧。
  3. BRGEMM 资源配对:use_brgemm 聚合标志与 brgemm_release() 的配对依赖循环内 brg 计算路径的完备性,空块提前 return 若漏置标志会破坏配对(当前 stage 1/stage 2 均已覆盖)。
  4. 测试覆盖局限:新增测试 topk=1 且 K=N=128 对称形状,未覆盖 topk>1 或 N 不等于 K 的组合;且测试仅在 CPU-only 条件下运行,CI 需确保 CPU 构建覆盖这些用例。
  5. 影响范围:所有 CPU 部署的 FP8/MXFP4 MoE 模型(如 Qwen3-Coder、GPT-OSS)都会经过新分派路径,GPU、dense、shared-expert 分派不受影响。

对用户而言,CPU 上部署 FP8/MXFP4 量化 MoE 模型可获得约 8.7% 的生成吞吐提升(geomean),集中路由场景提升更明显(最佳 1.5954x),但极端并发下有约 1% 回退风险。对系统而言,改动局限于 CPU 后端 sgl-kernels 的量化 MoE kernel 路径(csrc/cpu/sgl-kernels/moe_fp8.cpp 与 gemm.h),不需要配置或部署配套变更。对团队而言,分派 API 与 SGL 保持一致,降低了后续 sgl-kernels 版本对齐(code bump-up)的成本;同时该 PR 建立了“逐块 kernel 选择 + 资源释放配对”的模式,可复用于其他量化 MoE 后端优化。

核心 kernel 路径变更 并发 64 存在约 1% 回退点 M > 4 阈值硬编码且跨量化类型共用 BRGEMM release 配对依赖逐块决策完备性 新增测试仅 topk=1 且 N=K 对称形状

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论