Skip to content

[Bugfix][MoE] Reuse the FlashInfer CUTLASS MoE workspace across layers - #60570

Open
janbernloehr wants to merge 5 commits into
vllm-project:mainfrom
janbernloehr:jbernloehr/flashinfer-cutlass-moe-workspace
Open

janbernloehr wants to merge 5 commits into
vllm-project:mainfrom
janbernloehr:jbernloehr/flashinfer-cutlass-moe-workspace

Conversation

@janbernloehr

@janbernloehr janbernloehr commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Overview

Reuse the MoE framework's workspace2 for FlashInfer CUTLASS scratch to avoid large per-call allocations during serving. Addresses #60471; this targets a post-startup OOM and does not claim to fix the profiling-time capacity failure in #47127.

Claims

  • Reserve CUTLASS scratch during profiling through workspace_shapes and reuse the framework's shared allocation.
  • Keep scratch disjoint from output; report zero for the unused workspace13 shape.
  • Include gathered DP input, the autotuning minimum and elastic DP limits via fi_moe_largest_bucket, with logical dimensions and quantization dtypes matching the kernel.
  • Retain compatibility with FlashInfer versions lacking caller-owned CUTLASS workspace support.

Validation

Revision 0dad84737f was validated on B200 by applying its production patch to the pinned image below. GPU correctness, matched serving and full GSM8K evaluation are complete.

  • GPU regression/correctness: 174 passed, 0 failures, 0 errors, 0 skipped. This includes 12 BF16/NVFP4 cases exercising the real framework allocator, capacity for gathered/elastic DP, reuse after locking, output/scratch non-overlap and CUDA graph replay with changed inputs. The remaining cases cover the capability gate, SwiGLU parameters, NVFP4 and FP8 correctness.
  • Matched serving on 8× B200: the unpatched control reproduced a 19.99 GiB CUDA OOM. The previous persistent-workspace implementation and both workspace2 runs each completed all 5 / 320 / 1,280 requests at concurrency 1 / 64 / 256 (1,605 total).
Output tokens/s Previous persistent workspace Framework workspace2, run 1 Framework workspace2, run 2
Concurrency 1 127.27 129.88 128.48
Concurrency 64 5,223.66 5,183.24 5,227.70
Concurrency 256 12,739.68 12,651.67 12,784.89

The revised runs span 5,183.24–5,227.70 tok/s at concurrency 64 and 12,651.67–12,784.89 at concurrency 256. No throughput speedup is claimed.

Both workspace2 serving logs confirm allocation through the MoE framework, completed CUDA graph capture and no CUTLASS workspace-sizing fallback or OOM. KV capacity remained 2,813,184 tokens per rank, matching the persistent-workspace control.

Full GSM8K: lm-eval 0.4.11, 1,319 questions, 5-shot, same DP8/EP/CUDA-graph settings. Each arm completed 1,319 HTTP-200 completion requests, with zero empty responses, non-200 responses, OOMs or CUTLASS workspace-sizing fallbacks.

Exact-match accuracy ± standard error Unpatched Framework workspace2 Difference
Flexible extract 93.404% ± 0.684 pp (1,232/1,319) 93.025% ± 0.702 pp (1,227/1,319) −0.379 pp
Strict match 92.722% ± 0.716 pp (1,223/1,319) 92.191% ± 0.739 pp (1,216/1,319) −0.531 pp

The observed decreases are smaller than each run's reported standard error. This single comparison does not establish statistical equivalence or rule out a small accuracy regression.

Stack: vllm/vllm-openai@sha256:f620665f46d536e42698e5649fa9eebc51e2b71ff0d5f09ee11ea7674d4c655e, vLLM 0.30.1rc1.dev616+g1a001d584, FlashInfer 0.7.0.post1, PyTorch 2.13.0+cu130. Model: nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4. TP=1, DP=8, EP and CUDA graphs enabled; memory utilization 0.9, max-model-len 9216, max-num-batched-tokens 16384, flashinfer_cutlass MoE, FLASHINFER attention. AIPerf used 8,000 input / 1,000 output tokens with unchanged flags across arms.

The test tree was taken from 630e30d8f7 with the revised test_flashinfer_moe.py from 0dad84737f; imports resolved to the patched installed package. The executed test selection was:

python3 -m pytest -v -rsfE -p no:cacheprovider --continue-on-collection-errors \
  tests/utils_/test_flashinfer_utils.py::test_cutlass_workspace_requires_both_apis \
  tests/kernels/moe/test_flashinfer_moe.py::test_flashinfer_workspace_reused_after_lock \
  tests/kernels/moe/test_flashinfer_moe.py::test_flashinfer_swigluoai_params_are_forwarded \
  tests/kernels/moe/test_flashinfer_moe.py::test_flashinfer_fp4_moe_no_graph \
  tests/kernels/moe/test_flashinfer.py::test_flashinfer_cutlass_moe_fp8_no_graph

These results validate the production patch on the pinned image, rather than a build of the entire newer PR base.

The evaluation invocation below normalizes the local model path and service port; the server used the served-model name nemotron-super-v3-120b-a12b:

lm_eval --model openai-completions \
  --model_args 'base_url=http://localhost:8000/v1/completions,model=nemotron-super-v3-120b-a12b,tokenizer=nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4,tokenizer_backend=huggingface,tokenized_requests=false,max_length=9216,eos_string=<|im_end|>' \
  --tasks gsm8k --num_fewshot 5 --limit 1319 \
  --output_path ./gsm8k-results --log_samples
Serving reproduction

The original nightly tag was removed from Docker Hub. Pull the same base
image by digest:

docker run --gpus all --ipc=host -p 8000:8000 \
  vllm/vllm-openai@sha256:f620665f46d536e42698e5649fa9eebc51e2b71ff0d5f09ee11ea7674d4c655e \
  nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 \
  --trust-remote-code \
  --gpu-memory-utilization 0.9 \
  --tensor-parallel-size 1 --pipeline-parallel-size 1 --data-parallel-size 8 \
  --enable-expert-parallel --max-model-len 9216 \
  --attention-backend FLASHINFER \
  --quantization modelopt_fp4 --moe-backend flashinfer_cutlass \
  --enable-prefix-caching --mamba-cache-mode align \
  --enable-chunked-prefill --max-num-batched-tokens 16384

Send 8,000-input-token / 1,000-output-token requests, ignoring EOS, at
concurrency 1 / 64 / 256 with 5 / 320 / 1,280 requests respectively.
The recorded results above used AIPerf. This equivalent vLLM benchmark command
is provided for reproduction but has not been run for this draft:

for concurrency in 1 64 256; do
  .venv/bin/vllm bench serve \
    --backend vllm --base-url http://localhost:8000 \
    --model nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 \
    --dataset-name random --random-input-len 8000 --random-output-len 1000 \
    --max-concurrency "$concurrency" \
    --num-prompts "$((concurrency == 1 ? 5 : concurrency * 5))" \
    --request-rate inf --ignore-eos
done

Details

Without caller-owned scratch, FlashInfer allocates and releases large GEMM buffers on each MoE invocation. That transient allocation contributes to memory profiling but must be allocated again during serving, where fragmentation can cause an OOM despite substantial reserved-but-unused memory.

In response to review, workspace_shapes now reports CUTLASS scratch in workspace2. The framework allocates output storage and scratch simultaneously. workspace13 reports zero because the backend does not use it; the framework still reserves the output. This removes the separate persistent-resource allocation and lets sequential operations share the framework's scratch storage.

Sizing uses logical hidden/intermediate dimensions, including unpacked NVFP4 K, gated/non-gated N and packed weight dtypes. Size queries are cached. The byte capacity is rounded up to the framework's workspace dtype. Different token counts can produce different scratch offsets after the output reservation; backing storage is shared and captured graph addresses remain stable after locking.

Older FlashInfer versions and size-query failures retain per-call allocation and its original fragmentation risk. Inputs exceeding the profiled capacity follow the framework's normal locked-workspace checks. No scheduler or model-runner changes are required.

This updates the existing PR following review. Draft #60617 independently overlaps with the workspace2 approach; no additional PR is being opened. The CUTLASS API comes from FlashInfer #4057; TRTLLM-gen integration is separate in #62948.

AI assistance was used to implement the change, prepare tests and draft this description.


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. GPU regression and matched serving passed; full GSM8K results and their limitations are reported above.

  • 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.

BEFORE SUBMITTING, PLEASE READ https://docs.vllm.ai/en/latest/contributing (anything written below this line will be removed by GitHub Actions)

Reserve caller-owned CUTLASS MoE scratch during profiling through the
workspace manager and reuse it for bounded batches and sequential layers.
Retain the existing allocation path when the workspace API is unavailable.

Add compatibility and workspace lifetime regression coverage.

Co-authored-by: Codex <noreply@openai.com>

Signed-off-by: jbernloehr <jbernloehr@nvidia.com>

@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.

@mergify mergify Bot added nvidia bug Something isn't working labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: 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.

🚀

@IwakuraRein IwakuraRein left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Thanks for the fix.

IwakuraRein added a commit to flashinfer-ai/flashinfer that referenced this pull request Oct 20, 2026
<!-- .github/pull_request_template.md -->

## 📌 Description

Adds a caller-owned workspace to the trtllm-gen fused-MoE APIs, the
trtllm-gen counterpart of `cutlass_fused_moe_workspace_size` /
`workspace_buffer=` (used by vllm-project/vllm#60570).

`trtllm_fused_moe_workspace_size` is implemented by adding a
`query_workspace_size` mode to each trtllm-gen moe call. It will not
execute the runner, but record the tensors allocated before the runner
call.

Test result:

Tested on B300, bf16 routed, 16384 tokens, H=7168, I=2048, 256 experts,
top-8, on B300, autotuning peak allocation drops to 896.8 MiB from
3,579.9 MiB (−2,683.1 MiB).

## 🔍 Related Issues

#4916

## 🚀 Pull Request Checklist

Thank you for contributing to FlashInfer! Before we review your pull
request, please make sure the following items are complete.

### ✅ Pre-commit Checks

- [x] I have installed `pre-commit` by running `pip install pre-commit`
(or used your preferred method).
- [x] I have installed the hooks with `pre-commit install`.
- [x] I have run the hooks manually with `pre-commit run --all-files`
and fixed any reported issues.

> If you are unsure about how to set up `pre-commit`, see [the
pre-commit documentation](https://pre-commit.com/).

## 🧪 Tests

- [x] Tests have been added or updated as needed.
- [x] All tests are passing (`unittest`, etc.).

## 🔬 Experimental Track

<!-- Only for PRs submitted under the experimental policy
(CONTRIBUTING.md → "Experimental APIs and Backends").
     Leave this section untouched for normal PRs. -->

- [ ] This PR is **experimental**: it adds or changes code under
`flashinfer/experimental/` and/or an `@flashinfer_experimental_api`.
Tracking issue: #
- [ ] The tracking issue names an owner, the reason for the experimental
path, and a graduation plan with a target release.
- [ ] Core changes are limited to a thin entry point (signature, shared
validation, feature-gate check, backend selection, handoff).
- [ ] Tests live in `tests/experimental/` and were validated on the
intended hardware; a runnable example is included.
- [ ] Nothing is registered in `flashinfer/aot.py`, and no experimental
backend is reachable from `backend="auto"` without
`FLASHINFER_ALLOW_EXPERIMENTAL_AUTO_BACKENDS=1`. (Calling an
`@flashinfer_experimental_api` or naming a backend explicitly is itself
the opt-in and needs no environment variable.)
- [ ] **Test scope declared below.** The experimental CI lane runs
exactly these targets, so keep them as narrow as the change allows.

<!-- Required for experimental PRs. Replace the commented lines below
with your targets.
Do not delete the fence or change its `experimental-tests` tag — the
experimental-track
watcher reads it verbatim to decide which targets to ask CI for. -->

```experimental-tests
# One target per line: a directory or a file. (A pytest ::selector is not
# supported -- the sharding runner cannot consume one.) Must be under
# tests/experimental/ and must exist. Delete these comment lines and add yours, e.g.
#
#   tests/experimental/test_my_backend.py
#   tests/experimental/my_backend/
#
# Declaring the whole tree (tests/experimental/) is allowed but means every
# experimental PR pays for every other feature's tests, in every matrix cell.
```

## Reviewer Notes

<!-- Optional: anything you'd like reviewers to focus on, concerns, etc.
-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added reusable workspace-buffer support for supported fused
Mixture-of-Experts operations and quantization modes.
  - Added an API to query required workspace size before execution.
- Workspace buffers can be reused across calls, with automatic fallback
when capacity is insufficient.
- Added validation for buffer size, device, layout, data type,
alignment, and unsupported distributed scenarios.

- **Documentation**
  - Documented the fused MoE workspace-size API.

- **Tests**
- Added coverage for sizing, reuse, fallback behavior, performance, and
invalid configurations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Siyuan Fu <siyuanf@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
self.use_dp = moe_config.moe_parallel_config.dp_size > 1
# Upper bound on input.size(0) seen by the kernel. With DP the MoE
# input can be gathered across DP ranks before the experts run.
self._max_workspace_rows = moe_config.max_num_tokens * max(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fi_moe_largest_bucket was recommended here. See #62948 (comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

implemented

Use fi_moe_largest_bucket for the persistent workspace reservation. Extend the existing reuse regression to check capacity for the autotuning floor and elastic DP while preserving buffered output comparison and reuse after locking.

Co-authored-by: Codex <noreply@openai.com>

Signed-off-by: jbernloehr <jbernloehr@nvidia.com>
@janbernloehr
janbernloehr requested a review from bnellnm as a code owner October 23, 2026 09:29
# potential communication op and is involved in the expert computation.
return (workspace1, workspace2, output_shape)

def _get_workspace_buffer(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How does this workspace differ from the ones allocated by the MoE framework? (workspace1, workspace2). Is this new workspace buffer additional memory that is required or could it be overlapped with the existing workspaces?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah looks like cutlass may use workspace_shapes since it only implements FusedMoEExpertsModular?.

BTW, I noticed that cutlass declared the workspace13, but never used it in the apply(): https://github.com/janbernloehr/vllm/blob/630e30d8f7127731e75bfd71cb593fef55db2992/vllm/model_executor/layers/fused_moe/experts/flashinfer_cutlass_moe.py#L245

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated in 0dad84737f: CUTLASS scratch now comes from the MoE framework’s workspace2, and the unused workspace13 shape is zero. Scratch remains separate from the output.

On the same pinned image with 8× B200, the unpatched control reproduced the 19.99 GiB OOM; both revised serving runs completed all 1,605 requests. All 174 GPU tests passed, including numerical correctness, locked workspace reuse, and CUDA graph replay.

Full GSM8K (1,319 questions, 5-shot): 93.40% stock → 93.03% workspace2, a −0.38 pp difference versus reported standard errors of about 0.7 pp per run. Both completed all requests with no empty responses, HTTP errors or OOMs. This single comparison cannot rule out a small accuracy regression. Full results are in the PR description.

— Drafted with Codex

Co-authored-by: Codex <noreply@openai.com>
Signed-off-by: jbernloehr <jbernloehr@nvidia.com>
@IwakuraRein IwakuraRein moved this to Ready in NVIDIA Nov 3, 2026
@IwakuraRein

Copy link
Copy Markdown
Contributor

/ci run

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

❌ @IwakuraRein, Only reviewers with write access can use CI commands before CI is delegated to the PR author.

@vadiklyutiy

Copy link
Copy Markdown
Member

/ci run

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #99741 for commit 2c19a77b32f7.

@pavanimajety pavanimajety added the ready ONLY add when PR is ready to merge/full CI is needed label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

✅ @janbernloehr, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • Run commands test your branch with every commit currently on its upstream target branch. If it is behind, the bot merges the target branch into it first (pull before pushing again). Append --allow-stale to a run command to test an outdated branch as is, at your own risk.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

@pavanimajety

Copy link
Copy Markdown
Member

/ci retry

@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

✅ Queued 1 failed job(s) for retry in Buildkite CI #99741.

@pavanimajety
pavanimajety enabled auto-merge (squash) November 4, 2026 16:56

This branch has not been deployed

No deployments
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 ready ONLY add when PR is ready to merge/full CI is needed

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

5 participants