Repository navigation
spec-decode-attn: cast the block id to int64 before the stride multiply (fixes #86) - #91
Conversation
…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.
|
Tested on WSL2 / RTX 3090 (sm86), local image built from the repo Dockerfile with this PR cherry-picked onto |
|
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 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 Two notes while you are in there:
The rest of the reasoning is right and I checked the arithmetic: bf16 |
…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.
|
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 Falsified on a 4090 before pushing: without the cast The cast covers the int8 per-token-head scale pointers too, since they index with the same #90 is rebased on the corrected commit. |
|
Verified on WSL2 / RTX 3090 (sm86). Local image
bf16: max|ours-ref|=0.0001 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 ( |
|
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 ( With your cast inserted into that same file (
Series integrity I ran locally against a pristine v0.28.0 checkout rather than trusting CI: What is still unproven, for the record:
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:
|
|
On the two paths you list as untested: the 80K probe I ran was on the live server with the int8 KV route ( 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. |
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).
…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.
…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.
… 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.
… 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)
…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)
…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.
Fixes #86. The block id loaded from the int32 block table in
_spec_attn_partialis 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 inspec-decode-attn.patchwhere 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.pyreproduces 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 existingbench/test_spec_decode_attn.pybyte-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 printsSKIP(by design): this kernel has no fp8 path (no k_descale argument)(both cards), and with the fp8 path present a 3090 printsSKIP(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.