Repository navigation
[Perf][MoE] Wrap GateLinear ll_bf16_gemm in custom op to prevent torch.compile branch freezing - #60681
[Perf][MoE] Wrap GateLinear ll_bf16_gemm in custom op to prevent torch.compile branch freezing#60681douglas-reid wants to merge 1 commit into
Conversation
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
|
This pull request has merge conflicts that must be resolved before it can be |
2a3447c to
c0d44e9
Compare
c0d44e9 to
021d79b
Compare
…h.compile branch freezing In GateLinear.forward, Tier 1 (ll_bf16_gemm, the CuTe DSL low-latency BF16->FP32 router GEMM for M <= 16 on SM90+) was gated by an inline Python conditional on x.shape[0]. Unlike Tier 2 (torch.ops.vllm.fp32_router_gemm_dispatch) and Tier 4 (torch.ops.vllm.rocm_bf16x3_router_gemm_dispatch), Tier 1 did not wrap its runtime x.shape[0] check in a custom op. When GateLinear is called from @support_torch_compile model code, vLLM's TorchCompileWithNoGuardsWrapper drops all Dynamo guards, so a plain Python branch on x.shape[0] is frozen at first trace: tracing first at M > 16 (e.g. M = 2048 during warmup) freezes the branch to False and permanently falls back to Tier 5 torch.mm(x, weight.T, out_dtype=torch.float32) during decode, while an M <= 16 first trace would freeze ll_bf16_gemm in at M = 8192. Tier 1 requires bf16 weights + out_dtype=torch.float32 + bias=False + unquantized + SM90+ and a gate called from @support_torch_compile model code. On main that is Gemma 4 (Gemma4Router.proj), Nemotron-H (force_fp32_compute=True keeps bf16 weights when specialized kernels exist), and MiMo-V2 when moe_router_dtype is bfloat16. After this change, Gemma 4 decode (M <= 16) and prefill (M > 16) use different GEMM kernels, so router logits may differ in the last ULP across batch sizes; this is Tier 1's pre-existing design (DeepSeek already behaves this way) and VLLM_BATCH_INVARIANT gating is handled at init by vllm-project#59985. The c1_pdl warmup gap is addressed by vllm-project#50177. Changes: 1. Register torch.ops.vllm.ll_bf16_router_gemm_dispatch(x, weight, max_tokens) so 0 < x.shape[0] <= max_tokens is evaluated dynamically at runtime outside the compiled FX graph, calling ll_bf16_gemm(x.contiguous(), weight) for 0 < m <= max_tokens and falling back to Tier 5 torch.mm(x, weight.T, out_dtype=torch.float32) otherwise. 2. Update test_gate_linear_torch_compile_dynamic_m_dispatch to drop guards via guard_filter_fn and run decode calls under torch.compiler.set_stance("fail_on_recompile") so the test fails on main and passes with the custom op. Signed-off-by: Douglas Reid <21148125+douglas-reid@users.noreply.github.com>
021d79b to
268e68a
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
…m op Backport of vllm-project#60681 (head 268e68a). Without it, torch.compile with guards dropped freezes the M<=16 branch at the first trace, so Gemma4's compiled router never runs tier 1. (cherry picked from commit 268e68a) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YT5r4gfFSMWQir3KGU7PP8 Signed-off-by: Alex Bilichenko <alexbi29@users.noreply.github.com>
Summary
In
GateLinear.forward, Tier 1 (ll_bf16_gemm, the CuTe DSL low-latency BF16->FP32 router GEMM forM <= 16on SM90+) was gated by an inline Python conditional onx.shape[0]:Unlike Tier 2 (
torch.ops.vllm.fp32_router_gemm_dispatch) and Tier 4 (torch.ops.vllm.rocm_bf16x3_router_gemm_dispatch), Tier 1 did not wrap its runtimex.shape[0]check in a custom op:GateLinearis called from a@support_torch_compilemodelforward(), vLLM'sTorchCompileWithNoGuardsWrapperdrops all Dynamo guards (skip_all_guards_unsafe/guard_filter_fn).x.shape[0] <= 16is evaluated once during the initialtorch.compiletrace (e.g.M = 2048during warmup) and frozen toFalsefor all subsequent decode batch sizes (M = 1..16), permanently bypassingll_bf16_gemmin favor of the Tier 5torch.mm(x, weight.T, out_dtype=torch.float32)fallback.M <= 16first trace would freezell_bf16_gemmin atM = 8192), which is why Tiers 2 and 4 already use custom ops.Scope & Trade-offs
bf16weights +out_dtype=torch.float32+bias=False+ unquantized + SM90+ and a gate called from@support_torch_compilemodel code. Onmain, that is Gemma 4 (Gemma4Router.proj), Nemotron-H (force_fp32_compute=Truekeepsbf16weights when specialized kernels exist), and MiMo-V2 whenmoe_router_dtypeisbfloat16. Most models migrated in [MoE] Use GateLinear for all MoE models #58234 lackout_dtype=torch.float32, usefp32gate weights, setbias=True(gpt-oss), or passgate=into the opaquemoe_forward_sharedcustom op (DeepSeek family) and are unaffected.M <= 16) and prefill (M > 16) use different GEMM kernels, so router logits may differ in the last ULP across batch sizes; this is Tier 1's pre-existing design (DeepSeek already behaves this way) andVLLM_BATCH_INVARIANTgating is handled at init by [Bugfix] Keep GateLinear batch invariant under VLLM_BATCH_INVARIANT #59985.c1_pdl(M = 1) warmup gap is addressed by [7/N][warmup][DSv4] Migrate router GEMM kernels #50177.Changes
vllm/model_executor/layers/fused_moe/router/gate_linear.py:torch.ops.vllm.ll_bf16_router_gemm_dispatch(x, weight, max_tokens)(mirroringfp32_router_gemm_dispatchandrocm_bf16x3_router_gemm_dispatch) so0 < x.shape[0] <= max_tokensis evaluated dynamically at runtime outside the compiled FX graph, callingll_bf16_gemm(x.contiguous(), weight)for0 < m <= max_tokensand falling back to Tier 5torch.mm(x, weight.T, out_dtype=torch.float32)otherwise.tests/kernels/test_ll_bf16_gemm.py:test_gate_linear_torch_compile_dynamic_m_dispatchto drop guards viaguard_filter_fnand execute decode calls (M in (1, 4, 16)) undertorch.compiler.set_stance("fail_on_recompile")after tracing atM = 32, emulatingTorchCompileWithNoGuardsWrapperso the test fails onmain(AssertionError: assert [] == [1, 4, 16]) and passes with the custom op.Benchmarks (NVIDIA H100 80GB HBM3)
1.
Gemma4DecoderLayerSubgraph (VllmCompile+ CUDAGraph, 30 Layers)Full
Gemma4DecoderLayerFFN + MoE + Router + 7RMSNorms subgraph (google/gemma-4-26B-A4B-it,K=2816, E=128, top_k=8) compiled withCompilationMode.VLLM_COMPILEand captured in aCUDAGraph:T)main(us/ layer)us/ layer)81.03 us77.18 us-3.85 us-115.5 us / step119.34 us115.61 us-3.73 us-111.9 us / step169.49 us166.29 us-3.20 us-96.0 us / step278.41 us275.34 us-3.07 us-92.1 us / step2. End-to-End
google/gemma-4-26B-A4B-itLatency (input_len=128, output_len=64)Measured on H100 80GB HBM3 (
dtype=bfloat16,VLLM_COMPILE+ full/piecewise CUDAGraphs, median of 10 iterations after 3 warmup iterations). Batch size 32 (M = 32 > LL_BF16_MAX_TOKENS = 16) uses Tier 5torch.mmin bothmainand this PR and is unchanged as expected:mainLatency (ms)ms)ms)295.31 ms288.06 ms-7.25 ms-115.1 us / decode step(-3.84 us / layeracross 30 layers)350.02 ms345.49 ms-4.53 msll_bf16_gemmsplit-K kernel (5 <= M <= 16)385.49 ms380.82 ms-4.67 msll_bf16_gemmsplit-K kernel (5 <= M <= 16)480.68 ms480.30 ms-0.38 msM = 32 > 16falls back to Tier 5torch.mm)3. Isolated
GateLinearRouter GEMM Under Guard-Freetorch.compile+ CUDAGraphIn
main,TorchCompileWithNoGuardsWrapperdrops all Dynamo guards (options={"guard_filter_fn": lambda x: [False for _ in x]}) and traces first atM = 2048, freezing Tier 5torch.mm(x, weight.T, out_dtype=torch.float32)into the compiled graph (plaintorch.compile(dynamic=True)withoutguard_filter_fnretains guards and recompiles, soguard_filter_fnis required to reproduce the frozen production graph):K -> E)Mmain(Frozen Tier 5torch.mm)ll_bf16_router_gemm_dispatch)2816 -> 128)16.54 us2.59 us2816 -> 128)26.48 us3.23 us2816 -> 128)46.49 us3.45 us2816 -> 128)86.46 us5.31 us2816 -> 128)166.75 us5.89 usReproducible Microbenchmark Harness
Test Plan
pytest tests/kernels/test_ll_bf16_gemm.py -v(138 passed on NVIDIA H100 80GB HBM3, plus verifiedtest_gate_linear_torch_compile_dynamic_m_dispatchfails whenGateLinear.forwardis reverted tomain)