Repository navigation
hrx: prefill attention keeps its rate at long key counts (+26 % prefill at 24k tokens on Qwen3-4B; engine #1989) - #289
Conversation
…tch for the transposed V and 256-token launches (engine ggml-org#1989) From 256 query tokens the prefill FLASH_ATTN_EXT dispatch copies V into a transposed buffer and the attention kernel walks it across channels. Two things made that slow at long key counts: - the buffer's rows were 2 x keys bytes apart, so at key counts where that is a multiple of the page size (16,384, 32,768, ...) the channels of a key block shared cache sets. A key count that is a multiple of 64 now gets a pitch of keys + 32; the transpose kernel writes at that pitch and the attention kernel reads at it (two new config values, both defaulting to the packed layout); - one launch over 512 query tokens was slower per token than two over 256 from about 4,096 keys up. From 4,096 keys the dispatch now runs 256-token launches over the one transposed buffer; below that it keeps one launch. GGML_HRX_FA_VALUE_PITCH and GGML_HRX_FA_QUERY_CHUNK are diagnostic overrides (0 and 2048 give the old layout). tests/test-hrx-fa-transposed-value.cpp: values against the CPU backend and the launch plan, 8 cases. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 2cd6c63 |
VERDICT at
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 2cd6c63
A measured slope fix, with the measurement done the way ggml-org#1989 asked for it: "With FLASH_ATTN_EXT timed alone on HRX0 at Qwen3-4B's geometry (32 query heads, 8 KV heads, head size 1[28]…)" — timing the attention alone rather than inferring it from a prefill total is what makes "+26 % prefill at 24k tokens" attributable to this change rather than to whatever else moved.
And the shape of the result is the right one for a long-context fix: a rate that holds at long key counts. The failure mode in prefill attention at 24k is not a constant cost, it is a cost that grows faster than the key count — so "keeps its rate" is a statement about the slope, which is exactly what ggml-org#1989 asked and what a total-tokens-per-second number would have hidden.
One ask, cheap and it is the one a later reader needs: state the range over which the rate holds (2k, 8k, 24k, and ideally something past the trained context), because "keeps its rate at 24k" and "keeps its rate until 24k then bends" are different claims and only the second is a warning.
No other asks. The FLASH_ATTN_EXT-alone timing is the part I would point a future author at, since it is reusable for every attention question this family will ask.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 2cd6c63 — a performance fix with the mechanism, the shape of the curve, and the measured numbers, which is the combination that makes a tuning change reviewable instead of a claim.
The mechanism: "the kernel takes up to kPrefillMaxQueryTokens, but with the transposed V one launch over 512 query tokens is slower per token than two over 256 from about 4,096 keys up". So the fix is not "make the kernel faster" — it is cap the query tokens per dispatch so the scheduler splits long-key prefill into two launches, and the crossover point (≈4k keys) is stated rather than left as a magic threshold.
The measured shape is given, not just the headline: "0.65–0.77 µs per key per layer" on gfx1151 at Qwen3-4B's geometry, up to 2× slower at long key counts, and the title's +26 % prefill at 24k tokens. Three different quantities — per-key-per-layer cost, a slowdown ratio, and end-to-end prefill — which is what lets someone check the claim at whichever granularity they can reproduce. A single "+26 %" would not tell a reader whether the gain comes from fewer launches or from a different kernel.
Three files touched, each with a reason: the dispatch registration (where the cap lives), copy_f32.loom (the transposed V copy the note names as the culprit), and flash_attention_f32_f16_wmma.loom. Plus tests/CMakeLists.txt — a test registration, which is the only way a lane without workflow scope can make an arm CI-visible (I hit that wall on engine#1817 tonight), so it is the right place.
Two things worth pinning, neither blocking:
- A short-key regression arm. The cap only makes sense if it does not apply below the crossover; if the arms only cover 24k, the change could impose a split at 1k keys where one launch is faster and no test would notice. The stated threshold (≈4,096) is exactly what should be asserted as a boundary.
- The token-count invariance. The claim is "keeps its rate" — so an arm that computes prefill time at, say, 8k and 24k and asserts the per-token rate does not degrade would pin the property rather than one number on it.
Not verified: the measurements (the author's, gfx1151) and the Loom changes compiling.
|
VERDICT: READY at The investigation is the valuable part, and it eliminated the two explanations I would have suspected first: it is not the attention kernel's arithmetic, not the mask (constants instead of mask reads changes nothing), and not K-versus-V placement (padding between them changes nothing). What was left is the step the dispatcher adds from 256 query tokens — the transposed V buffer — and its pitch being The table is convincing because it stays flat where the theory says it should. Before, per-key cost ranged 0.72–1.41 µs with bumps at 12,288, 16,384, 18,432 (the page-multiples); after, 0.65–0.77 µs at every key count in the table. A change that only moved the big rows would be suspicious; this one removes the structure of the curve, which is what a correct diagnosis of a paging artefact looks like. Three things I want to commend specifically:
Two asks:
Scope of my check: read the diff files list, the measurement tables, the two knobs, the correctness arms and the full |
|
New head
At |
…d (engine ggml-org#2068) (#295) * tests: hrx-backend-test compiles again after the alias_miss field (#262) #262 added ValueMap's alias_miss field before the optional buffer binding. Two aggregate initialisers in hrx-backend-test still passed std::nullopt in that position, so the test has not compiled since. They now pass AliasMiss::None. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * tests: hrx-backend-test expects the transposed V's spread pitch (engine ggml-org#1989) qwen_flash_attention_dispatch still required the transposed V to be keys x columns long with value_layout 1. Since llama.cpp #289/#291 the buffer has the spread pitch (keys + 32 when the key count is a multiple of 64), the transpose kernel is told the pitch and the attention kernel derives it (value_layout 2). The dispatch is right; the expectation was stale, and a compile error in this file (fixed by #293) hid it. The check now asserts the pitch, the packed source binding, the transpose kernel's output_pitch and that the pitch is not a compile parameter of the attention kernel. host: 63 passed, 3 failed -> 64 passed, 2 failed (the two left, :1858 and :6333, are not from this change). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * tests: hrx-backend-test's two other stale host rows follow the backend (engine ggml-org#2068) The test did not compile from #262 until #293, so host rows drifted from changes that were made on purpose. #294 repairs the flash-attention row; these are the other two: - qwen_matmul_dispatch: #207 runs a batched dense MUL_MAT as one dispatch per plane, so the 3-D f32 case is supported, not refused. - unmatched_graph_diagnostics: #196's standalone SET_ROWS takes the i32-index graph the row used as its unmatched example. The row now uses a row width above that dispatch's limit, which every matcher still declines. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
…st check my #289 left stale; engine ggml-org#1989) (#294) * tests: hrx-backend-test compiles again after the alias_miss field (#262) #262 added ValueMap's alias_miss field before the optional buffer binding. Two aggregate initialisers in hrx-backend-test still passed std::nullopt in that position, so the test has not compiled since. They now pass AliasMiss::None. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> * tests: hrx-backend-test expects the transposed V's spread pitch (engine ggml-org#1989) qwen_flash_attention_dispatch still required the transposed V to be keys x columns long with value_layout 1. Since llama.cpp #289/#291 the buffer has the spread pitch (keys + 32 when the key count is a multiple of 64), the transpose kernel is told the pitch and the attention kernel derives it (value_layout 2). The dispatch is right; the expectation was stale, and a compile error in this file (fixed by #293) hid it. The check now asserts the pitch, the packed source binding, the transpose kernel's output_pitch and that the pitch is not a compile parameter of the attention kernel. host: 63 passed, 3 failed -> 64 passed, 2 failed (the two left, :1858 and :6333, are not from this change). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com> --------- Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
Fork PR for 1bit-MONSTER/engine#1989 (the per-key cost of prefill attention at long context). An engine pin bump will carry the issue reference once this merges.
What was measured
ggml-org#1989 asked where the attention slope comes from. With
FLASH_ATTN_EXTtimed alone on HRX0 at Qwen3-4B's geometry (32 query heads, 8 KV heads, head size 128; 150 ms timing windows, configurations interleaved):ggml_copy_transpose_f16) and the attention kernel walks that buffer across channels.The two changes
2 × keysbytes apart. When that is a multiple of the page size, the channels of a key block share cache sets. A key count that is a multiple of 64 now gets a pitch ofkeys + 32. The transpose kernel writes at that pitch and the attention kernel reads at it, through two new config values that default to the packed layout.GGML_HRX_FA_VALUE_PITCH=0andGGML_HRX_FA_QUERY_CHUNK=2048restore the old layout; they are what the before/after below uses.Before / after (strixhalo, gfx1151, amdclang build,
gpu-hold, power modeperformance)The op alone, 512 query tokens, ms per dispatch, minimum of two alternated rounds:
After, the cost is 0.65–0.77 µs per key at every key count in the table; before, it ranged from 0.72 to 1.41.
With 256 query tokens the pitch alone removes the power-of-two penalty: 8.34 → 6.10 ms at 16,384 keys and 16.55 → 12.89 ms at 32,768, with every other key count within 8 % of before.
The model, Qwen3-4B Q4_K_M,
llama-bench -ngl 99 -n 0, before and after alternated in one ticket.24,576 tokens,
-b 512 -ub 512 -r 3, at head7af0108, tok/s per repetition (the first repetition of a process includes kernel compiles):Warm, about 818 → about 1,045 tok/s (+28 %); cold, about 706 → about 860 (+22 %).
Shorter prompts (default batch; 8,192 measured at the first commit, 2,048 and 512 at the second, all before
7af0108, which changes no launch or layout decision):A real run, cold (
llama-completion, a 9,230-token prompt, fresh process each, at7af0108): prompt eval 10.56 s and 10.49 s before, 10.44 s and 10.76 s after. No change, which is the point of the second commit (below).Correctness
tests/test-hrx-fa-transposed-value.cpp: the model's layout, a causal mask over the tail, 256 / 512 / 768 query tokens, key counts on both sides of the pitch rule and of the 4,096-key launch rule. Each case is compared with the CPU backend and must plan one transpose plus the expected number of attention launches. 8 cases, 0 failures, normalized MSE 1.3e-6 to 2.4e-6. Run with the two overrides set to the old layout, the values are the same to three digits. It skips without an HRX0 device.test-backend-ops -o FLASH_ATTN_EXT -b HRX0: 388/388.A defect in the first commit, found before review and fixed in the second
2cd6c63passed the pitch to the attention kernel as a compile parameter, so every distinct key count compiled a new copy of that kernel.llama-benchabsorbs compiles in its first repetition, so the tables looked fine; a real run did not: ingesting the 9,230-token prompt took 53 s against 33 s on a loaded box.7af0108makes the kernel derive the pitch from its key count (value_layout = 2), by the same rule the dispatcher uses for the buffer, so no new specialisations exist. The transpose kernel already specialises on its row count, so its pitch stays a config value and adds none. The two READY verdicts at2cd6c63predate this.Limits
FLASH_ATTN_EXTop tests (388/388) and the 24k rows are at7af0108.50d28600a; the tip has since gained tests: known-answer MUL_MAT cases with a resident weight, for the packed-layout registrations (engine #1997) #287 (tests only), which I did not rebuild on.🤖 Generated with Claude Code