fix(qwen3.8-27b/dual-fast): unpin FlashInfer decode buffer to bound the MTP c≥4 Xid 31 crash - #1051
Open
A1RM4X wants to merge 1 commit into
Open
Conversation
…he MTP c>=4 Xid 31 crash The dual-fast slug (W4A8 + MTP n=4 + fp8 KV + prefix caching) dies in ~15s at concurrency 4: CUDA Xid 31 VIRT_READ_PDE, SIGABRT, 4x, on GA102 RTX 3090 Turbos. Root cause (from vllm-project/vllm#40756): the FlashInfer decode workspace buffer is pinned (pin_memory=True), so the MTP speculative-decoding drafter's per-step plan() refresh reads the buffer while an async H2D copy is still in flight (stale-plan race) -> corrupted batch_indices -> arbitrary VIRT_READ. Fix: pin_memory=False on the decode workspace buffer so the MTP refresh copies into pinned scratch first (the standard FlashInfer MTP-safe pattern). Decode-only (4 sites in flashinfer/decode.py) is the minimal sufficient fix; FI_PINQ_LIB_ALL=1 mirrors it across prefill/sparse/pod. Validated (4x RTX 3090 Turbo 96GB, TP=2, c=4, vllm 0.27.1 + flashinfer 0.6.16.post3): pristine dies ~15s (Xid 31); patched survives 425s clean (OK=473, ERR=0, Xid count frozen). Performance-neutral (<1% delta at c=1, within CV). No quality impact (throughput-only). Closes: n/a (upstream fix pending at flashinfer-ai/flashinfer) Fixes: vllm-project/vllm#40756 (root cause identified)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Slug:
vllm/qwen38-27b-dual-fast(W4A8 + MTP n=4 + fp8 KV + prefix caching, hybrid GDN)Upstream: vllm-project/vllm#40756 (thread comments 2026-08-17 — brasrox retraction + sempai SM86 corroboration)
Problem
This slug crashes the engine in ~15 s at
MAX_NUM_SEQS >= 4with the drafter on:Root cause (vllm#40756, 2026-08-17 thread): FlashInfer
plan()reuses one pinned host buffer per wrapper and copies it out asynchronously with nothing guarding it. The MTP drafter re-plans the decode wrapper K−1×/step, so a stale plan feeds the split-KV merge (PersistentVariableLengthMergeStatesKernel) a garbage row (observed: row 33,621 of an 85-row buffer) → Xid 31. It needs ≥4 genuinely concurrent in-flight MTP requests to lose the race; c≤3 and theSPEC_N=0path never crash.The prior documented mitigations were
SPEC_N=0(kill the drafter) orMAX_NUM_SEQS=1(no concurrency). This patch fixes the crash at full async speed so the drafter and concurrency can coexist.The fix
New patch
models/qwen3.8-27b/vllm/patches/vllm-flashinfer-decode-pin/— an idempotent, marker-gated Python patcher +install.sh, run from the compose entrypoint beforevllm serve. It flipspin_memory=True,→pin_memory=False,on the_pin_memory_int_workspace_bufferallocations inflashinfer/decode.py(the decode wrapper MTP re-plans).pin_memory=Falseforces a synchronous host→device plan copy each step, closing the stale-read window.Safety (matches this repo's "never serve unpatched" convention):
import flashinfer→ no-ops if not installed (a non-FlashInfer backend config boots clean).count(pin_memory=True,)must equal the number of_pin_memory_int_workspace_buffer* = torch.empty(allocs in each touched file, else hard-fail (exit 2) — the compose refuses a half-patched serve.FI_PINQ_LIB_ALL=1also unpins prefill/sparse/pod (the validated full mirror); default is decode-only.Validation (v0.27.1 / flashinfer 0.6.16.post3 / 2× RTX 3090 SM86, TP=2)
install.sh→decode.py: 4 pin_memory True->False (py_compile OK), idempotent re-runalready patched, decode 0/4 and the other three 4/0.What this does and does NOT change — deliberately
MAX_NUM_SEQS=1because of the separate W4A8 16K-prompt OOM documented in the compose header (peak 23,872 MiB at N=2, 1.75 GB over the 0.90 budget). That is a different constraint; this patch does not touch it. At the shipped N=1 default the patch is inert. It becomes load-bearing the momentMAX_NUM_SEQSis raised — then the drafter and concurrency coexist without the Xid.Gate status (honesty section — per CONTRIBUTING)
test-patch-attribution.shtest-compose-status-drift.shdocker compose config)bash scripts/report.sh --full(rig + verify-full + verify-stress + soak-continuous + bench)Why the full battery isn't attached: this PR changes one load-bearing patch on one slug; the compose's serving config (weights, sampler, KV, TP) is byte-identical to what's already validated on this slug. The patch is verified to (a) apply cleanly in the pinned image, (b) not change the c=1 path measurably, and (c) eliminate the c=4 crash. I have not run the repo's
report.sh --full/ 8-pack here, so I'm flagging it as the remaining validation rather than claiming it — the maintainer or a 2×3090 community rig should runreport.sh --fullas the merge gate. Known open question the patch does NOT resolve: whether the W4A8 16K-prompt OOM also clears at higher concurrency (separate from the Xid).Attribution
Root-cause analysis and the c=4 reproduction/validation are mine (A1RM4X, 2× RTX 3090 rig). The bug is upstream's — the fix is a one-file change in flashinfer (unpin the decode wrapper's plan buffer, or guard the async copy-out); I've packaged it as a vendored patch in the club-3090 shape per CONTRIBUTING. Credits where due: the 2026-08-17 vllm#40756 thread comments (brasrox's retraction + sempai's SM86 corroboration) are what identified the decode wrapper as the buffer.
Files
Drop-when
When flashinfer lands a fix for the pinned-buffer plan reuse (a
pin_memory=Falsedefault on the decode wrapper, or a sync guard on the async copy-out) and the pin moves past it: remove the compose mount + entrypoint call + this patch dir, and retire thepatches.ymlrow.