Skip to content

[Bugfix][Attention] Keep FlashInfer's plan() staging buffer pageable - #59493

Open
Sahil170595 wants to merge 1 commit into
vllm-project:mainfrom
Sahil170595:fix/flashinfer-plan-staging-race
Open

Sahil170595 wants to merge 1 commit into
vllm-project:mainfrom
Sahil170595:fix/flashinfer-plan-staging-race

Conversation

@Sahil170595

@Sahil170595 Sahil170595 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Purpose

Fixes a race where FlashInfer can overwrite a CPU planning buffer before the GPU has finished reading the previous plan. With MTP speculative decoding, the GPU can then use the wrong scheduling data and crash or hang. This addresses the staging-buffer race reported in #40756.

The change replaces FlashInfer's pinned CPU staging buffer with ordinary, pageable CPU memory when vLLM creates an attention wrapper. This lets CUDA stage the source data before the next plan reuses the buffer. It covers decode, prefill, DCP child wrappers and cascade child wrappers, and adds a regression test for both eager and CUDA graph decode.

Why it is needed

Repeated draft steps can plan the same wrapper before its previous GPU copy runs. A seq_lens.cpu() wait masks that overlap in the recorded main baseline, but the performance change in #57352 removes the wait for drafting. This fix protects the planning buffer independently of that wait. The fused drafting path in #58371 does not cover the native FlashInfer path that re-plans each step.

Why this is not a duplicate

Rechecked #40756 and open PR searches for the issue, _pin_memory_int_workspace_buffer and FlashInfer planning. No other matching vLLM staging-buffer fix was found. #42603 used an explicit stream wait; #57352 removes a wait. The other PRs referencing #40756 (#45005, #51508, #53450) address different failure mechanisms.

Validation

Previously recorded local results on an RTX 4080 Laptop under WSL2:

  • New queued-plan regression: 2 failed without the fix, 2 passed with it on FlashInfer 0.7.0.post1.
  • FlashInfer backend, DCP reorder and new regression tests: 31 passed, 5 skipped. The backend run used an ungated Llama config; changed-file pre-commit checks passed.
  • Standalone reproductions on FlashInfer 0.6.16.post3 and 0.7.0 showed stale plans and kernel hangs without the fix, and correct plans with pageable staging.
  • Qwen3.5-0.8B with MTP and fp8 KV cache: outputs and draft acceptance matched the local [Perf][Attention] FlashInfer: skip the seq_lens copy with speculative decoding #57352 comparison build across all three workloads. No consistent throughput regression was observed on the tested hardware. The real-model crash itself was not reproduced on this laptop.

These are the existing author-recorded results; GPU tests were not rerun during this source review. Hosted pre-commit is currently skipped because the repository's contributor eligibility check fails.

Limits

The patch adds no explicit stream synchronization. Pageable copies may still block or synchronize inside the CUDA driver, so the measurements below are not a guarantee for every device or driver (CUDA synchronization behavior). FlashInfer MLA wrappers are outside this change and have not been validated on Hopper.

Test commands, reproductions and model measurements

Test Plan

  1. New regression test, tests/v1/attention/test_flashinfer_plan_staging.py: builds a FlashInferMetadataBuilder, holds the GPU with torch.cuda._sleep, plans its decode wrapper twice through fast_plan_decode, and compares the workspace the first launch reads against a clean first plan. It also asserts the second plan was issued while the first copy was still queued, so it cannot pass vacuously. Covers the eager and CUDA graph decode wrappers.
  2. Standalone repro against FlashInfer 0.6.16.post3 and 0.7.0, comparing candidate fixes.
  3. plan() host time with a pinned vs pageable staging buffer while the GPU is held busy.
  4. Existing FlashInfer backend tests and pre-commit.
  5. End to end: Qwen3.5-0.8B with MTP, FlashInfer, fp8 KV cache, on main, main + [Perf][Attention] FlashInfer: skip the seq_lens copy with speculative decoding #57352, and main + [Perf][Attention] FlashInfer: skip the seq_lens copy with speculative decoding #57352 + this PR, with a local-only probe counting plans issued while the wrapper's previous copy was still queued.

All runs on an RTX 4080 Laptop (sm_89) under WSL2.

Test Result

(1) On this branch (FlashInfer 0.7.0.post1): fails without the fix for both wrappers, at assert torch.equal(seen_by_a, plan_a) (the first launch reads the second plan), and passes with it.

.venv/bin/python -m pytest tests/v1/attention/test_flashinfer_plan_staging.py
without the fix:  2 failed
with the fix:     2 passed

(2) Identical on FlashInfer 0.6.16.post3 and 0.7.0:

arm host blocked before 2nd plan first launch reads result
no guard, GPU busy 0 ms 2nd plan stale plan; launching the kernel hangs (every run: 3/3 on 0.6.16.post3, 1/1 on 0.7.0)
stream sync (as in #42603) ~410 ms 1st plan correct, serializes
event wait on the 1st copy ~413 ms 1st plan correct, serializes
pageable staging (this PR) 0 ms 1st plan correct

(3) Median host time per plan(), 200 back-to-back plans behind a ~1 s GPU stall (the GPU was still stalled after the loop, so no call waited on the stream):

plan pinned pageable
decode, batch 8 / 64 / 256 / 512 0.052 / 0.050 / 0.053 / 0.054 ms 0.050 / 0.049 / 0.049 / 0.054 ms
prefill, 16×512 / 64×256 tokens 0.166 / 0.323 ms 0.176 / 0.337 ms

(4) tests/v1/attention/test_attention_backends.py -k flashinfer, tests/v1/attention/test_flashinfer_dcp_spec_reorder.py and the new test: 31 passed, 5 skipped (run locally with the ungated NousResearch/Meta-Llama-3-8B config in place of the gated meta-llama/Meta-Llama-3-8B). pre-commit passes on the changed files.

(5) Run on baseline 4bb804c with FlashInfer 0.7.0 and Model Runner V2, with #57352 applied in the local comparison build at 5cf5be4c2. #57352 is still open upstream; "main" in the table means that recorded baseline. "Queued" counts plans issued while the same wrapper's previous copy had not run yet, "changed" counts those that changed the staged bytes, and "layout changed" counts those that also changed plan_info (only measured in the last workload).

workload build plans queued changed layout changed output tok/s draft acceptance outputs
48 req × 768 tokens, k=3 main 1132 0 0 2127, 2323 0.7763
main + #57352 1132 448-454 118-119 2283, 2340 0.7768
main + #57352 + this PR 1132 449 117-119 2300, 2330 0.7768 same as row above
96 req, random output lengths, k=3 main + #57352 1328 459-463 190-197 2258, 2240, 2360 0.7450
main + #57352 + this PR 1328 428-498 187-206 2264, 1737, 2374 0.7450 same as row above
64 req up to ~25k tokens, k=5 main + #57352 2226 1354-1381 692-698 8 601, 655 0.6105
main + #57352 + this PR 2226 1309-1358 688 7 655, 649 0.6105 same as row above

Credit to @brasrox for identifying pinned planning-buffer reuse in #40756, and to @sempi for corroborating runs.

AI assistance (Claude) was used for the investigation, patch and tests. Codex assisted with the subsequent source review and description cleanup.


Essential Elements of an Effective PR Description Checklist
  • Purpose and related issue.
  • Test plan and commands.
  • Test results and model comparison.
  • (Optional) Documentation update for a new model or example.

FlashInfer's plan() writes its scheduling metadata into a host buffer
that each wrapper reuses, then copies it to the GPU asynchronously.
The buffer is pinned, so the copy reads it only when the GPU reaches
the copy. The drafter plans the same decode wrapper once per draft
step, so unless something in between waits for the GPU, a launch
still queued behind earlier work runs with the next step's metadata
under its own plan_info offsets, and the split-KV merge can index out
of range (vllm-project#40756).

Replace the staging buffer with a pageable one whenever vLLM creates a
wrapper, as vllm-project#32799 and vllm-project#54660 already do for vLLM's own reused host
buffers. A copy from pageable memory reads its source when it is
issued, so no sync is needed.

Signed-off-by: Sahil Kadadekar <sahilkadadekar@gmail.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.

@Sahil170595

Copy link
Copy Markdown
Contributor Author

@benchislett on #42603 you said forcing a synchronization was an unacceptable fix until a root cause was identified. This PR has one for the staging-buffer race reported in #40756: FlashInfer's plan() writes its metadata into a single pinned host buffer per wrapper and copies it to the GPU asynchronously, so when the drafter re-plans the same wrapper before that copy executes, the earlier launch reads the later plan. The fix makes that staging buffer pageable, so the copy captures its source when it is issued, and adds no explicit sync. The new regression test reproduces the stale read deterministically (fails without the fix, passes with it). On main the overlap is currently masked by the seq_lens.cpu() wait that #57352 removes. Would you be able to take a look, or point me to the right reviewer?

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

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant