Repository navigation
fix(paged-attention): use 64-bit pool offsets in the fused kernels - #2186
Conversation
The fused paged-attention bodies computed the per-token pool base, (row * page + slot) * Hkv * D + kv_head * D, in 32-bit unsigned arithmetic. A layer slab past 2^32 elements per side (131,072 blocks of [32, 8, 128] for Llama-3.1-8B, which the server's ceil(ctx/32) * batch sizing reaches at long contexts and high --parallel) wrapped the address and silently read another row of the same buffer. The sparse decode wraps on the same v2 arithmetic. The pool base is now 64-bit in all six bodies (v1 decode and v2 partial for Metal, CUDA and HIP), with row widened before the multiply; loop counters, template args, grids and dtype keys are unchanged. The merge kernel keeps 32-bit indices: its launcher refuses v_in or output sizes above UINT32_MAX with std::invalid_argument, and PagedDecodePlan::validate rejects such a plan first so the batched decode returns to gather instead of failing the launch. Tests: a source pin of all six declarations, a merge refusal test on lazy broadcasts, a plan bound test, and an ignored GPU test that builds a 2^32 + 32,768 element f16 pool and checks v1 and v2 over its last block against a host reference. On gfx1151 the GPU test fails on the 32-bit bodies (max error 0.456 for both) and passes after. Closes #2153
Implementation Review SummaryIntent
Findings AddressedNone needed. No CRITICAL or HIGH findings. Remaining Items
Verification
Notes: all six base lines widen |
The doc example now says more than 131,072 blocks (exactly 2^32 elements still addressed correctly before the fix), the plan check comment names the partial kernel output it also protects, and a stray blank line in the merge launcher is removed. No behavior change. Refs #2153
Security and performance reviewNo CRITICAL or HIGH findings; no fix commits. Checked:
LOW / informational (no action requested):
|
Summary
The fused paged-attention kernels computed the per-token pool base in 32 bits, so a layer slab past 2^32 elements per side (131,072 blocks of
[32, 8, 128]for Llama-3.1-8B) silently read another row of the same buffer. The base is now 64-bit in all six bodies, and the 32-bit merge kernel is guarded by size checks instead.What changed
Kernel bodies: only the pool-base declaration changes, plus a one-line comment above it. The variable type and the casts change; template args, grid, JIT names and dtype keys are unchanged (
make verify-kernel-dtype-keys: 9 in scope).ulong)paged_attention.cpp:149paged_attention_v2.cpp:176uint64_t)paged_attention.cpp:303paged_attention_v2.cpp:372uint64_t)paged_attention_hip.h:140paged_attention_hip.h:302Each line becomes
T base = ((T)row * page + slot) * (T)stride_kv + kv_head * dim;, withrowwidened before the multiply. Thek_pool[base + d]/v_pool[base + d]reads use the 64-bit value unchanged. The same arithmetic infused_norm.cppandfused_rope_append.cppalready usesulong(Metal) anduint64_t(CUDA). Sparse decode runs on v2 partial, so it gets the same fix.paged_attention_merge_statesthrowsstd::invalid_argument(anErrin Rust) whenv_inor the merged output exceedsUINT32_MAXelements.PagedDecodePlan::validaterejects such plans before launch. The batched decode panics on a launchErr, so with this check those batches go to gather instead.MLXCEL_PAGED_SLAB_BLOCKSrow indocs/environment-variables.md.cache/paged_pool_offset_tests.rs, notpaged_batch_decode_tests.rs, which is already 599 lines.Verification
On gfx1151, rebased on
9c0ae2e9(after #2181). Review follow-upc3fef346changes comments and docs only:paged_pool_past_u32_elements_matches_gather(ignored, about 17 GiB): f16 pool of 131,073 blocks, K/V only in the last block, v1 and v2 checked against a host reference. With main's bodies it fails with max error 0.456 for both kernels. With this PR it passes (guarded runs). It also confirms the last block reads back and row 0 (where a 32-bit base lands) is zero.cargo test -p mlxcel-core --features rocm --lib -- paged: 264 passed. Clippy on lib and tests is clean, and the script gates plusverify-rocm-overlaypass.--parallel 4 --ctx-size 131072, 4 clients at ~16K prompt, 128 tokens, fused v2 on both arms (the log showsfused v2 launch). A build with main's kernel bodies (this branch with the three kernel files reverted) and this PR were run as a pair inside onerocm_gpu_guard.shwindow, three times, and all attempts were clean. Per-request decode tok/s was 14.0/13.9/14.0 before and 14.1/14.0/14.0 after. Both medians are 14.0, so there is no regression.examples/paged_attention_kernel_benchwas not used: it keeps the 32-block default slab, so at batch 4 / 16K it measures gather.make verify-rocmonc3fef346(withMLXCEL_ROCM_SMOKE_MODEL=models/mlx/Qwen3-0.6B-4bit): script gates, workspace clippy and smoke pass. Tests: 152 suites, 11984 passed, 5 failed, 383 ignored. The 5 failures are the Gemma 3 lookahead tests inserver::batch::scheduler(from perf(scheduler): restore Gemma 3 decode lookahead via ring rewind #2182). They fail the same way with every file of this PR reverted toorigin/main9c0ae2e9, pass when run alone, and are filed as test(scheduler): Gemma 3 lookahead tests fail in the full mlxcel lib run on ROCm #2187.Found while writing the test: on ROCm, strided MLX copies of 2^32 or more elements fail to launch. That is filed as #2184, and the test avoids it.
Not verified: Metal and CUDA are not available on this host. Their v1 and v2 partial pool-base lines in the table above changed and were only reviewed, not compiled or run.
Closes #2153