fix(qwen3): size decode page indices for prefix-shared views#482
Merged
xiaguan merged 2 commits intoJul 2, 2026
Merged
Conversation
added 2 commits
July 2, 2026 04:47
The concatenated per-request page-index list is counted by reference, not by physical block: prefix-cached blocks are shared, so N views holding the same cached prefix each list those page ids again. Sizing page_indices_d off the pool's physical block count therefore under-allocates under prefix sharing, and the overflow trips cudarc's copy assert — killing the qwen3-tp-rank-0 worker thread and wedging the engine permanently (openinfer-project#403, fault 1). Size the buffer for the reachable worst case instead — every decode row presenting a full-context view — and fail loud in sync_paged_meta if a view ever exceeds the context limit the buffers were sized for, so any future sizing bug surfaces as a step error, not a dead worker.
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.
Problem
BatchDecodeBuffers::page_indices_dis sizedtotal_blocks + max_bs, assuming the concatenated per-request page lists of a decode step are bounded by the physical pool. Prefix caching breaks that assumption: matched blocks are sharedImmutableBlocks, so N views holding the same cached prefix each list those page ids again — the concatenated list is counted by reference, not by physical block. Admission cannot catch it either:request_lifetime_blockscharges physical blocks, and sharing makes physical usage far smaller than the by-reference count.Once the sum crosses the buffer size,
sync_paged_meta'smemcpy_htodtrips cudarc'sdst.len() >= src.len()assert, killingqwen3-tp-rank-0; every later step fails withtensor-parallel worker step channel closedwhile/healthstays 200 — the permanent wedge of #403 (fault 1; fault 2, the blast radius, is #420's territory). Backtrace from a live repro onmain@1275262:This also explains the two puzzles in the issue report. A one-shot 160-request burst cannot reproduce: blocks only become matchable once sealed, and simultaneous prefills do not match each other — sharing needs sustained arrivals over an already-sealed prefix. And the 5070 Ti repro is intermittent because
scripts/bench_http_serving.py's 24-word vocabulary yields only 24 distinct prompts, so the shared double-count hovers right at the+ max_bsslack the old sizing left.With a shared prefix the failure is deterministic, not intermittent: warm one long prompt, then fire N concurrent requests with the same prompt (repro script in #403). Common prefixes are the serving norm — a system prompt is exactly this shape.
Change
page_indices_dfor the reachable worst case — every decode row presenting a full-context view:max_batch × ceil(max_context_tokens / page_size). The constructor takespage_sizeinstead ofmax_total_pages; the physical pool size no longer appears in the bound. For Qwen3-4B (bucket 256 × 2560 pages) this is 2.6 MiB of i32 vs ~98 KiB before — noise next to the pool it indexes, and the memory profiler builds the same buffers, so profiled KV sizing absorbs it automatically.sync_paged_metaif the concatenated list ever exceeds the buffer (reachable only if a view exceeds the context limit the buffers were sized for): a stepErrinstead of a dead worker thread.shared_prefix_views_are_counted_by_reference: N views listing the same physical pages sync fine; oversized views return the error instead of tripping the assert. GPU-only, no weights needed.docs/models/qwen3/prefix-cache.md: recorded the by-reference page accounting rule so the next concatenated-page-list consumer doesn't re-derive it.Verification (H100 80GB, sm_90, CUDA 12.4, Qwen3-8B serving / Qwen3-4B gates)
Repro A/B — warm a 3000-word prompt, then 256 concurrent identical requests (max_tokens 64):
/health200Gates:
(
--workspace --libis not runnable on this box —openinfer-cuptineeds CUPTI headers the CUDA 12.4 install lacks; the per-crate runs above cover every touched crate.)Serving A/B, same box/base,
vllm bench serverandom 256-out conc=64, N=200 (allocation-only change — expected noise, observed noise):Notes for review
decode_buffers.rs:116has the same physical-pool sizing. It is unreachable today (no prefix cache on that line yet), so it is flagged for the qwen35 prefix-cache design (qwen35: prefix caching needs a design for recurrent state (discussion) #257) rather than changed here.ensure!is defense in depth: with the honest bound it should never fire; if it does, it composes with fix(scheduler): contain worker fatal errors #420's containment (step error, not dead worker).🤖 Generated with Claude Code