fix: LFM2.5 paged turbo4 on gfx803 (warp-width, sub-128 head_dim, parallel partials) - #13
Merged
Merged
Conversation
…03 corruption)
The mt_paged_attention turbo4 scatter/decode/tile kernels and the set-rows
turbo4 quant kernel are written for 32-lane warps (WARP_SIZE=32,
DECODE_NUM_WARPS=NUM_THREADS/32, lane=tid%32). They issued bare 3-arg
__shfl_*_sync, which the HIP shim maps to width=warpSize. On gfx803 (GCN)
warpSize=64, so two logical 32-lane warps share one 64-wide hardware wave:
* scatter/set-rows nibble pack: __shfl_sync(.., my_nibble, lane^1) read the
partner from the wrong half of the wave for threads 32-63/96-127,
corrupting ~25% of packed K/V nibbles.
* decode/tile dequant: __shfl_sync(.., norm_f, 0) broadcast lane-0s norm
…ead_dim 64) The paged turbo kernels quantize 128-element blocks (QK_TURBO=128), so a head_dim < 128 (LFM2.5 hybrid attention: head_dim 64) is smaller than one block and the dispatch rejects it. Zero-pad each head to 128 in build_attn for the paged turbo path (q/k/v padded before the F16 cast since ggml_pad needs F32; output sliced back to head_dim), and size the paged cache to the padded head_dim to match. Mirrors the non-paged turbo path. Correctness: padded Q dims are zero so they contribute nothing to QK (K-pad quant is irrelevant); padded V output dims are sliced off. Validated end-to-end on gfx803 once the warp-width corruption was fixed -- LFM2.5 paged turbo4 now generates coherent, correct text on the full tiered config. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
launch_paged_attn_decode sizes the partials scratch and strides every
head/seq/chunk partial offset by max_q_len, which the dispatch set to
avg_q_len = total_q_tokens / num_seqs. With --parallel N and one active
request (1 active seq + N-1 idle slots) this floors to 0 (1/4 = 0), so:
* partials_n = n_heads * num_seqs * num_chunks * 0 * (HS+2) = 0 (empty buf)
* partial_chunk_base = (...) * max_q_len(0) * (HS+2) = 0 for ALL
heads/seqs/chunks -> every partial write collapses onto offset 0 of a
zero-sized buffer -> OOB + total collision -> garbage tokens.
Only surfaced under --parallel > 1 (decode path); --parallel 1 has
avg_q_len >= 1. Manifested as token-salad on LFM2.5 paged turbo4 at the
service's --parallel 4 (clean at --parallel 1); q8_0 only escaped by luck
of allocation layout (the OOB is UB).
Fix: max_q_len = total_q_tokens. The partials inner dim must be >= the
largest per-seq q_len; total_q_tokens is a safe upper bound (no seq exceeds
the batch total) and is already gate-capped to <= 8. The decode gate was
previously fixed to key off total_q_tokens for the same flooring reason;
this carries that correction through to the buffer sizing it forgot.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kmbandy
added a commit
that referenced
this pull request
Jul 31, 2026
…fx1030 crash) ggml_cuda_op_solve_tri fell back to cuBLAS/hipBLAS Strsm whenever k > 32. On gfx1030 (RX 6900 XT) that path SEGFAULTS inside rocBLAS itself: #7 rocblas_trsm_small<float,...,64,32> librocblas.so.5 #10 rocblas_strsm_batched librocblas.so.5 #13 hipblasStrsmBatched libhipblas.so.3 no llama.cpp frames -- a vendor bug. Confirmed with ROCBLAS_LAYER=1: the five smaller SOLVE_TRI test shapes make NO rocBLAS call (fast kernel) and pass; only n=k=64 reaches rocblas_batched_strsm(R,U,N,N,64,64,batch=4) and dies. WHY THIS IS NOT NICHE: Gated DeltaNet uses CS = 64 (delta-net-base.cpp:61, `const int CS = kda ? 16 : 64`) and solves [CS,CS] against [CS,CS], i.e. exactly n=k=64, once per delta-net layer. Every Qwen3-Next-class GDN model would crash on the first such layer on that card. It went unnoticed because prior HIP regressions were run filtered (-o MUL_MAT_ID) and no GDN model had been scheduled onto the 6900 XT. FIX (not a workaround, and not arch-gated): the k right-hand sides are independent -- x_i = b_i * A^-1 carries no cross-row dependency -- so k was only capped at MAX_K_FAST because blockDim.x*blockDim.y (WARP_SIZE * k) must stay within the 1024-thread block limit. Tile k across blockIdx.y in MAX_K_FAST-sized chunks: * col_idx = blockIdx.y*blockDim.y + threadIdx.y (the col_idx >= k guard already present retires the ragged tail) * the cooperative sA load strides by the BLOCK's column count (blockDim.y), not the full k; templated instantiations keep a compile-time stride so the unroll is preserved * dispatch now gates on n only Net effect: for every shape delta-net produces (n <= 64 always, CS is 64 or 16) the BLAS path is no longer reachable, so the rocBLAS bug cannot be hit. CUDA benefits too -- k > 32 now uses the fast kernel instead of cuBLAS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018kbRS3KuJquSpjpZXdtSNp
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.
Summary
Makes LFM2.5-8B-A1B paged turbo4 work correctly on gfx803 (RX 480), and fixes two latent bugs in the
mt_paged_attentionturbo path that affected it. Validated end-to-end on the real service config (512K ctx,--parallel 4, turbo4 paged KV +--kv-tiered 25,25,50+ semantic index) — coherent, correct generation at ~65 tok/s. Resolves MAD-298.Three independent root causes (one commit each):
1.
62f4f99ae— pin turbo4 cooperative warp-collectives toWARP_SIZE(gfx803 corruption)The turbo4 cooperative kernels (
mt_scatter_kv_turbo4_0_kernel,decode_coop_stage_turbo4, the tile decode, and theset-rowsturbo4 quant) are written for 32-lane warps (WARP_SIZE=32,DECODE_NUM_WARPS=NUM_THREADS/32,lane=tid%32) but issued bare 3-arg__shfl_*_sync, which the HIP shim maps towidth=warpSize. On gfx803 (GCN)warpSize=64, so two logical 32-lane warps share one 64-wide hardware wave:__shfl_sync(.., my_nibble, lane^1)read the partner from the wrong half of the wave for threads 32–63 / 96–127 → ~25% of packed K/V nibbles corrupted.__shfl_sync(.., norm_f, 0)pulled lane-0's norm across the whole 64-wide wave → logical warp 1 used logical warp 0's per-block norm.Fix: pass
WARP_SIZEexplicitly (4-arg form) on every warp-collective in these kernels. No-op on CUDA (warpSize already 32); restores correctness on gfx803.2.
fb556b194— support sub-128 head_dim for the paged turbo cache (LFM2.5 head_dim 64)Paged turbo quantizes 128-element blocks (
QK_TURBO=128); LFM2.5's head_dim 64 is smaller than one block and the dispatch rejected it. Zero-pad q/k/v to 128 inbuild_attnfor the paged turbo path (pad before the F16 cast sinceggml_padneeds F32; slice the output back to head_dim) and size the paged cache to the padded head_dim. Mirrors the non-paged turbo path. Padded Q dims are zero (contribute nothing to QK), padded V output dims are sliced off.3.
93a1e6a11— size paged-decode partials bytotal_q_tokens, notavg_q_len(arch-independent)launch_paged_attn_decodesizes the partials scratch and strides every head/seq/chunk partial offset bymax_q_len, which the dispatch set toavg_q_len = total_q_tokens / num_seqs. With--parallel Nand one active request (1 active seq + N−1 idle slots) this floors to 0 (1/4 = 0):partials_n = n_heads * num_seqs * num_chunks * 0 * (HS+2) = 0→ empty bufferpartial_chunk_base = (...) * max_q_len(0) * (HS+2) = 0for all heads/seqs/chunks → every partial write collapses onto offset 0 of a zero-sized buffer → OOB + total collision → token salad.Only surfaces at
--parallel > 1(the decode path). Manifested as token-salad on LFM2.5 paged turbo4 at the service's--parallel 4(clean at--parallel 1); q8_0 only escaped by luck of allocation layout (the OOB is UB). Fix:max_q_len = total_q_tokens(a safe upper bound on any single seq's q_len; gate-capped ≤ 8). The decode gate was already fixed to key offtotal_q_tokensfor the same flooring reason — this carries that correction through to the buffer sizing it forgot.How it was found
The isolated decode harness (
llama-gpu/tests/test_turbo4_decode_full.cu,-DHS_TEST=128) passed at HS=128 while production was broken — its test norms varied only slowly across tokens (0.4 + 0.0007*tok), masking bug #1's norm contamination. Reproduced only with strongly-varying norms (rel-L2 0.72 → 0.0003 with the fix). Bisection: q8_0/f16 paged coherent but turbo4 garbage (→ turbo-specific);--parallel 1coherent but--parallel 4garbage (→ bug #3);MAD_PAGEDATTN_PROBE=verboseshowedavg_q_len=0at parallel 4. The harness norm fill is now a regression guard.Testing
llama-server-lfm25-8b(RX 480, full tiered config): coherent + correct, e.g. "List three primary colors:" → "red, yellow, blue…", ~65 tok/s.build-army(sm_61) and ROCmbuild-rocm-gfx803.qwen3-06b) restarted on the new binary — valid 2048-dim embeddings.Notes for review / merge
--parallel>1) — see that ticket's comment.master(not upstream).