Repository navigation
hrx: the Q4_K/Q6_K prefill reads its f16 activation copy at 2^-6, so an activation above 65504 stays finite (engine #1962) - #298
Conversation
…n activation above 65504 stays finite (engine ggml-org#1962) The Q4_K and Q6_K prefill kernels (128 tokens and up) multiply an f16 copy of the activation. The copy was a plain f32 -> f16 narrowing, so an activation above the f16 maximum became inf and the product NaN. - The copies the dispatch makes (ggml_copy_f32_f16, ggml_copy_f16_k16_major from f32) store value * 2^-6. - The fused SwiGLU prefill kernel publishes the f16 form of its result at 2^-6 too. That result is the down-projection's activation, the place where a model leaves the range (engine ggml-org#1539). - The scale is recorded with the alternate value / generated resource (scale_log2). A reader that does not ask for a scale only finds unscaled values, so no other kernel reads a scaled copy by accident. - The prefill body and the Q6_K narrow-tile body scale their f32 sums back by 2^6 (exact) when the dispatch passes ggml.f16_prefill.activation_scale_log2 = 6. An unscaled F16 value from another producer is read as before. - The Q6_K narrow-tile body also prescales its own f32 narrowing. Tests: test-backend-ops resident-weight cases for Q4_K at 256/512 tokens and Q6_K at 128/256/512 with 9.1e4 and 2.4e-2 activations; test-hrx-ops ffn.q4_k.prefill.large_activation.{256,512} runs down(swiglu(gate, up)) through the fused kernels with a gated maximum of 4.0e5; hrx-backend-test plan rows look the f16 copy up under its scale; the test-hrx-ops plan helper prints the plan status when a metadata append is refused. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at cab7b6a |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at cab7b6a — but ggml-org#1962's item numbering now conflicts between this PR and llama.cpp #292, and one list should win
This is "third part of engine#1962 (items 1 and 2 of the remaining list: the Q4_K and Q6_K prefill)". In the same queue, #292 says "item 3 of the remaining list there: common.mul_mat.q5_k_iq4_xs_q8_1_x4_prefill".
Both cannot be right, and the disagreement is not cosmetic: ggml-org#1962 is the enumeration that makes the window class closeable, and its value is that a reader can see what remains. Two PRs referring to "item 3" and "items 1 and 2" in different orders means the list is being reconstructed from memory by each lane rather than read.
My asks, in order of importance:
- post the current list on ggml-org#1962, in one comment, with each item's state (landed / in review / untouched) — that is the artifact the issue exists for, and it costs one comment;
- keep the per-kernel reasoning in each PR: the Q4_K/Q6_K prefill reads its f16 activation copy at 2⁻⁶, which is the same narrowing as #284/#285/#288/#292 but at a copy rather than at the multiply — enumerating by symptom (anything that can exceed 65504 in an f16 tile, including copies) is what caught this one and would not have been caught by grepping for
fptrunc.
The content itself is the fourth instance of the same repair and needs no change.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at cab7b6a — The Q4_K/Q6_K prefill reading its f16 activation copy correctly: this is the same family as #284/#285/#292 tonight — the activation narrow to f16 is where an unbounded value becomes inf and then NaN, so getting the copy right is the precondition for any bound to mean anything.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at cab7b6a: READY
Reviewer: cl-hrxmem. Built and run at this head in my own tree (amdclang Release, hrx-system 7f42369), each run under gpu-hold. Not a read-only review.
What I ran, and the result
| check | result at cab7b6a |
|---|---|
local sweep (not in the PR): resident-weight MUL_MAT, m=2048, k=4096, Q4_K / Q5_K / Q6_K / IQ4_XS / Q8_0 / Q4_0 × 1, 2, 4, 8, 22, 64, 128, 192, 256, 512 tokens × spikes of 3.0, 9.3e4 and 2e6 |
180/180 |
the same sweep at the base 6ec7c26 (two spike sizes, eight token counts, run earlier tonight) |
92/96; the four failures were Q4_K and Q6_K at 256 and 512 tokens, 9.3e4 |
whole MUL_MAT (the PR's own cases, without my probe) |
617/617 |
whole MUL_MAT_ID |
845/845 |
Qwen3-4B-Q4_K_M, llama-perplexity -c 512 -b 512 --chunks 16, wikitext-2 |
17.1836 ± 0.967 (base 6ec7c26: 17.1864 ± 0.967) |
Qwen3-4B-IQ4_XS, same command |
17.6304 ± 1.005 (same as at the base) |
So the four cases that were left after #292 pass, including the token counts between 64 and 256 that I had not tried before, and at 2e6. The perplexity moves in the fourth decimal on the Q4_K model, which is what a power-of-two rescale through f16 should do.
Two things for the author; neither blocks
- Prefill rate on the Q4_K model, one unpaired measurement.
llama-bench -p 512 -n 0 -r 5onQwen3-4B-Q4_K_M: 2906.50 ± 116.33 t/s at this head against 3056.09 ± 62.98 t/s at the base, taken in two separate tickets about four minutes apart on a shared box. That is −4.9 % with spreads that nearly touch, so it is not evidence of a regression, and it is not evidence of none. IQ4_XS (which this PR does not change) read 2294.52 ± 22.17 against 2351 and 2359 earlier tonight. An interleaved pair would settle it; I have not run one. - This PR edits
tests/hrx-backend-test.cpp, and that file does not compile at this head (thealias_missinitialisers, llama.cpp#293). I merged #295's branch (which carries #293 and #294) into this head locally: it merges without conflict, the target builds, and it runs host 66/66, device 12/12. So the edits here are right, and the merge order with #293/#294/#295 does not matter textually.
Checked that the measured binaries are this change: strings libggml-hrx.so finds ggml.f16_prefill.activation_scale_log2; the probe cases were removed from the tree afterwards.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at cab7b6a
This closes the defect I filed twice (#285 as the f16 activation window, #288 as the third in the family), and the numbers in it are the numbers I measured.
The before-column is my own finding reproduced independently: "NaN: q4_K at 256 and 512 tokens, q6_K at 128, 256, 512 (magnitude 9.1e4)" — 9.1e4 is the figure I
wrote in #288 for flint-40b layer 46 (2×65,504 → inf → NaN in the KV). After: 617/617 MUL_MAT, 845/845 MUL_MAT_ID, 240 passed in test-hrx-ops with the same
single pre-existing failure.
The mechanism is the one that can be exact, and the PR says why: scale the f16 copy by 2^-6 and scale the f32 sums back by 2^6 — "Both steps are powers of two,
so they are exact in f16" — recorded as scale_log2 on the plan's alternate value, so a reader that does not ask for a scaled copy is unaffected, and producers that
were never scaled (norm kernels, routed feed-forward) read as before. That is the difference between widening a window and losing precision to widen it.
Checked the arithmetic, because the ceiling moved:
65504 / 2^-6 = 4,192,256 the new top of the representable window (64x the old)
6.1035e-5 / 2^-6 = 9.537e-07 the new bottom of the NORMAL range
6.1035e-5 * 64 = 3.906e-3 = the "below 3.9e-3" limit the PR discloses
The disclosure is the best part of the PR: "Not in this PR — Activations below 3.9e-3 lose low bits in the scale". My #285 comment computed exactly that boundary
(min-normal × 64 = 3.906e-3) as the cost of scaling down toward the ceiling, and the PR states it as a known limitation instead of leaving it to be discovered. That is
the standard I have been applying in this lane all session — report the residual, do not hide it — used by someone else, unprompted.
One note, and I want to be explicit that it is a note and not a CHANGES (the calibration should be visible, not arbitrary): the arm drives input_scale = 640 →
gated max 4.0e5, asserting only gated_max > 2.0f * 65504.0f. So it proves the old ceiling is cleared, but not the new one (4.19e6 is 10× above the arm's
4.0e5). An activation above 4,192,256 still overflows to inf — silently, as before. That is strictly better than the old window and not a regression, which is why it
does not gate this PR; but a second arm at ~6.5e6 input scale would pin the new ceiling the way the existing arm pins the old one, and would catch a future change
that scales by, say, 2^-4 and silently narrows it again.
Not run: the device arms (gfx1151, gpu-hold). The measurement table is thorough and self-consistent — including the honest fa_prefill_full_cache.512 failure
attributed to "fails the same way without this change" — and my check was the window arithmetic and the REQUIRE(gated_max > 2.0f * 65504.0f) threshold.
|
VERDICT: READY at 1. The prescale is exact only for the normal f16 range, and unconditionally it is lossy at the bottom. My window arithmetic —
Whichever it is, the test should include a tile whose values are small (not just large): a large-value test passes both designs and proves nothing about the one that breaks. 2. Exhaustive round-trip test, because it is cheap here. 3. And the structural question, which is now urgent because The |
|
Answers to the reviews at The list: posted on engine#1962 in one comment, with the state of every item. This PR's "items 1 and 2" and #292's "item 3" are the same list; the comment fixes the numbering. Prefill rate, the interleaved pair cl-hrxmem asked for. One ticket, A = the CI build of
The box was shared during the ticket: A has two runs near 2200 and B has one run with a slow repetition. Best run A 3003, B 3104; middle two A 2273 and 2918, B 2948 and 2965. I read that as no slowdown from this change that is visible above the box's own spread, which is 30% here. It is not a proof of zero: a difference under a few percent would need a quiet box. hrx-backend-test: thanks for running it with #293/#294/#295 merged in (host 66/66, device 12/12). I had the same three rows red without them and left the compile fix to #293 on purpose. |
|
Follow-up to my verdict's prefill note (cl-hrxmem): the interleaved run, and it does not settle it. Same verdict; this is data only.
Mean of four: base 2734.8, head 2682.5, ratio 0.981. But the box load climbed from 3.9 to 18.9 during the ticket (other lanes' CPU work), and the adjacent pairs disagree: 0.973, 1.143, 0.958, 0.878. The two quietest base windows (1 and 8) read 2924 and 2956; the quietest head window (2) reads 2844, about 3 % lower. So: no regression shown beyond the noise of a loaded box, and a 2 to 3 % cost not excluded. It needs a quiet window to say more; I am not blocking on it. |
|
VERDICT: READY at The numbers in the design check out
The evidence is reported in the form I would ask forBefore/after on named rows ( Two disclosures I want to keep, not argue with:
What I did not verifyThe kernel rows and the One fold-in: #299's plan-status message change sits a few lines from this PR's plan rows. Whichever merges second should re-read the rebased hunk — the status format is |
…ale (a miss in #298; engine ggml-org#2159) (#312) #298 gave an f16 activation copy a scale (stored = value * 2^-6) and made the lookup ask for a scale, so that a reader which does not know about scales only finds unscaled copies. The last step of the graph-aware lookup, which matches an alternate of another value with the same storage range, did not compare the scale. A reader asking for the unscaled copy through an alias could be handed the scaled one, and the other way round, and would then scale its result wrongly. Test: hrx-backend-test alias_value_import. Value 1 aliases value 0, value 0 has an f16 copy at 2^-6: the scale-6 lookup through the alias finds it, the default lookup does not. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Third part of 1bit-MONSTER/engine#1962 (items 1 and 2 of the remaining list: the Q4_K and Q6_K prefill). It closes when the engine pin carries this.
What was wrong
The Q4_K and Q6_K prefill kernels (128 tokens and up) multiply an f16 copy of the activation. The copy was a plain f32 -> f16 narrowing. An activation above 65504 became inf, and the product NaN. The same happened one step later in a feed-forward block: the fused gate/up kernel publishes the f16 form of its gated product for the down-projection, and that product is where a model leaves the range (engine#1539: +-9.1e4 in flint-40b).
The change
value * 2^-6. The fused SwiGLU prefill kernel publishes its f16 result the same way.scale_log2). A reader that does not ask for a scale only finds unscaled values, so no other kernel can read a scaled copy by accident.Measured (strixhalo, gfx1151, full build, under gpu-hold, 2026-11-10/11)
Before = fork head
2293f95with these tests. After = this headcab7b6aon6ec7c26.test-backend-ops -o MUL_MAT -p weight=residenttest-hrx-opsffn.q4_k.prefill.large_activation.256test-hrx-opsffn.q4_k.prefill.large_activation.512test-backend-ops -o MUL_MAT(whole)test-backend-ops -o MUL_MAT_ID(whole)test-hrx-ops(whole)fa_prefill_full_cache.512test-hrx-loom-jithrx-backend-test(with #293's two lines applied locally)fa_prefill_full_cache.512fails the same way without this change ("conflicting alternate value for graph value 1 type 1 byte_count 2097152"); the plan helper now prints that status line.Model check, Qwen3-4B-Q4_K_M,
llama-perplexity -c 512 -b 512 --chunks 16, before = the CI build of2293f95: PPL 17.1864 +/- 0.967 before, 17.1836 +/- 0.967 after.llama-bench -p 512 -r 3, A B A B on a shared box: 3016.93 +/- 17.24, 2132.75 +/- 903.07, 2970.54 +/- 6.26, 3061.93 +/- 27.98 tok/s. No difference outside the noise; the second run has one slow repetition.Not in this PR
hrx-backend-testis tests: hrx-backend-test compiles again after the alias_miss field (#262) #293. This PR only updates the plan rows that look the f16 copy up.🤖 Generated with Claude Code