Repository navigation
hrx: the fused conv4 prefill shares the weight copy prefill already reads (engine #2137) - #322
Conversation
…ored kernels (engine ggml-org#2137) The fused projection+conv kernel reads Q4_K only in the packed rows. For a file-mapped Q4_K weight that already has a stored device copy, decline the fused matcher so the projection runs on the stored 8-bit-block prefill kernel and the conv runs separately: one device copy per weight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rced stored (engine ggml-org#2137) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
VERDICT: READY at The test asks a different question from #320#320 covers That is the right second arm: two tests that both said "stored copy" would be one test twice; these constrain the layout and the fusion choice. The structural problem: the two PRs will conflictTwo shared files, no ancestry — whichever merges second resolves a conflict by hand in the dispatch registration, which is the file whose content is the test's subject. Stack one on the other (either order works; #322's The one ask, same as #320Show that each arm fails without the engine change. For a test-only PR that is the whole claim, and here it is sharper than usual: if the unfused path is what |
…ll reads it packed (engine ggml-org#2137) The fused kernel shares the prefill body that already takes Q6_K as format 46 (packed rows). Bind that layout when q6k_packed_rows_enabled() says prefill reads this weight packed and no GET_ROWS reads it stored, so the weight keeps one device copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Answer to the READY at The ask: the arm fails without the change. Same So on the fork head the forced-stored Q4_K rows take the fused kernel, and the arm fails. The conflict: there is none. #320 is merged ( What no suite row pins: the Q6_K commit's layout choice. |
|
VERDICT: READY at The binding is one condition, not a preference// engine #2137: this fused kernel reads Q4_K only in the packed rows. For a Q4_K weight that decode already has
// stored, the plain matchers take the projection instead (the stored 8-bit-block prefill kernel, then the separate
// conv), so the weight keeps one device copy.
if (q4k_aligned_prefill_reads_stored(*mat.weight)) {That is the right shape: the fused kernel's requirement is a property of the fusion (it reads packed rows), so when decode already holds the weight stored, the fusion is not merely suboptimal — it is asking for a second copy. Declining the fusion in that case is what makes "one device copy" true, and it is the same reasoning as the Q6_K branch below it ( The predicate now reads both ways ( And the test moved with itThe arm now takes the environment switch into account rather than asserting a fixed sequence — which is the fix for a test that would otherwise have gone red the moment the layout policy changed. Testing through the same switch the production path reads is the right instrument. Two asks
Note on its earlier lifeThis PR was test-only when I last reviewed it ( |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at a591023 — 15 lines that finish the 27B's double-copy story, with the text identical across all four arms and the rate honestly reported as "not slower; no gain claimed".
The measurement is the right shape for a memory change
Copies, arms base / new / new / base, text md5 91fdacac410d in all four:
base 866 copies, 16,181.4 MiB, 16 weights held twice, 462.9 MiB extra
new 850 copies, 15,718.5 MiB, 0 weights held twice, 0 extra → −462.9 MiB
Rates, six arms (new/base alternating), text identical in all six:
base 320.9 / 308.4 / 306.2 prefill, 10.86 / 10.91 / 10.89 decode
new 328.5 / 323.3 / 306.0 prefill, 11.01 / 10.86 / 10.87 decode
"16 weights held twice → 0" with the text md5 unchanged in every arm, and the rate table interleaved base/new so drift cannot pass for a speed claim. "Not slower; no gain claimed" is the sentence I want on a change whose subject is memory: it would have been easy to point at 328.5 vs 320.9 and call it a win, and it is not claimed.
The guard is necessary, and the reason survives the code
bool q6_packed = type == Q6_K &&
q6k_packed_rows_weight_ok(*w, input_size, output_size) &&
q6k_packed_rows_enabled(*w);
for (consumer : graph.index().consumers(w->id))
if (consumer != nullptr && consumer->op == GGML_OP_GET_ROWS) q6_packed = false;A weight that a GET_ROWS also reads cannot be bound in the packed rows for this kernel, because the other reader wants the stored layout — so without that loop the change would trade 462 MiB for a layout mismatch on one of the two readers. The comment says it in one line ("Not for a weight a GET_ROWS also reads stored"), and the three preconditions reuse q6k_packed_rows_weight_ok / q6k_packed_rows_enabled rather than re-deriving the geometry — the #1830 rule.
My one ask, and it is about reuse rather than correctness
The mature site already has this check as a named predicate:
static bool weight_has_native_layout_reader(const Graph& graph, const Value& value) {
for (consumer : graph.index().consumers(value.id))
if (consumer != nullptr && consumer->op == GGML_OP_GET_ROWS) return true;
return false;
}This PR inlines the same loop (dispatch-mul-mat.cpp:199 calls the predicate; dispatch-ssm-conv.cpp re-implements it). Two copies of one rule is how the third site gets it subtly different — and this one already differs in a way worth noting: the mature predicate returns false when the graph has no index (!graph.has_index()), while this loop would simply find no consumers. Same answer today, different by construction. One line — q6_packed = q6_packed && !weight_has_native_layout_reader(context.graph, *mat.weight); — makes them the same rule.
Verified by the PR itself and not re-run here: test-hrx-ops 267/267 and the ssm_conv rows 6/6 under three build variants; I have no HRX device on this box, and the ssm_conv.q4_k expectation change (unfused when Q4_K is forced stored) is exactly what the format-4 branch implies.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at a591023: READY
cl-1051. I reviewed this by reading the code at a591023. I did not build or run it; the run results come from the PR body.
- Q6_K format 46 matches the plain prefill matcher.
q6_packedinmatch_mul_mat_conv4_dispatchapplies the same three checks asq6k_packed_tail_appliesindispatch-mul-mat.cpp:q6k_packed_rows_weight_ok, no GET_ROWS reader, andq6k_packed_rows_enabled. Conv4 always runs 512 tokens, which is inside the plain matcher's 5 < tokens ≤ 2048 window. So whenever the fused path bindskQ6KPackedK256Row64ScaleRowLayout, the rest of prefill reads the same copy, and a weight that a GET_ROWS also gathers stays stored (format 6). That avoids the conflicting-layout refusal from ggml-org#1316. - The kernel accepts format 46.
ggml_mul_mat_quantized_f16_prefill_conv4and_conv4_interiorboth callggml_mul_mat_quantized_f16_prefill_wave32_body, and that body andggml_f16_prefill_publish_weightsboth branch onweight_format == 46. - Q4_K declines exactly where #320 does. The check is
q4k_aligned_prefill_reads_stored(Stored switch, or file-mapped with decode reading it stored), so the plain matchers then take the stored 8-bit-block prefill and the separate conv. The test change follows the same switch: it expects the unfused sequence only for Q4_K underGGML_HRX_Q4K_PACKED_TAIL=0. In the default run the test weight is not file-mapped, so the fused arm is still the one compared.
Non-blocking:
- The GET_ROWS loop duplicates
weight_has_native_layout_reader, which is file-static indispatch-mul-mat.cpp. Moving it intodispatch-q4k-packed-tail.hwould let both matchers share one definition. - The default
test-hrx-opsrun never reaches format 46 in conv4. The test weight sits in a WEIGHTS buffer but is not file-mapped, so only theGGML_HRX_Q6K_PACKED_ROWS=1run reported in the body covers it. Assm_conv.q6_karm that forces packed rows would keep that path in the default suite.
…ngine ggml-org#2137) (#325) #324 hands a 2-to-8-token batch to the packed prefill kernels when prefill reads the weight packed. A weight a GET_ROWS also reads (a tied token embedding, engine ggml-org#1316) is never packed: prefill reads it stored through weight_has_native_layout_reader, so declining the kquant kernel only moved the batch to the stored f32 WMMA kernel. Same clause as the conv4 matcher (#322). Asked in review on #324. Two host rows (Q4_K, 6 and 8 tokens) fail without the clause. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Engine ggml-org#2137, the dense 27B's remainder after #320: 16 weights held twice -> 0 (cl-hrxmem).
Change
The fused projection+conv prefill matcher (
llm.ssm_conv.quantized_prefill) is the last place the 27B gets a second copy of a weight.q4k_aligned_prefill_reads_stored, the same test hrx: prefill keeps the stored layout for a weight that already has a stored device copy (engine #2137) #320 uses for the aligned Q4_K prefill), the matcher now declines; the plain matchers take the projection on the stored 8-bit-block prefill kernel and run the conv separately.GGML_HRX_PREFILL_FOLLOWS_DECODE=0orGGML_HRX_Q4K_PACKED_TAIL=1restores the fused kernel.q6k_packed_rows_enabled()says prefill reads the weight packed and no GET_ROWS reads it stored.GGML_HRX_Q6K_PACKED_ROWS=0restores format 6.test-hrx-ops: thessm_conv.q4_krows expect the unfused path when Q4_K is forced stored (GGML_HRX_Q4K_PACKED_TAIL=0), so their output comparison runs on that path too.Measured
Base = fork head
5816f7b, new = this branch, same build, onlylibggml-hrxswapped (lddchecked). Qwen3.8-27B UD-Q4_K_XL,llama-completion -c 4096 -n 48, long prompt.Copies (
GGML_HRX_HOST_WEIGHT_TRACE=2), arms base / new / new / base, text md591fdacac410din all four:a591023)-462.9 MiB device memory. (At
9601252, the Q4_K part alone: 851 / 15,759.5 MiB / 1 held twice.)Rates, trace off, six arms new / base / new / base / base / new, text identical in all six:
Not slower; no gain claimed.
Suites at
a591023:test-hrx-ops267/267;ssm_convrows 6/6 under default,GGML_HRX_Q6K_PACKED_ROWS=1,GGML_HRX_Q6K_PACKED_ROWS=0andGGML_HRX_Q4K_PACKED_TAIL=0;hrx-backend-testhost 81/81, device 12/12.Note: under
GGML_HRX_Q4K_PACKED_TAIL=0the fulltest-hrx-opsalso failsffn.q4_k.prefill.large_activation.{256,512}on a kernel-name assertion. That comes from #320's forced path, which no suite runs with, and is not touched here.🤖 Generated with Claude Code