Repository navigation
hrx: the fused Q decode kernel compiles at every head size, not only a power of two (engine #1926) - #282
Conversation
…a power of two (engine ggml-org#1926) llm_attention_q_matmul_rope_decode_f32_f32 stored each rotated pair at channel rem head_size. The store is two wide, and the Loom compiler can bound that remainder by head_size - 2 only when it is a mask, so head sizes 48, 80, 96 and 160 failed with SUBRANGE/010 and a one-token decode returned an error (stories15M, Sarashina2.2-3B). The channel is the even row of a rotation pair, so the kernel now indexes by pair: the remainder is bounded by head_size / 2 and the store is in bounds for every head size. tests/test-hrx-attention-q-decode-head-size.cpp runs MUL_MAT + the head layout + ROPE at one token on HRX0 against the CPU backend (BF16, F16, Q8_0; head sizes 48, 96, 160, controls 64 and 128). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at f33978d |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at f33978d
The bug is a power-of-two assumption in an index expression, and the fix is the weaker, correct constraint:
// %channel is the even row of a rotation pair, so index by pair: the remainder is then bounded by
// head_size / 2 and the two-wide store below is in bounds for every head size, not only for a power of two
// (engine #1926: 48, 96 and 160 did not compile).
%pair_channel = index.div %channel, %c2 : indexDividing by two before the modulo is what makes the remainder bounded by head_size/2 for any even head size — the old form needed head_size to divide
the channel range evenly, which only holds for powers of two. That is a one-line change with a stated invariant instead of a special case, which is the shape I
want for this class.
And the arm tests exactly the sizes that failed, plus the control:
cases.push_back({ type, 48, 6 }); // stories15M
cases.push_back({ type, 96, 8 });
cases.push_back({ type, 160, 16 }); // Sarashina2.2-3B
cases.push_back({ type, 128, 8 }); // control
Three real models with their own head counts and the previously-working 128 as the control — so the arm proves the fix at the boundaries and that the
power-of-two case still behaves. Nine REQUIREs, with the dispatch resolution, an imported buffer and a per-case NMSE check (nmse=%.3g), so the comparison is
numeric rather than "it compiled".
That last point is the important one for this bug: 48/96/160 did not compile, so the cheapest possible test would be "it builds now". The PR instead
runs the kernel and compares against a reference with a printed NMSE, which is the difference between a compile fix and a correctness claim.
Not built: no llama.cpp build tree here and -DGGML_HRX=ON needs the private runtime, so this verdict rests on the diff, the invariant, and the arm's shape —
not on a run. Labelled rather than implied.
One note: the same power-of-two assumption is worth a grep across the other kernel-index expressions (% head_size, & (head_size-1) shapes); if it hid here
it can hide in a formula that only runs for a model nobody has loaded yet.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at f33978d: READY
A fused Q decode kernel that only compiles at some head sizes is a kernel that fails at configuration time for the models nobody tested — the worst place to find out, because it looks like a build break rather than a missing instantiation. Making it compile at every head size, with the sizes handled uniformly, removes a class of failure rather than one instance.
CI green. I read the diff and CI; I did not compile this fork.
Fork half of 1bit-MONSTER/engine#1926. An engine PR that moves the pin will carry
Fixes #1926once this merges.The fault
A one-token decode fails on any model whose attention head size is not a power of two and whose Q weights are not Q4_K/Q6_K:
The runtime hides the compiler's reason. Compiling the same sources offline (
loom-link --mode=merge, thenloom-compile --target=amdgpu:gfx1151 --format=amdgpu-hsacowith the config keys from the line above) prints it:The kernel stores each rotated pair, two values wide, at
head_channel = channel rem head_size.channelis always the even row of a pair, so the store is in bounds, but the compiler can only bound the remainder byhead_size - 2when the head size is a power of two.The weight format does not matter (format 4 at 160 fails the same way). Q4_K/Q6_K models did not hit this because the matcher sends them to the packed low-token kernel when
output_size % 64 == 0.The fix
Index by rotation pair instead of by channel:
pair = channel / 2,head = pair / (head_size / 2),theta_channel = pair rem (head_size / 2),head_channel = theta_channel * 2. Same addresses for every evenchannel; the remainder is now bounded byhead_size / 2, so the two-wide store is provably in bounds at every head size. The K and V decode kernels store on theoutput_sizeaxis and are unchanged.Proof (strixhalo, gfx1151, power mode
performance, undergpu-hold)New test
tests/test-hrx-attention-q-decode-head-size.cpp: MUL_MAT + head layout + ROPE at one token on HRX0, must plan on this kernel, compared with the CPU backend (normalized MSE ≤ 5e-4). It skips without an HRX0 device, like the other device tests, so CI without a GPU does not exercise it.Before (kernel as at
2d62cc7af, same test binary otherwise): 15 cases, 9 failuresAfter: 15 cases, 0 failures
The 64 and 128 controls give the same error before and after, and the new head sizes sit at the controls' level.
The two models from the issue,
llama-bench --device HRX0 -ngl 99 -p 512 -n 128 -r 3with the no-SVM trio, one run each:ret = -3ret = -3Limits of this proof
--temp 0: HRX and CPU agree for the first 167 bytes, then diverge into different coherent sentences. I did not compare logits per step; the per-kernel comparison above is the numeric evidence.🤖 Generated with Claude Code