Skip to content

fix(qwen35) span the whole KV pool for slot-mapped verify attention - #652

Open
davide221 wants to merge 4 commits into
qwen38-dsparkfrom
fix/kvflash-spec-verify-fa-view
Open

fix(qwen35) span the whole KV pool for slot-mapped verify attention#652
davide221 wants to merge 4 commits into
qwen38-dsparkfrom
fix/kvflash-spec-verify-fa-view

Conversation

@davide221

@davide221 davide221 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

The bug

With --kvflash, a speculative request whose prompt is short enough returns decode_failed. On a Radeon AI PRO R9700 running Qwen3.8-27B with --kvflash auto (16,384-token pool) and the block-16 DFlash2 drafter:

prompt tokens before after
1,556 decode_failed 59.5 tok/s
6,208 decode_failed 43.3 tok/s
13,148 40.5 tok/s 39.0 tok/s
26,728 38.6 tok/s 38.6 tok/s

The server log shows:

[spec-decode] invalid draft seed -1 after 2 emitted tokens; switching to AR
[server] chat DONE ... ok=false ... error=decode_failed

Root cause

Under 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 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_argmax returns -1 for every verify row past the first.

Instrumenting the failing case makes it concrete: at kv_start=1556 the 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_decode sees 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_rows KV 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.

  • Sweep above passes with zero invalid-seed events; a label planted at the top of the context is recalled at every length.
  • No change to the default path. Without --kvflash, HumanEval-10 measures 144.61 tok/s with output sha a4467e9d, 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.

Review in cubic

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.
@davide221 davide221 changed the title qwen35: span the whole KV pool for slot-mapped verify attention fix(qwen35) span the whole KV pool for slot-mapped verify attention Aug 23, 2026
@davide221
davide221 changed the base branch from main to qwen38-dspark August 23, 2026 22:57

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 56 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread server/src/qwen35/qwen35_target_graph.cpp

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread server/src/qwen35/qwen35_target_graph.cpp Outdated
Comment thread server/test/test_batched_gdn.cpp
Comment thread server/scripts/convert_dflash_to_gguf.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant