fix(qwen35) span the whole KV pool for slot-mapped verify attention - #652
Open
davide221 wants to merge 4 commits into
Open
fix(qwen35) span the whole KV pool for slot-mapped verify attention#652davide221 wants to merge 4 commits into
davide221 wants to merge 4 commits into
Conversation
With --kvflash the KV cache lives at pool slots and verify_batch builds its attention mask in slot space over the entire pool. The flash-attention view, however, was still sized from the logical context length (kv_start + n_tokens, rounded to the 256 stride). Those two disagree: slot indices are not ordered by logical position, so the view could end below slots the mask still marked visible. Attention then read rows that were never written and the softmax row degenerated, which surfaced as an argmax of -1 for every verify row past the first. The symptom was a hard failure. do_spec_decode saw the invalid seed, fell back to plain decode, and that fallback inherited the same state and failed too, so the request returned decode_failed. Reproduced on a Radeon AI PRO R9700 with Qwen3.8-27B and --kvflash auto (16384-token pool): prompts of 1556 and 6208 tokens died on the first speculative step, while 13148 and 26728 happened to survive because their logical extent covered the slots in use. Span the pool instead. The mask is sized from the same pool and is what restricts which slots are readable, so this is the bound that matches the caller's contract. The condition is scoped to the slot-mapped path: a set_rows KV write together with an explicit mask is a pair only kvflash verify produces, since the non-kvflash step-invariant write requires no mask and the paged path never reaches this branch. Measured on the same box, --kvflash auto, block-16 DFlash2, greedy, prompts that previously failed now complete: 1556 tokens 59.5 tok/s, 6208 tokens 43.3 tok/s, both recalling a label planted at the top of the context; 13148 and 26728 are unchanged at 39.0 and 38.6. Zero invalid-seed events across the sweep. The default (non-kvflash) path is untouched: HumanEval-10 is 144.61 tok/s with output sha a4467e9d, identical to before the change.
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 56 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 7 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
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.
The bug
With
--kvflash, a speculative request whose prompt is short enough returnsdecode_failed. On a Radeon AI PRO R9700 running Qwen3.8-27B with--kvflash auto(16,384-token pool) and the block-16 DFlash2 drafter:decode_faileddecode_failedThe server log shows:
Root cause
Under kvflash the KV cache lives at pool slots, and
verify_batchbuilds its attention mask in slot space over the entire pool. The flash-attention view was still sized from the logical context length (kv_start + n_tokens, rounded up to the 256 stride).Those two disagree. Slot indices are not ordered by logical position, so the view can end below slots the mask still marks visible. Attention then reads rows that were never written, the softmax row degenerates, and
ggml_argmaxreturns -1 for every verify row past the first.Instrumenting the failing case makes it concrete: at
kv_start=1556the view is 1,792 rows of a 16,384-row pool while the mask is 16,416 columns wide. Row 0 lands inside the view and returns a valid token; row 1 does not.That invalid token then propagates.
do_spec_decodesees the bad seed and falls back to plain decode, but the fallback inherits the same state and fails too, turning a recoverable condition into a failed request. Larger prompts survived only by accident: their logical extent happened to cover the slots in use.The fix
Span the whole pool. The mask is sized from that same pool and is what restricts which slots are readable, so it is the bound that matches the caller's contract.
The condition is scoped to the slot-mapped path. A
set_rowsKV write together with an explicit mask is a pair only kvflash verify produces: the non-kvflash step-invariant write requires!with_mask, and the paged path never reaches this branch.Validation
All on one R9700, gfx1201, ROCm 7.2, Qwen3.8-27B IQ4_XS with the DFlash2 q8_0 drafter, greedy.
--kvflash, HumanEval-10 measures 144.61 tok/s with output shaa4467e9d, identical to before.Note for reviewers: this fixes correctness, not speed. kvflash still measures slower than plain full attention on this hardware at long context (38.6 vs 44.4 tok/s at 27K), which is a separate question.