You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
hrx: disjoint transients no longer share an arena slot (arena 256 -> 512 on the same plan) #534
hrx-backend-test's disjoint_transient_plan_packing asserts that two transients whose lifetimes are
disjoint share one arena slot: 4 × ggml_binary_f32, 2 allocations, transients.arena_size == 256.
On the current pin (llama.cpp af04c9f2 + hrx-system 82804197b, host/CPU run, no device) the same graph
plans the same four ggml_binary_f32 dispatches and the same 2 allocations, and the command program still
verifies clean (verify_command_program: ok=1 errors=0), but the arena is 512 bytes:
dispatch/transient-allocator.cpp:364 sets plan.arena_alignment = 256 and :449 rounds the arena up to
it, so two 128-byte transients that used to land in one 256-byte arena now occupy 256 bytes each — i.e. the
lifetime intervals are no longer being coalesced, or the per-allocation alignment is now 256.
Correctness is unaffected (the program verifies, and the packing has no effect on results), so this is a plan-efficiency regression: every graph with disjoint small transients now allocates twice the arena it
needs. Small in absolute terms (256 bytes here), but it is the mechanism the case exists to protect, and on
larger graphs the same change multiplies the transient footprint.
Found while finishing engine #445's classification; the case is left red (or tagged against this issue)
rather than having its expectation bumped to 512, because 512 is not the behaviour the allocator's own
alignment constants imply.
Bisect answer: the split is not in the 10-18 .. e7e5cd08 range. The case has been red since the guarded
packer landed on 2026-10-05, and the 256 in the case was the stock packer's number, not this pin's.
What the graph actually plans
Same graph as the case (4 x ggml_binary_f32, out0 = add(sum0, c), out1 = add(sum1, f)), host run,
hrx-system 82804197bc:
default (guarded packer) arena 512 allocs 2 value 2 size 32 offset 0
value 7 size 32 offset 256
GGML_HRX_TRANSIENT_REUSE=legacy (stock) arena 256 allocs 2 value 2 size 32 offset 0
value 7 size 32 offset 0
GGML_HRX_TRANSIENT_REUSE=legacy at 953b6397 (the 10-18 pin): arena 256, both offsets 0
default at 953b6397 (the 10-18 pin): arena 512 value 2 offset 0, value 7 offset 256
So the mechanism is the packer choice, not the sizes: dispatch/transient-allocator.cpp:364 still sets arena_alignment = 256 and rounds the arena up to it at :449, and each transient is 32 bytes with alignment
256; the difference is only whether sum1 may take sum0's freed offset.
Why it stopped sharing, and why that is intended
pack_transients_without_false_dependencies() (dispatch/transient-reuse-guard.h, added by d9594130 on
2026-10-05, "transient arena reuse without false dependencies") hands a freed arena range to the next value only
when that value's first command already depends, through the program's own data dependencies, on every command
that used the range before. In this graph the two chains are independent - out0 does not feed out1 - so
reusing the offset would make the recorder put a write-after-read barrier between two commands that share no
data, which is what the guard exists to prevent. sum1 gets fresh space instead.
The guard's own policy bounds the cost: a program keeps the guarded packing only while its arena stays within
twice the unguarded one plus 8 MiB, and above a 16 MiB unguarded arena it falls back to the stock packer.
Why the bisect range looked plausible but is empty
Everything under ggml/src/ggml-hrx/dispatch/ that changed between 953b6397 and e7e5cd08 is one commit, 61e19a45 (a 7-line zero-element CPY elision in dispatch-scheduler.cpp). transient-allocator.cpp and transient-reuse-guard.{h,cpp} are byte-identical across the whole range, and the
case already measured 512/0/256 at 953b6397. The regression the issue describes is therefore not a regression:
the expectation predates the guard.
Resolution
Fork PR 1bit-MONSTER/llama.cpp#149 (stacked on #148): the case pins the guarded layout
(arena 512, offsets 0 and 256), documents the stock-arm numbers above as the reason the old 256 was written, and
the #534 known-fail marker is retired. hrx-backend-test at the pin is 62 passed, 0 failed, 4 known-fail
(host) and 11 passed, 0 failed (device, HRX0). If the guarded packing is ever measured to cost more than the
barrier it avoids, GGML_HRX_TRANSIENT_REUSE=legacy is the switch and this case is the check.
Intended behaviour, now pinned by fork 1bit-MONSTER/llama.cpp#149 (merged): the guarded transient packer (fork d9594130, 2026-10-05) refuses to share an arena slot between two independent chains because sharing would add a false write-after-read barrier; arena 512 is the correct layout for that graph, 256 was the stock packer's. The test documents both arms (GGML_HRX_TRANSIENT_REUSE=legacy reproduces 256).
Activity
bong-water-water-bong commentedon Oct 25, 2026
Bisect answer: the split is not in the
10-18 .. e7e5cd08range. The case has been red since the guardedpacker landed on 2026-10-05, and the 256 in the case was the stock packer's number, not this pin's.
What the graph actually plans
Same graph as the case (
4 x ggml_binary_f32,out0 = add(sum0, c),out1 = add(sum1, f)), host run,hrx-system
82804197bc:So the mechanism is the packer choice, not the sizes:
dispatch/transient-allocator.cpp:364still setsarena_alignment = 256and rounds the arena up to it at:449, and each transient is 32 bytes with alignment256; the difference is only whether
sum1may takesum0's freed offset.Why it stopped sharing, and why that is intended
pack_transients_without_false_dependencies()(dispatch/transient-reuse-guard.h, added byd9594130on2026-10-05, "transient arena reuse without false dependencies") hands a freed arena range to the next value only
when that value's first command already depends, through the program's own data dependencies, on every command
that used the range before. In this graph the two chains are independent -
out0does not feedout1- soreusing the offset would make the recorder put a write-after-read barrier between two commands that share no
data, which is what the guard exists to prevent.
sum1gets fresh space instead.The guard's own policy bounds the cost: a program keeps the guarded packing only while its arena stays within
twice the unguarded one plus 8 MiB, and above a 16 MiB unguarded arena it falls back to the stock packer.
Why the bisect range looked plausible but is empty
Everything under
ggml/src/ggml-hrx/dispatch/that changed between953b6397ande7e5cd08is one commit,61e19a45(a 7-line zero-element CPY elision indispatch-scheduler.cpp).transient-allocator.cppandtransient-reuse-guard.{h,cpp}are byte-identical across the whole range, and thecase already measured 512/0/256 at
953b6397. The regression the issue describes is therefore not a regression:the expectation predates the guard.
Resolution
Fork PR 1bit-MONSTER/llama.cpp#149 (stacked on #148): the case pins the guarded layout
(arena 512, offsets 0 and 256), documents the stock-arm numbers above as the reason the old 256 was written, and
the
#534known-fail marker is retired.hrx-backend-testat the pin is62 passed, 0 failed, 4 known-fail(host) and
11 passed, 0 failed(device, HRX0). If the guarded packing is ever measured to cost more than thebarrier it avoids,
GGML_HRX_TRANSIENT_REUSE=legacyis the switch and this case is the check.bong-water-water-bong commentedon Oct 25, 2026
Intended behaviour, now pinned by fork 1bit-MONSTER/llama.cpp#149 (merged): the guarded transient packer (fork d9594130, 2026-10-05) refuses to share an arena slot between two independent chains because sharing would add a false write-after-read barrier; arena 512 is the correct layout for that graph, 256 was the stock packer's. The test documents both arms (GGML_HRX_TRANSIENT_REUSE=legacy reproduces 256).