Prhub

#24515 LPLB: linear-programming load balancer for MoE expert parallelism

原始 PR 作者 feliang-git 合并时间 2026-06-17 01:19 文件变更 19 提交数 43 评论 30 代码增减 +2324 / -14

执行摘要

新增 LPLB 线性规划负载均衡,优化 MoE 专家路由

当前的动态专家调度(dynamic)在路由偏斜数据集(如GSM8K)上会导致负载不均衡,限制吞吐量。LPLB通过将每层的令牌分配问题建模为线性规划,并利用所有EP rank的全局计数求解最优分配,从而缓解这一瓶颈。

这是一个高质量的PR,核心IPM内核设计、空rank处理、性能优化(融合核、预分配、流同步)都值得精读。建议架构师和MoE开发者重点关注,了解如何通过小规模LP优化专家负载分配。

讨论亮点

以下是review中的核心讨论:

  • d_max累加bug:gemini-code-assist指出IPM核心中d_max被覆盖而非累加,导致步长错误。作者在后续commit中修复为fmaxf累加。

  • 数值稳定性:建议对Cholesky对角fmaxf避免NaN,及d_max为零时的保护。作者采纳并在ipm.cuh中添加了钳位逻辑。

  • 启动约束放宽:ch-wan质疑--enable-dp-attention--moe-a2a-backend=deepep是否必需。作者实验后去除了这些强制校验(commit 326b956)。

  • 空rank逻辑集中:xutizhou建议将空rank参与逻辑移入empty_topk_output以减少重复。作者完成重构(commit 0c84e63)。

  • Math-DX依赖:ch-wan询问download-mathdx.sh,作者随后移除该脚本,改用nvidia-mathdx pip包和MATHDX_HOME环境变量(commit edf3217)。

  • torch参考实现:ch-wan要求提供torch IPM参考以比较数值差异。作者添加了torch_solver.py中的参考求解器和等价性测试(commit e9dd0dd)。

实现拆解

  1. JIT CUDA IPM求解器:在 python/sglang/jit_kernel/lplb/cuda_solver.pycsrc/lplb/ipm.cuh 中实现单SM融合IPM内核,使用cuBLASDx的GEMM和手写块Cholesky。通过 load_jit 按形状特化编译,CPU开销仅5-10µs。

  2. 预处理/后处理融合:编写 lp_prep.cuhlp_post.cuh,分别将8个和5个torch操作合并为单内核启动,减少内存流量。cuda_solver.py 中的 prep_lp_inputsextract_log2phy_prob 负责驱动。

  3. LPLBSolver封装:在 python/sglang/srt/eplb/lplb_solver.py 中定义 LPLBSolver 类,初始化时预计算LP约束矩阵,每batch调用 solve 方法:先对 topk_ids 进行 bincount,再执行 all_reduce 获得全局计数,然后调用求解器输出概率向量 log2phy_prob。设计上所有rank独立求解,无需广播。

  4. 空rank参与保护:修改 python/sglang/srt/layers/moe/topk.pyempty_topk_output,在启用LP时让空rank也调用 solver.solve() 参与 all_reduce,避免死锁。同时修改 deepseek_v2.py 的前向路径,使用统一的空rank逻辑。

  5. 启动参数与校验:新增 --ep-dispatch-algorithm=lp,在 server_args.py 中添加 check_lplb_server_args 校验(Hopper SM≥9.0、Math-DX可用、模型架构受支持)。提供 --lplb-require-lp--lplb-require-fused 严格模式确保无静默回退。

  6. 分布式测试:添加 test/registered/eplb/test_lplb_distributed.py,使用 torch.multiprocessing.spawn 启动2个CUDA进程,验证all-reduce一致性、空rank场景、数值等价于torch IPM参考,以及重均衡后求解器正确更新。

文件 模块 状态 重要度
python/sglang/jit_kernel/lplb/cuda_solver.py JIT 内核 added 9.25
python/sglang/srt/eplb/lplb_solver.py 均衡调度 added 9.25
python/sglang/jit_kernel/lplb/torch_solver.py 求解器入口 added 9.13
python/sglang/jit_kernel/lplb/shmem_budget.py 内存预算 added 9.03
test/registered/eplb/test_lplb_distributed.py 分布式测试 added 8.14
python/sglang/jit_kernel/utils.py 工具函数 modified 7.55
python/sglang/srt/layers/moe/topk.py MoE 路由 modified 7.27
python/sglang/srt/models/deepseek_v2.py 模型适配 modified 6.58

关键符号

solve_ipm prep_lp_inputs extract_log2phy_prob LPLBSolver.solve LPLBSolver._solve assert_lplb_supported_model empty_topk_output _init_fused_backend warmup get_mathdx_root get_mathdx_include_paths

关键源码片段

python/sglang/jit_kernel/lplb/cuda_solver.py core-logic

核心 IPM 求解器的 JIT CUDA 实现,包含 solve_ipm、prep_lp_inputs 等关键函数,是 LPLB 的性能基础。

def solve_ipm(
    A: torch.Tensor, b: torch.Tensor, c: torch.Tensor,
    num_iters: int = DEFAULT_NUM_ITERS,
    result: torch.Tensor | None = None,
) -> torch.Tensor:
    """Run the fused single-SM IPM kernel using cuBLASDx GEMMs and a hand-written
    block Cholesky for the POSV.  All state is in shared memory."""
    # 确保输入在 CUDA 上且为 float32 类型
    assert A.is_cuda and b.is_cuda and c.is_cuda
    assert A.dtype == torch.float32
    nc, nv = A.shape
    assert b.shape == (nc,), f"b shape mismatch: {b.shape} vs ({nc},)"
    assert c.shape == (nv,), f"c shape mismatch: {c.shape} vs ({nv},)"
​
    # 编译或从缓存获取指定形状的 IPM 模块
    module = _ipm_module(nc, nv, DEFAULT_BLOCK_DIM, num_iters, _sm_ver())
    # 若未提供输出缓冲区则自动分配(节省 ~20 µs 分配时间)
    if result is None:
        result = torch.empty(nv, dtype=torch.float32, device=A.device)
    # 启动单 block 的内核;所有 LP 状态驻留在共享内存中
    module.ipm_solve(A, b, c, result)
    return result

评论区精华

IPM 内核 d_max 累加 bug 正确性

gemini-code-assist 指出 `d_max` 被覆盖而非累加,导致步长计算错误。

结论:作者修复为正确的 `fmaxf` 累加。 · 已解决

数值稳定性改进(Cholesky、步长) 正确性

建议对 Cholesky 对角 `fmaxf` 避免 NaN,以及 `d_max` 为零时的保护。

结论:作者采纳并添加钳位逻辑。 · 已解决

是否必须 dp-attention 和 deepep 设计

ch-wan 质疑 `--enable-dp-attention` 和 `--moe-a2a-backend=deepep` 的必要性。

结论:作者实验后去除强制校验,LP 不在强依赖它们。 · 已解决

空 rank 逻辑集中到 empty_topk_output 设计

xutizhou 建议将重复的空 rank 参与逻辑移到 `empty_topk_output`。

结论:作者完成重构,统一了 deepseek_v2.py 中的空调用。 · 已解决

Math-DX 依赖与下载脚本 other

ch-wan 询问 `download-mathdx.sh` 的用途。

结论:作者移除该脚本,改用 `nvidia-mathdx` pip 包和 `MATHDX_HOME` 环境变量。 · 已解决

风险与影响

  1. 新依赖Math-DX:若GPU缺少cuBLASDx头文件,服务启动时直接报错,无静默回退;用户需手动安装nvidia-mathdx或设置MATHDX_HOME

  2. GPU硬件限制:仅支持Hopper及以上(SM≥9.0),旧GPU(如A100)无法使用;assert_fits在共享内存超限时也会中断。

  3. 模型兼容性有限:仅验证了DeepSeek-v2系列等少数架构,其他MoE模型如果启用LP可能因空rank路径不同而触发下游all_reduce死锁;assert_lplb_supported_model在初始化时阻止不支持的模型。

  4. JIT编译时间:首次编译每个形状的LP内核需20-40秒,已通过预热机制缓解;但若预热失败(如编译错误)会导致启动失败。

  5. 空rank死锁风险:若新模型引入时未正确处理空rank路径,可能导致all_reduce挂起;分布式测试覆盖了基本场景,但仍有遗漏可能。

  6. 性能回归:在非路由偏斜或预填充受限工作负载上,LP求解可能带来额外开销,实测MMLU和ShareGPT的吞吐量变化在±0.5%内,但不排除更极端场景。

  • 用户:使用--ep-dispatch-algorithm=lp需要安装新依赖和Hopper GPU;不支持其他MoE架构。受益用户主要是DeepSeek等大模型部署者,在路由偏斜场景下可获5%+吞吐提升。

  • 系统:初始化时间增加(JIT编译+矩阵预计算),但默认不启用;运行时每MoE层增加约76μs GPU求解时间,但通过融合和预分配控制了开销。

  • 团队:需维护4个新CUDA内核文件和Python封装;依赖管理新增Math-DX;需持续扩展模型兼容性列表;分布式测试增加了CI资源需求。

仅支持 Hopper GPU 必须安装 Math-DX 模型兼容性有限 JIT 编译耗时 空 rank 死锁风险

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论