Skip to content

[Bugfix][MoE] Size the permute scratch from its inputs - #60447

Merged
mgoin merged 4 commits into
vllm-project:mainfrom
mgoin:mgoin/moe-permute-scratch-grow
Oct 8, 2026
Merged

mgoin merged 4 commits into
vllm-project:mainfrom
mgoin:mgoin/moe-permute-scratch-grow

Conversation

@mgoin

@mgoin mgoin commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Overview

Stop predicting the MoE permute scratch capacity from FusedMoEConfig; size it from the inputs it actually receives, growing until the workspace manager is locked. This is intended as the root fix behind #55105 (DeepEP v2 + sequence parallelism) and #60423 (PCP), which each patch a different term of the same capacity formula for the assert n_token <= self.max_num_tokens failure in the Humming grouped path.

Claims

  • The Humming grouped experts no longer derive a row bound from max_num_tokens * dp_size * experts_per_token, so DP, sequence parallelism, PCP, and DeepEP v2's power-of-two receive buckets cannot make the scratch too small.
  • The scratch no longer has a hidden-state buffer or dtype, which removes the hidden_states.dtype == self.hidden_dtype mismatch on the prequantized (block-FP8 activation) path.
  • CutlassExpertsFp8 and the CUTLASS W4A8 experts get the same behavior. They sized the scratch with max_num_tokens and no dp_size factor.
  • One scratch is shared per (num_experts, num_local_experts, device) instead of one per (capacity, topk, hidden size, dtype).

Validation

Check Result
pytest tests/kernels/moe/test_moe_permute_unpermute.py tests/quantization/test_humming_mxfp4_block_fp8.py tests/kernels/moe/test_moe.py -k "permute or humming or cutlass or block_fp8" (1x B300) 906 passed (with tests/kernels/moe/test_cutlass_moe.py and -k "permute or humming or cutlass")
GSM8K, nvidia/Qwen3-30B-A3B-NVFP4, --moe-backend humming, VLLM_HUMMING_MOE_GEMM_TYPE=grouped_contiguous, 1x B300, eager (existing configs/humming/Qwen3-30B-A3B-NVFP4-humming.yaml) 0.8817 on 1319 questions (threshold 0.86), 0 invalid
GSM8K, same model, DP=2 + EP, allgather_reducescatter, CUDA graphs, --max-num-batched-tokens 3000 0.8969 on 1319 questions (threshold 0.86), 0 invalid
pytest tests/model_executor/model_loader/test_reload.py -k "test_reload_weights and FP8_DYNAMIC" (1x B300, VLLM_CUTLASS FP8 backend, in-place weight reload) 1 passed, 1 skipped; failed in CI before the shared-scratch fix
DeepSeek-V4.1-Flash with PCP on 8x H20 (SM90), run by @wangyicong52 (results in #59857) reported working; #60423 closed in favor of this PR
pre-commit on changed files, mypy-3.12 passed

Not validated:

  • The DeepEP v2 topology from Fix moe scratch buffer sizing for the TP>1 and DP>1 case #55105 is not reproduced here: DeepEP v2 is unavailable on the test machine (NCCL 2.29.7 < 2.30.4). @elvircrn, could you try this branch on your setup?
  • The prequantized Humming grouped path (block-FP8 activations before dispatch) was not run with real kernels; in-tree coverage of HummingGroupedExperts.apply is mock-based. It is guarded by dtype and shape assertions.

Details

Root cause. workspace13/workspace2 are sized from the actual M and grow until lock_workspace(). MoEPermuteScratch was instead sized up front from config, which requires knowing how many rows every prepare/finalize backend can return in every topology. That bound is a property of the dispatcher, not of the experts, and it has been wrong for PCP, for DeepEP v2 with non-power-of-two capacities, and for CUTLASS with DP.

Change.

  • MoEPermuteScratch holds only index metadata and gains reserve(expanded_rows). It reallocates while the workspace manager is unlocked and raises Scratch growth is not allowed after locking afterwards, matching the shared workspace's contract.
  • Permuted hidden states come from the caller's workspace. CUTLASS already passed a workspace2 slice. Humming's prequantized path now writes into quanted_gate_up_input, which is allocated with the right shape and dtype and is otherwise unused when w13 quantization is skipped, so this costs no extra memory.
  • The index buffers stay a persistent resource rather than moving into get_simultaneous: a second get_simultaneous call inside apply() would alias the live workspace13/workspace2, and token_expert_indices is an arange that would otherwise be refilled every call. They are small (seven int32 arrays over the expanded rows plus the sort workspace).

Tradeoffs and limitations.

  • A runtime shape larger than anything seen during warmup still fails, with the same contract and at the same point as workspace13.
  • moe_permute(scratch=...) without permuted_hidden_states now allocates that tensor per call. Both in-tree callers pass a buffer; benchmark_moe_permute_unpermute.py's scratch numbers are not comparable to earlier ones.
  • get_persistent_resource's docstring is loosened from "must not resize" to "must not resize once the manager is locked".
  • The CUTLASS experts use the same shared scratch as Humming. A scratch owned by the experts instance broke weight reload: the experts are rebuilt after the workspace is locked, so the new scratch was empty and could not grow (caught by test_reload_weights in CI build 93385).
  • Anything that first reaches a scratch-using path after the lock, without that path having run during warmup, now fails where the old fixed-size scratch would have allocated lazily.

Relation to open PRs. Not a duplicate: #55105 and #60423 extend the capacity formula for one topology each and conflict with each other; this removes the formula. The DeepEP v2 handle-capacity change in #55105 (next_power_of_2(ceil(max_num_tokens / sp_size))) is independent and still useful. Searches for MoEPermuteScratch and permute scratch workspace found no other open PR.

AI assistance was used to analyze the capacity mismatch, implement the change, and run the validation above.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

🤖 Generated with Claude Code

The Humming grouped and CUTLASS experts sized MoEPermuteScratch from
FusedMoEConfig, predicting how many rows the all2all backend would hand
them. That prediction missed PCP, DeepEP v2's power-of-two receive
buckets, and (for CUTLASS) DP entirely, and it keyed the hidden buffer on
the unquantized dtype.

The scratch now holds only index metadata and grows with the inputs it
sees until the workspace manager is locked, like the shared workspace.
Permuted hidden states come from the caller's workspace; Humming reuses
its idle w13 input buffer on the prequantized path.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: mgoin <mgoin64@gmail.com>
@mergify mergify Bot added performance Performance-related issues nvidia bug Something isn't working labels Oct 7, 2026
@mgoin
mgoin marked this pull request as ready for review October 7, 2026 17:52

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mgoin

mgoin commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

/ci run

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #93385 for commit 44ea22781411.

@vllm-agent

Copy link
Copy Markdown
Contributor

CI selector (shadow): 11 test steps (22 jobs) instead of 84 (109 jobs)

Shadow mode: this changes nothing about what CI runs. It shows what the evidence-based selector would pick for this PR, next to today's rules. How it works.

Feedback welcome: reply here if it would skip a step this change needs, or runs something unrelated.

steps (jobs) Today's rules Selector Would skip Would add
NVIDIA, CPU and others 84 (109) 11 (22) 77 (96) 4 (9)
AMD mirrors 87 (126) 2 (6) 85 (120) 0 (0)
Selector would run (11)
  • amd-fp8-moe-kernels-mi355
  • arm-cpu-test ×3
  • basic-models-test-other-cpu
  • batch-invariance-b200
  • cpu-quantization-model-tests
  • kernels-b200 ×3
  • kernels-moe-test ×5
  • kernels-root-misc-test-b200
  • model-executor
  • quantization ×4
  • quantized-models-test
Would skip (today's rules run them) (77)
  • amd-qwen3-next-mtp-async-eplb-accuracy
  • ascend-npu-test
  • async-engine-inputs-utils-worker
  • basic-correctness ×2
  • basic-correctness-cpu-offload
  • basic-correctness-cumem
  • basic-correctness-prefetch-offload
  • basic-correctness-sleep-mode
  • basic-models-tests-initialization
  • basic-models-tests-other
  • batch-invariance-h100
  • benchmarks-cli-test
  • cpu-language-generation-and-pooling-model-tests ×3
  • cpu-multimodal-config
  • cpu-params-env-tokenizers-parser
  • cpu-reasoning-renderers
  • cpu-tool-parsers
  • distributed-compile-rpc-tests-2-gpus
  • distributed-compile-unit-tests-2xh100
  • distributed-dp-tests-2-gpus
  • distributed-torchrun-shutdown-tests-2-gpus
  • e2e-core-1-gpu
  • e2e-core-large-memory
  • e2e-scheduling-1-gpu
  • e2e-scheduling-accuracy-1-gpu
  • entrypoints-integration-api-server ×4
  • entrypoints-integration-api-server-generate
  • entrypoints-integration-api-server-openai-chat_completion
  • entrypoints-integration-api-server-openai-completion
  • entrypoints-integration-llm
  • entrypoints-integration-multimodal
  • entrypoints-integration-pooling
  • entrypoints-integration-responses-api
  • entrypoints-integration-speech_to_text
  • fault-tolerance-e2e-2xh100
  • fusion-e2e-quick-h100
  • fusion-e2e-tp2-ar-rms-dynamo-partition-amd
  • fusion-e2e-tp2-ar-rms-inductor-partition-amd
  • fusion-e2e-tp2-b200
  • fusion-e2e-tp2-quick-h100
  • kernels-deepgemm-test-h100
  • kernels-fla-ops-test-b200
  • kernels-fusedmoe-layer-test-2-b200s
  • kernels-fusedmoe-layer-test-2-h100s
  • kernels-mhc-test-b200
  • language-models-tests-granite-l4-compatibility
  • language-models-tests-hybrid ×2
  • language-models-tests-standard
  • lora-tp-distributed ×4
  • metrics-tracing-2-gpus
  • multi-modal-models-standard-1-qwen2
  • multi-modal-models-standard-2-qwen3-gemma
  • multi-modal-models-standard-3-llava-qwen2-vl
  • multi-modal-models-standard-4-other-whisper
  • multi-modal-processor ×4
  • multi-modal-processor-cpu ×4
  • pytorch-compilation-dynamic-shapes
  • pytorch-compilation-passes-unit-tests
  • pytorch-compilation-unit-tests
  • pytorch-compilation-unit-tests-h100
  • pytorch-fullgraph-cudagraph-l4-compatibility
  • pytorch-fullgraph-test
  • quantized-moe-test-b200
  • regression
  • rl-entrypoints-tests
  • rust-frontend-distributed
  • samplers-multimodal-beam-search
  • samplers-test
  • v1-core
  • v1-executor-worker
  • v1-kv-connectors ×4
  • v1-kv-offload
  • v1-logits-oracle
  • v1-metrics-lmeval
  • v1-others-cpu
  • v1-sample
  • v1-spec-decode
Would add (today's rules do not run them) (4)
  • arm-cpu-test ×3 (code map)
  • cpu-quantization-model-tests (code map)
  • quantization ×4 (code map)
  • quantized-models-test (Python record)
AMD mirrors: would skip (85)
  • async-engine-inputs-utils-worker
  • basic-correctness ×2
  • basic-correctness-cpu-offload
  • basic-correctness-cumem
  • basic-correctness-prefetch-offload
  • basic-correctness-sleep-mode
  • basic-models-tests-extra-initialization ×14
  • basic-models-tests-initialization
  • basic-models-tests-other
  • batch-invariance-h100
  • benchmarks-cli-test
  • deepseek-v4-kernel-test-b200
  • distributed-compile-rpc-tests-2-gpus
  • distributed-compile-unit-tests-2xh100
  • distributed-dp-tests-2-gpus
  • distributed-model-tests-2-gpus ×3
  • distributed-torchrun-shutdown-tests-2-gpus
  • e2e-core-1-gpu
  • e2e-core-large-memory
  • e2e-scheduling-1-gpu
  • e2e-scheduling-accuracy-1-gpu
  • entrypoints-integration-api-server ×4
  • entrypoints-integration-api-server-generate
  • entrypoints-integration-api-server-openai-chat_completion
  • entrypoints-integration-api-server-openai-completion
  • entrypoints-integration-llm
  • entrypoints-integration-multimodal
  • entrypoints-integration-pooling
  • entrypoints-integration-responses-api
  • entrypoints-integration-speech_to_text
  • fault-tolerance-e2e-2xh100
  • fusion-and-compile-unit-tests-2xb200
  • fusion-e2e-quick-h100
  • fusion-e2e-tp2-ar-rms-config-sweep-h100
  • fusion-e2e-tp2-b200
  • fusion-e2e-tp2-quick-h100
  • kernels-b200 ×3
  • kernels-fla-ops-test-b200
  • kernels-fp4-moe-test-b200
  • kernels-fusedmoe-layer-test-2-b200s
  • kernels-fusedmoe-layer-test-2-h100s
  • kernels-mhc-test-b200
  • kernels-root-misc-test-b200
  • language-models-tests-extra-standard ×2
  • language-models-tests-granite-l4-compatibility
  • language-models-tests-hybrid ×2
  • language-models-tests-standard
  • lora-tp-distributed ×4
  • metrics-tracing-2-gpus
  • multi-modal-models-standard-1-qwen2
  • multi-modal-models-standard-2-qwen3-gemma
  • multi-modal-models-standard-3-llava-qwen2-vl
  • multi-modal-models-standard-4-other-whisper
  • multi-modal-processor ×4
  • openai-api-correctness
  • pipeline-context-parallelism-4-gpus
  • platform-tests
  • pytorch-compilation-dynamic-shapes
  • pytorch-compilation-passes-unit-tests
  • pytorch-compilation-unit-tests
  • pytorch-compilation-unit-tests-h100
  • pytorch-fullgraph-cudagraph-l4-compatibility
  • pytorch-fullgraph-test
  • quantized-moe-test-b200
  • regression
  • rl-entrypoints-tests
  • rust-frontend-distributed
  • samplers-multimodal-beam-search
  • samplers-test
  • spec-decode-draft-model ×4
  • spec-decode-eagle-1-deepseek-qwen
  • spec-decode-eagle-2-llama3-qwen-vl-other
  • spec-decode-mtp-deepseek-mimo
  • spec-decode-mtp-gemma4
  • spec-decode-mtp-qwen3-5
  • spec-decode-ngram-suffix
  • spec-decode-speculators
  • v1-core
  • v1-executor-worker
  • v1-kv-connectors ×4
  • v1-kv-offload
  • v1-logits-oracle
  • v1-metrics-lmeval
  • v1-sample
  • v1-spec-decode
AMD mirrors: would add (0)

none

7 changed files · base 32fbfa15e8 · head 44ea227814 · Python record: build 93039 at 1e5d0ea888 · kernel record: table 43b4aae (build 93244), map 43b4aae · not counted: 10 build steps, 5 A100 steps the generator no longer emits, 18 optional steps the selector would also run

@wangyicong52

wangyicong52 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Thanks @mgoin for the general solution. I validated this PR with DeepSeek-V4.1-Flash PCP on one 8x H20 (SM90) node and it looks good. The test result can be found in #59857. Since this PR is a more general solution, I have closed #60423.

Btw, could you take a look at #59857 when you have time? Any feedback or review comments would be greatly appreciated!

mgoin and others added 2 commits October 8, 2026 14:11
A weight reload rebuilds the experts after the workspace is locked, so a
scratch owned by the experts instance came back empty and could not grow.
Use the workspace manager's shared scratch, as Humming does.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: mgoin <mgoin64@gmail.com>
@mgoin

mgoin commented Oct 8, 2026

Copy link
Copy Markdown
Member Author

/ci run

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #93606 for commit 7f4131c90d32.

@mgoin
mgoin merged commit e3a29aa into vllm-project:main Oct 8, 2026
222 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working nvidia performance Performance-related issues

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants