Skip to content

hrx: Q6_K prefill can read the packed rows decode reads, one device copy per weight; fixes a bus error in GGML_HRX_Q6K_PACKED_PREFILL (engine #2137, opt-in) - #310

Merged
bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
1bit/2137-q6k-packed-tail
Nov 11, 2026
Merged

bong-water-water-bong merged 1 commit into
1bit/hrx-vulkan-patchedfrom
1bit/2137-q6k-packed-tail

Conversation

@bong-water-water-bong

Copy link
Copy Markdown

Refs 1bit-MONSTER/engine#2137. Second part, after #308 (Q4_K). Opt-in: the default path does not change. It also fixes a fault in an existing opt-in.

What was left after #308

Qwen3-4B Q4_K_M (216 q4_K, 37 q6_K; 2,375.9 MiB), GGML_HRX_Q4K_PACKED_TAIL=1, after a 1,700-token prompt and a 16-token reply: 2,744.0 MiB of device copies. The extra 368 MiB is 35 q6_K weights, each held twice: prefill reads q6_K in the stored layout at every batch size, decode reads it in the packed row layout.

The existing GGML_HRX_Q6K_PACKED_PREFILL=1 does not change that. Its dispatch selects a different kernel but binds the stored layout, and on 256-aligned batches that kernel reads the stored rows (measured: registration common.mul_mat.q6_k_prefill_wave32 binds ggml-native with the switch on).

The change

GGML_HRX_Q6K_PACKED_ROWS=1: prefill reads q6_K in the packed rows decode reads, at every batch size.

A fault in GGML_HRX_Q6K_PACKED_PREFILL=1, measured and fixed

Off its wide tile (tokens not a multiple of 256, or outputs not a multiple of 128) that switch's kernel reads 17,536-byte int8 tiles, while the dispatch binds the 13,440-byte stored rows. It read past the weight. Before this PR:

GGML_HRX_Q6K_PACKED_PREFILL=1 test-backend-ops test -b HRX0 -o MUL_MAT -p "type_a=q6_K.*weight=resident"
  MUL_MAT(type_a=q6_K,m=2048,n=128,k=4096,weight=resident):
buffer_get: HRX buffer download failed
Bus error (core dumped)                                           rc 135, 9 cases OK before it

The switch now selects that kernel only on the wide tile, where it reads the rows it is bound to. After: the same command, 28 OK, 0 FAIL, rc 0. The 128- and 384-token cases that found it are in this PR.

Measured at ccf14f9c9 (gfx1151, Qwen3-4B Q4_K_M, each block under a gpu-hold ticket)

"on" is GGML_HRX_Q4K_PACKED_TAIL=1 GGML_HRX_Q6K_PACKED_ROWS=1.

off on
device copies, -p 100 2,375.9 MiB 2,375.9 MiB
device copies, -p 1700 4,032.5 MiB 2,375.9 MiB
device copies, -p 1700 -n 16 4,400.6 MiB 2,375.9 MiB (one copy per weight)
llama-perplexity, wikitext-2, 12 chunks, -c 300 -b 300 19.2890 19.2890
same, -c 384 -b 384 18.1680 18.1680
same, -c 512 -b 512 16.9054 16.9054
test-backend-ops -o MUL_MAT on HRX0 641 OK, 0 FAIL 641 OK, 0 FAIL

llama-bench -p 300,512,1700 -n 16 -r 4, off on off on (tok/s; this block was run on the same sources one rebase earlier):

off on off on
prefill 300 1,135.8 1,238.6 1,117.6 1,279.5
prefill 512 3,172.1 3,126.1 3,145.1 3,292.5
prefill 1,700 2,211.7 2,319.6 1,901.1 2,307.0
decode 74.9 67.8 74.1 74.6

Prefill at 300 tokens is faster with the switches on, at 512 it is within the spread, at 1,700 it is faster. Decode binds the same packed copy in both arms; the 67.8 is one arm of four ([66 66 69 70]) and the other "on" arm reads 74.6, so I take it as the box, not the change, but it is one pair.

hrx-backend-test host: 69 passed, 0 failed. The matmul suite includes 14 new weight=resident q6_K cases (22 to 384 tokens, an output that is not a multiple of 128).

Not covered

🤖 Generated with Claude Code

…ight has one device copy (engine ggml-org#2137, opt-in)

Q6_K is read in the stored layout by prefill at every batch size and in the packed row layout by decode, so after a
prompt and a reply each Q6_K weight had two device copies. With GGML_HRX_Q6K_PACKED_ROWS=1 prefill reads the packed
rows too: the tiled kernels as weight format 46 (new 4-wide reader), the 128-aligned f16 prefill kernel as
weight_layout 2 (new 16-wide pair reader, in its wide tile and its ordinary body). Only for a weight in a weights
buffer. The stored path stays the default.

GGML_HRX_Q6K_PACKED_PREFILL, the older switch on the aligned kernel, bound the stored layout while its kernel read
17,536-byte int8 tiles off its wide tile: MUL_MAT(q6_K, m=2048, n=128, k=4096, weight=resident) with it on ended in
"HRX buffer download failed" and a bus error. It now selects that kernel only on the wide tile, where the kernel
reads the stored rows it is bound to.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Author

ready for review at ccf14f9 (opt-in; also restricts GGML_HRX_Q6K_PACKED_PREFILL to the tile where its kernel is correct; device and host rows in the body)

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT at ccf14f9: READY

The Q6_K counterpart to #308, and it comes with a confession worth having: "It also fixes a fault in an existing opt-in." A follow-on that reports a defect in the flag it is extending is more valuable than one that only adds a case — the first opt-in path was shipped with a bug, and the second part found it because extending it meant reading it.

Opt-in again, with "the default path does not change", and filed against ggml-org#2137 — the residual ggml-org#856 deliberately left. That is now the second part of a residual being taken in order (#308 Q4_K, this Q6_K), which is what makes the "document the duplicate and track it" decision on ggml-org#856 defensible rather than a way of deferring.

The diff replacing the inline packed_tail conjunction with a helper continues the right direction: what was four && clauses on two formats is becoming a named predicate, which is where the shape decision belongs.

@bong-water-water-bong
bong-water-water-bong merged commit d229429 into 1bit/hrx-vulkan-patched Nov 11, 2026

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: CHANGES at ccf14f9

The gate is right and the arms miss exactly the half that produced the bus error.

The new gate is token_count % 256 == 0 && match.output_size % 128 == 0 — two conditions, AND. The reported fault was:

MUL_MAT(type_a=q6_K,m=2048,n=128,k=4096,weight=resident):  buffer_get: HRX buffer download failed / Bus error, rc 135

m=2048 is the output size → 2048 % 128 == 0 ✔. n=128 is the batch → 128 % 256 != 0 ✘. So the failing case is "outputs aligned, tokens not" — the first condition was satisfied and the second
was the one that had to hold.

The arms you added are n ∈ {22, 100, 164, 300} for q6_K. Measured:

n n % 256 n % 128 covers which half of the AND
22 22 22 neither condition satisfied
100 100 100 neither
164 164 36 neither
300 44 44 neither
128 128 0 outputs aligned, tokens not ← the reported case
384 128 0 same half, larger

Every arm sits in the "neither" cell. A dispatch that tested only token_count % 256 == 0 and dropped the output_size % 128 conjunct would pass all four — and that is the conjunct the
reported case exercised. The AND is only tested in one direction today.

Ask: add n = 128 (the exact reported geometry) and n = 384 for q6_K. Two lines, and they are the cheapest regression pin available for a fault that cost a Bus error and a core
dump. If m also has an off-128 variant worth covering, m = 2048, n = 128 is the reported one and m = 2100 would be its counterpart.

What I verified and like: the fix selects the from_packed kernel only when the tile is genuinely wide ("the from_packed kernel reads the stored rows on its wide tile (tokens a multiple of
256, outputs of 128) and 17,536-byte int8 tiles on every other tile, while this dispatch binds the stored layout"
), which is the correct shape — the binding and the kernel's expectation
agree or the kernel is not chosen. Opt-in default unchanged (GGML_HRX_Q6K_PACKED_ROWS=1), and the appended arms for q4_K at n ∈ {6, 22, 164, 300} cover the same off-grid batch question
for the other type.

Measured, not run: the coverage arithmetic above. The device run (28 OK vs 9 OK then rc 135) is yours and needs GGML_HRX=ON.

bong-water-water-bong added a commit that referenced this pull request Nov 11, 2026
…out by default, so it has one device copy (engine ggml-org#2137) (#313)

llama.cpp #308 and #310 let prefill read the packed rows decode reads, behind
GGML_HRX_Q4K_PACKED_TAIL=1 and GGML_HRX_Q6K_PACKED_ROWS=1. The packed layout
saves a copy exactly when the stored one costs a copy, which is when the weight
is in the loader's file mapping (#304); for an uploaded weight the stored layout
is already on the device and the packed one would be the extra copy.

So the default now follows the weight's own buffer: packed for a weight in the
file mapping, stored otherwise. The switches still force either: =1 packed for
every weight, =0 stored for every weight.

Test (device-free): hrx-backend-test
packed_layout_follows_weight.{q4_k,q6_k}.{file_mapped,uploaded}.

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: Claude Fable 5.2 <noreply@anthropic.com>
@bong-water-water-bong
bong-water-water-bong deleted the 1bit/2137-q6k-packed-tail branch November 12, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant