Skip to content

spec-decode-attn: cast the block id to int64 before the stride multiply (fixes #86) - #91

Merged
mhenrichsen merged 2 commits into
syv-ai:mainfrom
cpuchip:spec-attn-int64-blk
Sep 13, 2026
Merged

mhenrichsen merged 2 commits into
syv-ai:mainfrom
cpuchip:spec-attn-int64-blk

Conversation

@cpuchip

@cpuchip cpuchip commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #86. The block id loaded from the int32 block table in _spec_attn_partial is multiplied by the cache strides in int32, which overflows for high block ids in a large KV pool (astrapro40k's report: a 480k-token pool on a 64 GB card, illegal memory access on the first verify after a long cached prefix). The fix is the one line proposed there, blk = blk.to(tl.int64) after the load, placed in spec-decode-attn.patch where the kernel lives; the int8 per-token-head scale pointers index with the same block id, so the one cast covers them.

bench/test_spec_decode_bigpool.py reproduces it on a 24 GB card: a 2,600-block pool with one request placed at the top. Before the cast, Triton Error [CUDA]: an illegal memory access was encountered, every time, on a 4090 (WSL2) and a native 3090. After it, max error 0.0002 against an fp32 reference on the 4090 (0.0001 on the 3090) for bf16, and 0.0002 for fp8 where that path exists; the existing bench/test_spec_decode_attn.py byte-identical on both boxes; per-layer timing within single-run noise. The test's fp8 half has two designed skips and names which one fired, both verified on real hardware: a kernel without the fp8 path prints SKIP(by design): this kernel has no fp8 path (no k_descale argument) (both cards), and with the fp8 path present a 3090 prints SKIP(by design): no fp8 conversion on this card (CompilationError); any other exception there fails the run. The later patches that touch this file (spec-decode-int8-kv, spec-decode-scratch-within-budget) apply unchanged; the cast sits outside their hunk context.

#90 will rebase on this.

…ly (issue syv-ai#86)

The block id loaded from the int32 block table was multiplied by the cache strides in int32, which
overflows for high block ids in a large KV pool (astrapro40k, syv-ai#86: a 480k-token pool on a 64 GB card,
illegal memory access on the first verify after a long cached prefix). One cast after the load covers
the K/V pointers and the int8 per-token-head scale pointers, which index with the same block id.

bench/test_spec_decode_bigpool.py reproduces it on a 24 GB card: a 2,600-block pool with one request
placed at the top of it. Before: Triton Error [CUDA]: an illegal memory access was encountered.
After: max error 0.0002 against an fp32 reference on a 4090 (0.0001 on a native 3090), the existing
13-shape oracle byte-identical, per-layer timing within single-run noise. Fix as proposed in syv-ai#86.
@persano

persano commented Sep 11, 2026

Copy link
Copy Markdown

Tested on WSL2 / RTX 3090 (sm86), local image built from the repo Dockerfile with this PR cherry-picked onto main; verify.sh all PASS. On the int8 route (VLLM_SPEC_DECODE_ATTN=1, --kv-cache-dtype int8_per_token_head, pool 145,030 tokens) an 80K request with markers at 5K and 60K depth returned both exact, no CUDA fault. Before this patch that route was on our known-issue list because of #86; it isn't now.

@mhenrichsen

Copy link
Copy Markdown
Contributor

Blocking, and I think it is a slip rather than a disagreement: the cast is not in this branch. The commit is titled for it, the body describes it, but a9d9442 adds one file and nothing else.

$ git diff --stat origin/main...pr91
 bench/test_spec_decode_bigpool.py | 69 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 69 insertions(+)

$ grep -n int64 patches/spec-decode-attn.patch
(no output)

The kernel on main is still

blk = tl.load(bt_ptr + req * stride_bt + pos // BLOCK_SIZE, mask=k_ok, other=0)
slot = pos % BLOCK_SIZE
k_ptrs = k_ptr + blk[:, None] * stride_kb + ...

so #86 is live. Push the one line into patches/spec-decode-attn.patch and this goes in — the test is the hard part and it is already written and already run on two cards.

Two notes while you are in there:

The rest of the reasoning is right and I checked the arithmetic: bf16 stride_kb is 880 * 4 * 256 = 901,120 elements, so block ids above 2,383 overflow int32, and a 2,600-block pool with the request placed at the top clears that by 217 blocks. The int8 per-token-head scale pointers index with the same blk, so the one cast does cover them.

…ed (fix syv-ai#86)

a9d9442 titled and described the int64 block-id cast but committed only the
bench test, not the one-line patch edit, so syv-ai#86 stayed live on main. This adds
the cast to patches/spec-decode-attn.patch:

    blk = tl.load(bt_ptr + req * stride_bt + pos // BLOCK_SIZE, mask=k_ok, other=0)
    blk = blk.to(tl.int64)

so blk * stride_kb (and the int8 per-token-head scale strides, which index with
the same blk) compute in int64 for high block ids in a large KV pool. bf16
stride_kb is 880 * 4 * 256 = 901,120 elements, so block ids above ~2,383
overflow int32; the bench's 2,600-block pool with the request at the top clears
that by 217 blocks.

Falsified on card: without the cast bench/test_spec_decode_bigpool.py dies with
`Triton Error [CUDA]: an illegal memory access`; with it, `BIGPOOL OK`, max error
0.0002 (4090) / 0.0001 (3090). The test also gains a runtime note in its
pre-crash path so a killed process reads as the pre-fix outcome, not a broken test.
@cpuchip

cpuchip commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

You are right, and thank you for catching it. The commit was titled and described for the cast but only carried the test file, so #86 stayed live. That is a delivery gap, not a disagreement.

The cast is now in patches/spec-decode-attn.patch:

blk = tl.load(bt_ptr + req * stride_bt + pos // BLOCK_SIZE, mask=k_ok, other=0)
blk = blk.to(tl.int64)

Falsified on a 4090 before pushing: without the cast bench/test_spec_decode_bigpool.py dies with the illegal access; with it, BIGPOOL OK, max error 0.0002. The bench also gained a line in its pre-crash path so a killed process reads as the pre-fix outcome rather than a broken test, as you asked.

The cast covers the int8 per-token-head scale pointers too, since they index with the same blk, and your arithmetic is the one I used: bf16 stride_kb = 880 * 4 * 256 = 901,120 elements, block ids above ~2,383 overflow int32, and the 2,600-block pool with the request at the top clears that by 217.

#90 is rebased on the corrected commit.

@persano

persano commented Sep 12, 2026

Copy link
Copy Markdown

Verified on WSL2 / RTX 3090 (sm86). Local image qwen-syv:local-2026-09-11 = origin/main@1834917 + this PR's two commits (a9d9442 test, a9bbbbf cast) + #85 + #55; verify.sh OK, 0 failures.

bench/test_spec_decode_bigpool.py (2,600-block pool, request on blocks 2577..2599, L=20000):

bf16: max|ours-ref|=0.0001 OK
fp8: SKIP(by design): this kernel has no fp8 path (no k_descale argument)
BIGPOOL OK

Exit 0, no illegal memory access, the pre fix failure mode. Run in a temp container; a server side pool that size OOMs on 24 GB.

End to end on the live int8 route (VLLM_SPEC_DECODE_ATTN=1 --attention-backend TRITON_ATTN --kv-cache-dtype int8_per_token_head, pool 145,030 tokens): an 80K prompt with markers at ~5K and ~60K depth returns both exact, 209 s cold; warm recheck of the same prompt 4.4 s with 79,152/80,082 tokens cached, no fault. So the cast holds at high block ids on a real cached prefix, not just in the bench.

@mhenrichsen

Copy link
Copy Markdown
Contributor

Merging.

The branch now contains the change the title advertises — a9d9442 was test-only, a9bbbbf adds the cast — and I reproduced #86 and its fix on a 3090 with production stopped.

Repro, on the pre-fix installed kernel (vllm/v1/attention/ops/spec_decode_attn.py, no int64 anywhere):

$ python test_spec_decode_bigpool.py
RuntimeError: Triton Error [CUDA]: an illegal memory access was encountered   EXIT=1

With your cast inserted into that same file (__pycache__ and the Triton cache wiped, fresh process):

bf16: pool 2600 blocks, request on blocks 2577..2599, L=20000: max|ours-ref|=0.0001 OK
BIGPOOL OK   EXIT=0

bench/test_spec_decode_attn.py reports okcount=13 on both arms, with max|ours-ref| matching max|FA-ref| to the printed digit on every case. Timing A/B from the same bench, arms alternated within one session, 3 reps each, both Triton cache entries pre-warmed and discarded: at the production geometry (kv=60000, q_len=8) the cast arm is +0.98%; across the 12 geometries the deltas run +3.15% to -3.30% with mixed signs, i.e. noise, not a systematic cost. That mattered to check because single-user/start_qwen.sh sets VLLM_SPEC_DECODE_ATTN=1, so this is the spec-verify attention on the default path, not an opt-in.

Series integrity I ran locally against a pristine v0.28.0 checkout rather than trusting CI: bash patches/check_vllm_series.sh → 30 patches applied, 0 with an offset, git-apply pass clean.

What is still unproven, for the record:

  • The fp8 half of your reproducer SKIPs on this install by design (no k_descale argument; we carry the int8-KV patch instead). That the one cast also covers the quantized scale strides is verified by reading — the installed kernel's ks_ptr + blk * stride_ksb load uses the same blk — and not by a run. Nobody has exercised the int8 scale-stride path at high block ids on any card.
  • The bug is unreachable in this repo's own 24 GB config: the pool is ~66 blocks (57,669 tokens / 880) against an overflow threshold of 2^31 / (8804256) ≈ 2,383 blocks. So this run proves the kernel is buggy and that your cast fixes it; it does not show the 3090 deployment was ever affected. The fix is for large-card users, which is how I'd frame it in any release note.
  • Not tested under the CUDA-graph-captured path or with prefix caching on — standalone kernel only.

Credit where it's due: the synthetic reproducer is the reason this was a ten-minute verification instead of a day of guessing. Forcing the blocks to the top of an oversized pool is exactly the right way to make a 64 GB bug fire on a 24 GB card, and the arithmetic in the issue thread was right.

Two non-blocking follow-ups after this lands:

  1. docs/gotchas.md item 12 covers spec-decode-attn.patch and its query-cap trap but says nothing about the large-pool limit. One sentence — the cast, and that a pool over ~2,383 blocks was silently reading the wrong memory before it — keeps that file honest.
  2. Split-KV verify attention on the per-tensor fp8 KV cache (TRITON_ATTN, sm89+) #90 needs to sit on a9bbbbf, not a9d9442, so it doesn't inherit the gap. You said that's done; I'll re-check once this is in.

@persano

persano commented Sep 13, 2026

Copy link
Copy Markdown

On the two paths you list as untested: the 80K probe I ran was on the live server with the int8 KV route (VLLM_SPEC_DECODE_ATTN=1 --attention-backend TRITON_ATTN --kv-cache-dtype int8_per_token_head), prefix caching on and graphs captured. Markers at ~5K and ~60K depth both exact, warm recheck 4.4 s with 79,152/80,082 tokens cached, no fault. Block ids stayed well under the threshold on a 145,030 token pool, so it doesn't exercise the overflow, but it does cover the int8 scale stride path and the captured graph path that the standalone bench leaves out.

You're also right that the 24 GB config can't reach the bug. I'll correct my README, which had the int8 profile flagged as affected.

cpuchip added a commit to cpuchip/qwen38-27b-rtx3090 that referenced this pull request Sep 13, 2026
docs/pr-standard.md is the maintainer's bar, read off his review comments on syv-ai#90, syv-ai#91, syv-ai#93, syv-ai#99, syv-ai#100, syv-ai#101
and syv-ai#54: verified on the box; title, body, lead commit and diff say one thing; what is unproven stated;
promised docs in the same PR; sibling launchers swept; a series line for every patch; knobs registered in
envs.py; benches guard their hardware; no claim wider than its measurement; credit and asks specific.
scripts/pr-gate/pr_gate.py checks what text can check and prints the rest; reply_gate.py does the same for
a public comment; retracted.txt is the list of phrases we have withdrawn. Run against the three held PRs it
reports every hold he placed and one more of the same class (syv-ai#90 reads VLLM_SPEC_ATTN_DEBUG unregistered).
cpuchip added a commit to cpuchip/qwen38-27b-rtx3090 that referenced this pull request Sep 13, 2026
…ment beside item 44

Two follow-ups asked for in review. syv-ai#91's merge comment: item 12 covered spec-decode-attn.patch and its
query-cap trap and said nothing about the large-pool limit; it now records the int32 cast, that a pool over
about 2,383 blocks was silently reading the wrong memory before it, the synthetic test, and that the int8
prefill kernel had the same pattern. syv-ai#101's merge comment: item 44 gains the null-gap retirement, the 16 to
5 retained-block bound measured on the reference 3090, that the win is mtp/long shaped and inert on the
dflash2 fast profile, and the single-space blank context lines in that patch file that a whitespace trim
destroys.
cpuchip added a commit to cpuchip/qwen38-27b-rtx3090 that referenced this pull request Sep 13, 2026
…ment beside item 44

Two follow-ups asked for in review. syv-ai#91's merge comment: item 12 covered spec-decode-attn.patch and its
query-cap trap and said nothing about the large-pool limit; it now records the int32 cast, that a pool over
about 2,383 blocks was silently reading the wrong memory before it, the synthetic test, and that the int8
prefill kernel had the same pattern. syv-ai#101's merge comment: item 44 gains the null-gap retirement, the 16 to
5 retained-block bound measured on the reference 3090, that the win is mtp/long shaped and inert on the
dflash2 fast profile, and the single-space blank context lines in that patch file that a whitespace trim
destroys.
cpuchip added a commit to cpuchip/qwen38-27b-rtx3090 that referenced this pull request Sep 14, 2026
… fork; the syv-ai#91 and prefill int64 casts

Rebased over syv-ai#54 (patches/series), syv-ai#91, syv-ai#99 and syv-ai#101. patches/series lists the 0.29 set: the two files
0.29.0 retired (vllm-pr54282-draft-gumbel-salt, xgrammar-spec-terminated) leave the list and
offload-mtp-serve.patch (syv-ai#100) joins it after mamba-align-retire-null-gaps. The Dockerfile, README loop,
verify.sh and check_vllm_series.sh apply in series order, at --fuzz 0 as before, and the int8 knob check
asks the live registry.

Every patch file is re-exported from cpuchip/vllm qwen38/0.29 at c68ae347f. Content changes: the split-KV
verify kernel casts the block id to int64 before the stride multiply (syv-ai#86/syv-ai#91, the same three lines the 0.28
series has since e79596b), and the int8 prefill kernel casts at its three block-table loads (the syv-ai#86
pattern in a second kernel; the 0.28 fix is syv-ai#109). triton-prefill-attn-int8.patch is prefill-attn-int8.patch
here as on main. Three files (dflash2-prewarm, dflash2-z-adaptive-emitted, prefill-attn-int8) carried the
whole diff a second time as stray lines above the first file header, a leftover of the import from the old
patch files; the fork commit bodies are prose only now and the preambles follow.
cpuchip added a commit to cpuchip/qwen38-27b-rtx3090 that referenced this pull request Sep 14, 2026
… the rewritten qwen38/0.29 history: no syv-ai#91 cast, no prefill cast, no VLLM_PREFILL_ATTN registration; a fork-image build from it is the pre-fix tree)
cpuchip added a commit to cpuchip/qwen38-27b-rtx3090 that referenced this pull request Sep 14, 2026
…he rewritten qwen38/0.29 history, six files behind: the syv-ai#91 and prefill casts, the VLLM_PREFILL_ATTN registration, the two backport files; threadchip, 2026-09-14)
mhenrichsen pushed a commit that referenced this pull request Sep 15, 2026
…ment beside item 44

Two follow-ups asked for in review. #91's merge comment: item 12 covered spec-decode-attn.patch and its
query-cap trap and said nothing about the large-pool limit; it now records the int32 cast, that a pool over
about 2,383 blocks was silently reading the wrong memory before it, the synthetic test, and that the int8
prefill kernel had the same pattern. #101's merge comment: item 44 gains the null-gap retirement, the 16 to
5 retained-block bound measured on the reference 3090, that the win is mtp/long shaped and inert on the
dflash2 fast profile, and the single-space blank context lines in that patch file that a whitespace trim
destroys.
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.

spec-decode-attn: illegal memory access (Xid 31) on long cached-prefix requests — int32 overflow of the block index in _spec_attn_partial

3 participants