Skip to content

fix: LFM2.5 paged turbo4 on gfx803 (warp-width, sub-128 head_dim, parallel partials) - #13

Merged
kmbandy merged 3 commits into
masterfrom
fix/lfm25-paged-turbo4-gfx803
Jun 25, 2026
Merged

kmbandy merged 3 commits into
masterfrom
fix/lfm25-paged-turbo4-gfx803

Conversation

@kmbandy

@kmbandy kmbandy commented Jun 15, 2026

Copy link
Copy Markdown
Owner

Summary

Makes LFM2.5-8B-A1B paged turbo4 work correctly on gfx803 (RX 480), and fixes two latent bugs in the mt_paged_attention turbo 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 to WARP_SIZE (gfx803 corruption)

The turbo4 cooperative kernels (mt_scatter_kv_turbo4_0_kernel, decode_coop_stage_turbo4, the tile decode, and the set-rows turbo4 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 to width=warpSize. On gfx803 (GCN) warpSize=64, so two logical 32-lane warps share one 64-wide hardware wave:

  • nibble-pack __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.
  • dequant norm broadcast __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_SIZE explicitly (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 in build_attn for the paged turbo path (pad before the F16 cast since ggml_pad needs 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 by total_q_tokens, not avg_q_len (arch-independent)

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):

  • partials_n = n_heads * num_seqs * num_chunks * 0 * (HS+2) = 0 → empty buffer
  • 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 → 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 off total_q_tokens for 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 1 coherent but --parallel 4 garbage (→ bug #3); MAD_PAGEDATTN_PROBE=verbose showed avg_q_len=0 at parallel 4. The harness norm fill is now a regression guard.

Testing

  • gfx803 isolated harness: scatter + decode (q_len 1/2/4, GQA dims, multi-chunk) all PASS at HS=128/256 after fixes.
  • Live service llama-server-lfm25-8b (RX 480, full tiered config): coherent + correct, e.g. "List three primary colors:" → "red, yellow, blue…", ~65 tok/s.
  • Both builds compile: CUDA build-army (sm_61) and ROCm build-rocm-gfx803.
  • Mneme embedder (qwen3-06b) restarted on the new binary — valid 2048-dim embeddings.

Notes for review / merge

  • Commits 1 and 3 are no-ops on CUDA; commit 3 is arch-independent and likely also bears on MAD-288 (omnicoder paged-turbo4 CUDA corruption under --parallel>1) — see that ticket's comment.
  • Targets fork master (not upstream).

kmbandy and others added 3 commits June 14, 2026 20:25
…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
kmbandy merged commit 889c02f into master Jun 25, 2026
11 of 75 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant