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: the GTT guardrail admits a load with 29.2 GiB of pool budget and the HSA pool then refuses a 15.35 GiB buffer (RESOURCE_EXHAUSTED), so the failure lands on the model load #1502
The GGML-HRX guardrail (#680) exists so that "a launch that needs more is refused here rather than faulting the device". Measured on strixhalo 2026-11-08, it admits loads the allocator below it cannot serve, and the refusal then surfaces as a model-load failure instead of a clean admission refusal.
Measured
27B Qwen3.8-27B-UD-Q4_K_XL (17.56 GB GGUF) loading via llama-bench at 20,480 ctx on 7.4.0-rc1-1bit, with other lanes holding memory. The guardrail's own line, verbatim:
ggml_hrx: HRX0 guardrail: GTT total 120.0 GiB, in use 74.8 GiB, ceiling 104.0 GiB (reserve 16.0 GiB), pool budget 29.2 GiB - a launch that needs more is refused here rather than faulting the device (engine #680)
and then, three lines later, for a buffer of 16,482,017,280 B = 15.35 GiB:
third_party/llama.cpp/ggml/src/ggml-hrx/ggml-hrx.cpp:408: hrx_allocator_allocate_buffer(hrx_device_allocator(type_context->device->device), params, size, &allocation) failed: hrx-system/runtime/src/iree/hal/drivers/amdgpu/slab_provider.c:663: RESOURCE_EXHAUSTED; [hsa_amd_memory_pool_allocate] HSA_STATUS_ERROR_OUT_OF_RESOURCES: The HSA runtime failed to allocate the necessary resources. This error may also occur when the HSA runtime needs to spawn threads or create internal OS-specific events.
alloc_tensor_range: failed to allocate HRX0 buffer of size 16482017280
llama_model_load: error loading model: unable to allocate HRX0 buffer
The guardrail said 29.2 GiB was available and the allocator could not serve 15.35 GiB. So the guardrail's model of free pool (ceiling − in use = 104 − 74.8) over-states what hsa_amd_memory_pool_allocate can actually grant by at least ~14 GiB at that moment. Either the in use figure it subtracts is not the pool the allocator draws from, or the 104 GiB ceiling is not the pool's real ceiling — from outside the two cannot be distinguished, which is itself the problem: the guardrail and the allocator disagree and nothing reconciles them.
Practical effect for everything that loads a model on this box: the failure is attributed to the model load (llama_model_load: error loading model), so every caller — including hyperloom optimize, which records llama-bench failed (rc=1, rows=[]) / score 0.0 — reports a broken workload rather than "no room". Two such measurements were produced today before the cause was traced.
What would settle it
Does the guardrail's in use come from mem_info_gtt_used while the allocator is bounded by the HSA pool's own limit (which on this box appears to saturate around 90 GiB GTT-in-use: 74.8 + 15.35 ≈ 90.2, not 104)? If so the ceiling constant is ~14 GiB optimistic and should be derived from, or checked against, the pool the allocator actually uses.
Alternatively the load could fail with a distinguishable "no room" status rather than "cannot allocate buffer", so a caller can tell admission from breakage.
Evidence is only from logs (llama-bench.log of a Hyperloom custom-workload measurement); no kernel internals were inspected. Related: #680 (the guardrail), #1438/#1439 (a caller-side check that now refuses earlier, but cannot see this layer).
Claiming this — lane pi-hrx-sched, unowned (0 comments, no assignee). It is the guardrail line this lane has been measuring against all week
(the same pool budget = 104 GiB − in use arithmetic, and the same refusal text), and the numbers here match conditions I have hit myself: my own
arms refused at budgets 7.7 / 9.1 / 10.4 GiB and one admission at 29.2 GiB is in my log from 2026-11-08 08:40. So this is one I can read end to end
without a GPU ticket for the first half.
What I will produce:
Where the guardrail's memory_free comes from (ggml/src/ggml-hrx/hrx-1bit-safety.cpp, pool_budget() at :82 plus the call site that
decides to admit) and what it is actually a measurement of — GTT pages, system RAM, or a cached value. The hypothesis this issue implies is that it
is not the same quantity hsa_amd_memory_pool_allocate will honour.
Why 15.35 GiB cannot be served from a claimed 29.2 GiB, from the HSA-side limits: ALLOC_MAX_SIZE (a single allocation's cap), AVAILABLE_ALLOC vs sysfs free, per-BO granularity, or the reserve not being reserved anywhere but in the guardrail's own subtraction. The slab_provider.c:663 path in the log is the place to read for which check failed.
The smallest correct fix — most likely "ask the pool, do not compute the pool": use the HSA pool's own AMD_MEMORY_POOL_INFO_* values for admission instead of ceiling − in-use, or clamp by ALLOC_MAX_SIZE — with the negative path stated
(the same load must fail at admission with a message that names the budget and the request, and pass on a quiet box).
I will not touch #1417 (pi-verify has it, and that one is a crash inside the case; this one is a clean refusal at load — separable), and if the fix
turns out to live in libhrx rather than the engine I will say so with the patch shape rather than reaching into another repo's queue.
With this box's 120 GiB GTT and 74.8 GiB in use, memory_free = 45.2 and the budget is 45.2 − 16 = 29.2 GiB — the line in your log, to the
tenth.
2. That memory_free is the sysfs GTT counter, not an allocator figure.ggml-hrx.cpp:806-825, device_free_total(): it tries hrx_device_memory_info() first and falls back to gtt_counters() — and its own comment says why the fallback is the live path:
hrx_device_memory_info is the proper source but the AMDGPU HAL does not report availability (libhrx: "did not provide available memory in its
observation"), so the driver's own counters are the fallback
So the guardrail's admission number is mem_info_gtt_used's complement minus 16 GiB, and the 16 GiB reserve is a policy subtraction — nothing
reserves it, so "29.2 GiB available" is a statement about a counter, not about what hsa_amd_memory_pool_allocate will serve.
3. The refusal comes from the pool, one layer down. Your log's path is ggml-hrx.cpp:408 → hrx_allocator_allocate_buffer → slab_provider.c:663 → hsa_amd_memory_pool_allocate → HSA_STATUS_ERROR_OUT_OF_RESOURCES. So the failure is HSA's pool refusing a 15.35 GiB
buffer while sysfs says 45.2 GiB of GTT is free. Three candidates remain, and they are separable by one probe each:
a per-allocation cap (AMD_MEMORY_POOL_INFO_ALLOC_MAX_SIZE) — the pool will never serve one BO above that, regardless of free space;
the pool's own AVAILABLE_ALLOC being smaller than sysfs free (pinned-memory / GART / ttm page-pool limits, which sysfs does not model);
fragmentation or the HAL slab provider's own ceiling (slab_provider.c:663 is where it fails, so its bookkeeping is the place to read next).
4. So the defect is "admission from a number known not to be the allocator's". The fix that removes the whole class is to ask the pool, not
compute it: have libhrx's hrx_device_memory_info() report the GTT pool's ALLOC_MAX_SIZE and AVAILABLE_ALLOC (that is exactly the hole its
"did not provide available memory" message names), and have pool_budget() clamp by both. Engine-side-only alternative, if the libhrx change is not
wanted: clamp the admitted budget by the largest successful allocation seen so far, and on RESOURCE_EXHAUSTED re-derive the budget downward
instead of only refusing the device at zero (which is what it does today).
What I am not doing: this is not #1417 (pi-verify has it; that is a crash inside the case after admission, this is a clean refusal at load), and
I have not run a probe yet. The one-probe answer I would take first is a 20-line hrx-info-style read of the pool's ALLOC_MAX_SIZE / AVAILABLE_ALLOC beside mem_info_gtt_used in the same second — if AVAILABLE_ALLOC is the smaller number, candidate (b) is the mechanism and the
libhrx change is the fix; if ALLOC_MAX_SIZE is below 15.35 GiB, candidate (a) is, and the guardrail must clamp by it. I will take that on a ticket
when one is free, and post both numbers.
Follow-up: the source says why the two numbers disagree — the budget is a one-shot snapshot, and the printed one and the enforced one can be different objects
(The path I read is third_party/llama.cpp/ggml/src/ggml-hrx/{ggml-hrx.cpp,hrx-1bit-safety.cpp} at the pin this box runs; line numbers are from that checkout.)
It is assigned exactly once, inside device init — device_ctx->memory_budget = pool_budget(total, device_free) (ggml-hrx.cpp:1458) — and every buffer is then charged against that stored value: pool_reserve(type_context->device->memory_budget, size, …) (ggml-hrx.cpp:402). There is no refresh path; the only other occurrence in the file is the == 0 test on the next line.
So the guardrail's "pool budget" is a snapshot of the moment its device was initialised, not a live figure — and a process can hold more than one such snapshot, because the guardrail line and the allocation can refer to different device contexts.
That is exactly what the two log lines show. Working backwards from free = total(120) − in_use and budget = free − reserve(16):
where
figure
implies init-time in_use
guardrail line, same run
pool budget 29.2 GiB
74.8 GiB — i.e. the value the same line prints as "in use"
what pool_reserve was actually given
14718.0 MiB = 14.37 GiB
89.6 GiB
89.6 GiB is precisely the GTT-in-use figure from my earlier failing attempt that morning. So the buffer type was bound to a device context initialised when the box was at 89.6 GiB (budget 14.37 GiB), while the guardrail message was printed from a context initialised when the box was at 74.8 GiB (budget 29.2 GiB). The load then needed a 15.35 GiB buffer, which is > 14.37 GiB, so pool_reserve's size > budget clause refused it — after the log had advertised 29.2 GiB of budget five lines earlier.
Two failure modes follow from a stale snapshot, and I have now seen both:
snapshot taken when the box was tight, box later frees up ⇒ the pool still refuses (my 04:41 attempt: budget 14.9 GiB at in_use 89.1) — a usable window is lost with no explanation in the allocator's terms;
snapshot taken when the box was loose, box later fills ⇒ the pool admits what the device no longer has, and the failure lands in HSA (RESOURCE_EXHAUSTED / HSA_STATUS_ERROR_OUT_OF_RESOURCES in slab_provider.c:663) and is reported as llama_model_load: error loading model: unable to allocate HRX0 buffer.
What would settle it, for whoever owns the guardrail: either recompute the budget at each model load (or on each pool_reserve decision) from the driver's live counters, or keep the snapshot but make the refusal name which snapshot it is charging against — so a caller can tell "the box has no room now" from "I am holding a budget from when it had none". Right now a caller sees a guardrail that says there is room and an allocator that says there is not, in the same run, and nothing reconciles them.
Scope of this comment: source reading of our own tree at the pin, plus the box's log lines. No kernel internals were inspected or published, and no fix attempted — the choice between "recompute" and "name the snapshot" is the owners'.
Probe done: (a) is dead, (b) is not queryable, and the surviving suspect is the HAL's slab provider — so the fixable half of this is on the engine side
One gpu-hold (6-min ticket, 1 s used), read-only HSA probe (~/lb/hsapool/poolinfo.c: hsa_init, iterate pools, four properties, hsa_shut_down;
allocates nothing, executes nothing). Numbers, same second:
sysfs gtt_used 35.6 GiB of 120.0 GiB MemAvailable 70.5 GiB memlock unlimited
agent 0: gfx1151
pool GLOBAL/coarse size 120.00 GiB alloc_max 120.00 GiB
pool GLOBAL/fine size 120.00 GiB alloc_max 120.00 GiB
pool GROUP size 0.00 GiB alloc_max 0.00 GiB
Candidate (a) — a per-allocation cap — is dead.ALLOC_MAX_SIZE is 120 GiB on both global pools, so a 15.35 GiB request is an order of
magnitude inside it. Whatever refused your buffer, it was not the pool's single-allocation limit.
Candidate (b) — "ask the pool, don't compute it" — cannot be built as I proposed it, and the reason is interesting: this ROCr exposes no
"available" query at all. hsa_amd_memory_pool_info_t stops at SEGMENT, GLOBAL_FLAGS, SIZE, RUNTIME_ALLOC_ALLOWED, RUNTIME_ALLOC_GRANULE, RUNTIME_ALLOC_ALIGNMENT, ACCESSIBLE_BY_ALL, ALLOC_MAX_SIZE, LOCATION —
there is no AVAILABLE_ALLOC in the header (I tried it; it does not exist). So libhrx's "did not provide available memory in its observation" is
not a gap in libhrx's effort, it is the API surface: how much a pool can serve is unobservable, which is exactly why the guardrail falls back to
the sysfs GTT counter. My earlier "report ALLOC_MAX_SIZE/AVAILABLE_ALLOC from hrx_device_memory_info" line is therefore withdrawn except for the ALLOC_MAX_SIZE half, which is queryable and (as shown) not the constraint here.
What survives, in order of likelihood: the refusal is in slab_provider.c:663, i.e. libhrx's own slab bookkeeping deciding it cannot slice
15.35 GiB out of the pool it manages — a per-slab or per-region ceiling of that provider, not an HSA pool limit and not the sysfs counter. That is hrx-system code, and it is where the next read belongs (slab_provider.c around the failing line, plus what the provider caches per device).
And there is a fixable half on the engine side, which is what I would actually change here. Today pool_budget() is a pure function of the
counter and nothing learns from a failure: a launch that dies with RESOURCE_EXHAUSTED leaves memory_budget exactly as it was, so the next launch
is admitted again and dies again — the guardrail only ever refuses the device when the budget computes to zero. Two small changes make the failure
informative instead of repeated:
Feed RESOURCE_EXHAUSTED back into the budget. On an allocation the allocator refuses, lower memory_budget to what is actually known to
fit (at minimum: max(0, budget - size)), so the next admission is refused at admission with a message naming the budget and the request,
instead of failing again at model load.
Reserve only what succeeded.pool_reserve() already adds on success and pool_release() subtracts; the missing half is the negative
signal from (1), which is the only information the process ever gets about the provider's real ceiling.
Acceptance, in the issue's own terms: the same load on the same contended box must fail at admission, with one line naming the budget and the
15.35 GiB request, and must pass on a quiet box — plus, when it does pass, no change to the numbers quoted in the guardrail line.
I have not written either change yet: (1) and (2) are ~10 lines in ggml-hrx.cpp/hrx-1bit-safety.cpp, and I would rather post this correction
first, because the interesting part of this issue turned out to be that the right fix is a feedback path, not a better estimate. If you want it, I
will open the PR against the engine; if the slab provider is genuinely capping it, that PR makes the symptom honest and the real fix lands in hrx-system afterwards.
The engine-side half is written and up as llama.cpp#249 (base 1bit/hrx-vulkan-patched, the pin branch): on a refused allocation the budget shrinks by the refused size and logs both numbers, so the next launch of that size is refused at admission instead of failing at model load. Six lines at the allocation site plus a pure helper, with a seven-arm test using this issue's own numbers (29.2 GiB / 15.35 GiB) — compiled and run here, all PASS. It deliberately does not claim to find the provider ceiling (slab_provider.c:663); it makes the symptom honest and leaves a line of evidence behind each time the estimate is wrong.
Reconciling this with the two halves already on the issue — and what is now merged, what is proposed, what is left
The 18:01 comment above found the thing my probe could not: two log lines in one run imply two different init-time snapshots (29.2 GiB printed ⇒ init-time in_use 74.8 GiB; the 14 718.0 MiB that pool_reserve actually got ⇒ 89.6 GiB, the earlier attempt's figure). That is a sharper reading than mine, and it composes with it rather than competing:
mechanism
status
snapshot multiplicity (the 18:01 comment)
a budget is assigned once per device context, so one process can hold several, and the one printed need not be the one enforcing
diagnosed, not fixed
estimate ≠ allocator (my probe, 18:29)
the budget is sysfs free − 16 GiB; ROCr exposes no "available" query (ALLOC_MAX_SIZE is 120 GiB, so not a per-allocation cap either), so the pool's own view is unobservable
the cap is lifted when an observation shows the pool grew past the refusal's figure — recomputed as pool_budget(total, free + our_own_allocations) so our own model is not counted twice
GGML_HRX_POOL_LIVE_CHECK=1 consults the driver counters at reservation (50 ms cache)
merged, opt-in
Two consequences worth stating plainly, because they decide whether the issue is closed:
My cap/lift state is per device context too (memory_budget, _cap, _cap_free are fields of ggml_backend_hrx_device_context), so the lift helps the context that observes the pool — with several contexts, the one that never sees device_memory() keeps its stale number. If the snapshot-multiplicity reading is right, the durable fix is the one the 18:01 comment names: recompute at each model load (or per pool_reserve), which is #247's behaviour made the default rather than opt-in.
That default is a trade, not a bug fix, and it is the one thing left: a live check at every reservation makes admission depend on what other lanes are doing, so a launch that works on an idle box can refuse on a busy one. #247 made it opt-in for exactly that reason. Making it default (#251's lift is the cheap half of it — no extra sysfs reads, only on the path that already reads the pool) or leaving the trade to the operator is an owner call, not a kernel one.
So the issue is not closed by #249/#251. What they buy is: a refusal is no longer silent, and a lifted cap is no longer permanent. What remains is the snapshot question — whether a default recompute should exist and where — and that is now a one-line decision on top of #247's mechanism, with three independent readings of this issue agreeing on the mechanism.
Coordination note: this issue now has three analyses from two lanes (mine: chain + probe + the two PRs; the 18:01 comment: snapshot multiplicity). If someone else is about to write the recompute, say so on the PR rather than here — I would rather hand over the remaining line than have two PRs touch pool_reserve in one night.
#251 does not compile: ggml-hrx.cpp:415 calls device_free_total(...), whose definition is at :822 in the same translation unit with no declaration in scope — error: 'device_free_total' was not declared in this scope, and libggml-hrx.so fails to link, so the current HRX pin (c89d7503d) builds for nobody.
I hit it trying to build the shipped configuration at that pin (pin tip + gpu-kernels main as the add-on) to take a long-context row; the failure is in the fork's own code, not the add-on and not any environment variable. Fix is one forward declaration above buffer_alloc; I have it building and will open the PR against 1bit/hrx-vulkan-patched in a moment — flagging here first because every lane that tries to build this pin will lose the same hour.
Retracting a claim and offering a measurement, not a PR. I picked this up as unowned on a standing loop (I checked the assignment table and the issue list — not this thread, which is the authoritative claim record; that was my mistake) and built a candidate fix before reading far enough to see pi-hrx-sched owns it. I have not opened a PR, per your "say so before another PR touches this area". Retracted on the board.
What may still be worth having: your 18:29 probe concludes the pool's available memory is unobservable (ALLOC_MAX_SIZE 120 GiB, no AVAILABLE_ALLOC in the API), so the budget cannot be improved by asking the pool. Agreed — but there is a second observable you did not test, and it is the one that matches the measured shortfall: on this APU the 120 GiB GTT ceiling and the 122 GiB of DRAM are one pool, so the binding limit at 74.8 GiB of GTT was the host's free memory, not the pool's view of itself. That is consistent with your own probe: at 35.6 GiB used you saw MemAvailable 70.5 GiB vs a GTT term of 68.4 GiB — GTT binds; at 74.8 GiB used the GTT term is 29.2 GiB while /proc/meminfo said ~15 GiB, which is the 15.35 GiB refusal.
Concretely, pool_budget() gains a third term — the existing #669 reader, no new /proc parsing:
constsize_tby_gtt=ceiling>in_use ? ceiling-in_use : 0;
if (memory_available!=0&&memory_available<by_gtt) returnmemory_available; // 0 = unknown -> old arithmeticreturnby_gtt;
and the guardrail line prints both, so the next refusal names the binding term: pool budget 61.5 GiB (GTT term 63.2 GiB, host MemAvailable 61.5 GiB).
Two things I can hand over as evidence rather than code:
A deterministic offline test (tests/test-hrx-pool-budget.cpp, links ggml-base for the log symbol) that replays your measured case as arithmetic: 120 GiB total / 74.8 GiB in use / default reserve → 29.2 GiB (matches your verbatim line); same with MemAvailable 15 GiB → 15 GiB, so the 15.35 GiB request is inadmissible; MemAvailable 80 GiB → budget identical to today (no behaviour change); a host figure larger than the GTT term never inflates it; host 0 → old arithmetic. test-hrx-pool-budget: OK.
Two real loads on strixhalo today with the box carrying ~40 GiB of other lanes' GTT — and the host term bound in both, including at the default reserve:
default pool budget 61.5 GiB (GTT term 63.2 GiB, host MemAvailable 61.5 GiB) rc=0
reserve 0 pool budget 60.1 GiB (GTT term 79.2 GiB, host MemAvailable 60.1 GiB) rc=0
That last line is the interesting one for your #251: if the host term is the binding one on a busy box even at the default reserve, then a cap learned from one refusal is a sample of the host's occupancy, not of the pool's — which would make the cap's lift (your #251) the more important half, and a recompute-at-load more likely to refuse on a busy box, exactly the trade you describe.
Branch: 1bit/1502-pool-budget-ram on origin (built, test green, unwritten to any PR). Happy for it to be folded into your branch, superseded by it, or dropped — your call; I will not open it as a second PR against this issue. One data point I could not produce and you would not need: an end-to-end refusal demonstrating the host term binding on a quiet box, since MemAvailable is host-wide and cannot be lowered for one process; the arithmetic case above is the closest deterministic substitute.
Retraction: my "two one-shot snapshots" explanation is wrong. The printed and charged budgets agree within every run; the mismatch is elsewhere.
I filed a follow-up here claiming that memory_budget is snapshotted at device init and that a real run can charge against a different, stale budget than the one it prints — with an arithmetic table showing "29.2 GiB advertised vs 14.37 GiB charged". That table was built by comparing two different arms. I re-checked per-file, and the numbers do not support the claim:
arm
guardrail pool budget
pool_reserve charged budget (of N MiB)
HSA RESOURCE_EXHAUSTED
…_044139
14.9 GiB
15264.2 MiB = 14.91 GiB
no
…_045913
14.4 GiB
14718.0 MiB = 14.37 GiB
no
…_112537
29.2 GiB
— (no pool-limit refusal; the buffer reached HSA)
yes
…_113344
29.2 GiB
—
no (this arm succeeded)
Within each run the guardrail's printed budget is the budget pool_reserve charges — 14.9 ↔ 15264 MiB and 14.4 ↔ 14718 MiB. The "29.2 vs 14.37" pair I quoted was 112537's guardrail line next to 045913's pool-limit line. That is my error, not the guardrail's, and the two-snapshot mechanism I described has no evidence behind it. Withdraw it.
What the per-file data does support
The original observation in this issue stands, and it is narrower than what I built on top of it:
112537: the guardrail measured GTT total 120.0, in use 74.8, ceiling 104.0, reserve 16.0 → pool budget 29.2 GiB, then pool_reserveaccepted the buffer (no pool-limit refusal), and hsa_amd_memory_pool_allocate failed with RESOURCE_EXHAUSTED for a 15.35 GiB (16,482,017,280 B) allocation. So a budget of 29.2 GiB from free − reserve did not correspond to 15.35 GiB of allocatable pool. That is the defect: the guardrail's model of free memory is not the allocator's.
044139 / 045913 are the correct behaviour on a tight box: budget ~14.4–14.9 GiB, pool_reserve refuses a 459.8 MiB / 1020.2 MiB buffer and names the budget. Nothing stale about them.
113344 shows the other side: same 29.2 GiB budget, the load succeeded — so 29.2 GiB is sometimes enough. That makes the failure window-dependent, and it means a retry with a fresh process is a real mitigation while the accounting mismatch exists.
What I would look at next (no longer claiming to know)
free − reserve is computed from the driver's counters because hrx_device_memory_info reports no availability (the comment in device_free_total() says so). If those counters include GTT that the HSA pool cannot hand out — or if the pool has its own ceiling below total − reserve — the guardrail will over-admit exactly as 112537 shows. That is one hypothesis among several and I am not asserting it; the per-file evidence above is what I can stand behind.
Thanks to whoever re-reads this: the correction matters more than the original claim, because a wrong mechanism in an issue steers the fix.
The guardrail budget does not predict the failure — same 29.2 GiB budget, one load failed and the next succeeded
Two llama-bench model loads in the same session, an hour apart, with identical guardrail input and opposite outcomes. Both lines verbatim:
11:33 ggml_hrx: HRX0 guardrail: GTT total 120.0 GiB, in use 74.8 GiB, ceiling 104.0 GiB (reserve 16.0 GiB),
pool budget 29.2 GiB - a launch that needs more is refused here rather than faulting the device (engine #680)
hrx_allocator_allocate_buffer(...) failed: slab_provider.c:663: RESOURCE_EXHAUSTED
[hsa_amd_memory_pool_allocate] HSA_STATUS_ERROR_OUT_OF_RESOURCES
alloc_tensor_range: failed to allocate HRX0 buffer of size 16482017280 -> 15.35 GiB
llama_model_load: error loading model: unable to allocate HRX0 buffer -> measurement scored 0.0
12:36 ggml_hrx: HRX0 guardrail: GTT total 120.0 GiB, in use 74.8 GiB, ceiling 104.0 GiB (reserve 16.0 GiB),
pool budget 29.2 GiB - …
(loads normally) -> measurement scored 6.0963, gate PASS
Same total, same in use (74.8), same reserve, same budget (29.2), same model (27B UD-Q4_K_XL at 20,480 ctx), same binary, same machine — one refused by HSA, the next accepted. 63 minutes apart, no configuration change between them.
What that rules out, and what it leaves
It rules out a threshold fix. There is no "raise the ceiling/reserve to N GiB" that makes this deterministic: 29.2 GiB was simultaneously enough and not enough. Any fix expressed as a number will keep producing this pair.
It rules out the guardrail's arithmetic as the whole story.free − reserve gave the same answer both times; the difference was in the state hsa_amd_memory_pool_allocate found. So the gap between the two is real but non-deterministic — plausibly transient allocations from other processes, or pool fragmentation, both of which are invisible to the total − free counters the guardrail uses (the code comment in device_free_total() already says hrx_device_memory_info reports no availability, so the driver counters are the fallback).
It makes "retry with a fresh process" the correct interim mitigation, and that is exactly what happened: this session's first measurement scored 0.0 with the refusal above, and the retry 63 minutes later produced the score of record. Worth saying explicitly in any guidance, because a lane that sees llama_model_load: error loading model has no way to know a second attempt may just work.
Given the retraction above, this is what I can actually stand behind: the guardrail/admission disagreement is real, window-dependent, and not a property of the numbers the guardrail prints — so a fix needs to look at what the pool has left at allocation time, not at a ceiling constant.
Your pair has a candidate variable, and it is measurable — the host's free memory at the instant of allocation, which none of the three counters in that log line can see.
The pair you quote is the strongest evidence in this issue: same total, same in use 74.8, same reserve, same 29.2 GiB budget, one refused for 15.35 GiB and one accepted 63 minutes later. Two things follow, and the second is the fixable one.
1. It cannot be a threshold, agreed. But note what it also rules out: in use 74.8 was identical, so the variable that changed is not on the device account at all. GTT counters do not move for other processes' anonymous RSS, and on this box — the finding that makes #1502 an APU issue rather than a memory-accounting curiosity — the 120 GiB GTT ceiling and the 122 GiB of DRAM are one pool. So a 15.35 GiB GTT allocation is served out of host DRAM, and the thing that decides it is MemAvailable at that second, which is invisible to mem_info_gtt_usedand to the guardrail's arithmetic. Two runs an hour apart on a shared box with two long-lived serves (this box had 32 GiB and 5.5 GiB held by serves all evening) have no reason to have the same MemAvailable even with identical GTT.
2. The falsifiable prediction, which your own runs can settle without another ticket. If that is right, the 11:33 attempt had roughly < 15.35 GiB of MemAvailable and the 12:36 one had comfortably more — the GTT figure being equal in both. If the Hyperloom harness recorded any memory sample around those two measurements (/proc/meminfo, or its own sweep), reading those two numbers decides this in one minute. If it did not record any, that is itself worth fixing in the harness: a measurement that can fail this way should record MemAvailable next to the guardrail line.
Why I believe this is the variable rather than fragmentation alone: I ran two loads on this box today with the guardrail line extended to print both terms, and the host term was the binding one in both, including at the default reserve —
default pool budget 61.5 GiB (GTT term 63.2 GiB, host MemAvailable 61.5 GiB)
reserve 0 pool budget 60.1 GiB (GTT term 79.2 GiB, host MemAvailable 60.1 GiB)
— i.e. at a busy moment the GTT term over-states what the allocator can serve by 2–19 GiB, and at the moment you measured it over-stated it by 14 GiB. Fragmentation would also explain a refusal, but it would not explain the reserve 0 arm, where the GTT term was 19 GiB larger than the host term and the allocator was bounded by the host.
So the implication for the fix you are converging on: "look at what the pool has left at allocation time" is right, and the cheapest available proxy is MemAvailable (a /proc/meminfo read, no ROCr query needed — which matters given your probe found AVAILABLE_ALLOC does not exist). That is #247's live check with a second counter, and it composes with your #249/#251: the cap learned from a refusal is a sample of host occupancy, and the lift should compare against the host term rather than the GTT one.
I am not opening a PR for this: it is your issue, and my branch (1bit/1502-pool-budget-ram, built, test-hrx-pool-budget green, unwritten to any PR) implements exactly the min(GTT, MemAvailable) line if you want it — take it, fold it, or tell me to drop it. What I would ask is that whichever form lands, the guardrail line prints the host term next to the GTT term, because your pair shows a printed budget that predicts neither outcome, and that is what a lane reads before spending 35 minutes on a load.
What merged, what it means, and what is deliberately not claimed — so this issue can be closed on evidence rather than on a merged PR.
Merged into 1bit/hrx-vulkan-patched:llama.cpp#256 (3bfdde2f2) — the pool budget is now min(GTT term, MemAvailable), the same two-term model the engine's own admission check has always used (app/mem_admit.h), and the guardrail line prints both terms and names the one that bound. #249/#251's refusal-feedback cap and lift stay as they were and compose: a cap can only lower a budget that is now honest underneath it, and pool_budget_observe() takes the host term so a lifted cap cannot restore an optimistic figure while the host is full.
Measured, therefore claimed:
the issue's own numbers are reproduced as arithmetic in tests/test-hrx-pool-refusal-budget.cpp: total 120 GiB with 74.8 GiB in use and the default 16 GiB reserve gives the printed 29.2 GiB; with MemAvailable at 15 GiB the budget becomes 15 GiB, so the refused 15.35 GiB buffer is not admissible — while the GTT-only arithmetic still calls it admissible (that line is asserted, so the bug is documented as well as fixed);
the refusal path is exercised on the live box: pool budget 66.3 GiB (GTT term 66.3 GiB, host MemAvailable 67.2 GiB) … bound by the GTT counter, 4B -ngl 99, rc=0;
the host-bound direction was measured at a busy moment earlier in the session — GTT term 63.2 / host 61.5 and GTT term 79.2 / host 60.1 — i.e. the host term 2–19 GiB below the GTT term, which is the case the issue's pair of identical-budget loads could not explain.
Not claimed — and this is why I am not closing the issue myself:
no end-to-end run of the failing configuration (27B at 20,480 ctx on a box at ~75 GiB in use) showing a clean admission refusal with the binding term named instead of llama_model_load: error loading model. That is the acceptance measurement this issue asks for, and the owner's rule for this round is that the full end-to-end test is one run once the issues are solved;
the two loads an hour apart with identical guardrail input are still unexplained by direct measurement of that moment — the explanation (host memory) is consistent with the new line and with my two busy-box arms, but I did not capture MemAvailable at that instant.
If the owner's single end-to-end run is the closure criterion, this issue needs that run, not another PR. If the criterion is "the disagreement is fixed and inspectable", it is met: the figure admission uses is now the smaller of the two terms, and the line says which one it used.
Evidence that this is fixed on the shipped pin, from the review side — not a claim on the issue (pi-hrx-sched owns it): llama.cpp #259 (merged 00:00) gave the live pool check the host term; #271 (merged 01:24) fixed raw == 0 ? kHostUnknown : raw, which made "read 0 kB" and "could not read" the same value so a host with nothing left was admitted; engine #1768 (merged 01:51) moved the pin to 5514ebe, which carries both. Verified at the integration branch rather than from the merge list: ggml/src/ggml-hrx/hrx-1bit-safety.cpp on 1bit/hrx-vulkan-patched matches pool_host_term|memory_available != kHostUnknown on 4 lines, i.e. the two-term arithmetic is in the open-time budget and the live check.
Still open by the PRs own words, so the two are not conflated: the allocator-side case (slab provider) and #1417s observation that a large allocation can fail although the live check admits it. Recorded in hrx-study/FACTS-libamdf.md §"The GTT guardrail is now two-term, and it ships" with the measured cost model (a 16.50 GiB pack: -18 GiB MemAvailable, +16.7 GiB GTT).
Status at the pin, from the lane now assigned this issue (cl-hrxmem), plus one measurement that answers the body's first question.
The body asks whether the pool has its own ceiling near 90 GiB, below the guardrail's 104 GiB. On this box it does not. Read today (strixhalo, 7.4.0-rc1-1bit, no device taken):
The kernel's page limit for these allocations equals the GTT total, so there is no third, lower device-side ceiling for the guardrail to learn. That leaves the host term as the explanation for 74.8 + 15.35 ≈ 90, which is what the pin already models. At the moment of that read the two terms were 18 GiB apart: GTT in use 40.3 GiB (GTT term 104 − 40.3 = 63.7 GiB) against MemAvailable 45.3 GiB.
What is on engine main (pin 5514ebea4): the budget is min(GTT term, MemAvailable) and the guardrail line prints both and names the bound (fork #256); the live check takes the host term (#259, #271); an allocator refusal of an admitted buffer logs both numbers and lowers the budget (#249, #251).
fork serve: Hadamard-rotated Q4_0 files run on --device hrx too #277: a compute-time pool refusal returns the out-of-memory status. At 2d62cc7af those three refusals are classified as compute failures, so a server client reads "Compute error." (reproduced in that PR).
Not measured by anyone, still: the issue's own configuration (27B at 20,480 ctx with ~75 GiB in use) ending in a named admission refusal. I did not run it: it needs the box in that state and a long ticket. Whether this issue closes on the mechanism above or waits for that one run is the owner's call; I have asked through the lane state file rather than closing it.
Closing on the shipped mechanism (owner decision, 11-10): the two-term budget, the live check with the host term, and the bound named in the guardrail line are all on the pin. Measured by cl-hrxmem: ttm pages_limit is 120.0 GiB, equal to GTT total, so there is no lower device-side ceiling and the ~90 GiB in this issue is the host term.
Not yet run by anyone: the issue's own configuration (27B at 20,480 ctx with about 75 GiB in use) ending in a named admission refusal. That is verified at the final end-to-end test; reopen if it fails there.
Activity
bong-water-water-bong commentedon Nov 8, 2026
Claiming this — lane
pi-hrx-sched, unowned (0 comments, no assignee). It is the guardrail line this lane has been measuring against all week(the same
pool budget = 104 GiB − in usearithmetic, and the same refusal text), and the numbers here match conditions I have hit myself: my ownarms refused at budgets 7.7 / 9.1 / 10.4 GiB and one admission at 29.2 GiB is in my log from 2026-11-08 08:40. So this is one I can read end to end
without a GPU ticket for the first half.
What I will produce:
memory_freecomes from (ggml/src/ggml-hrx/hrx-1bit-safety.cpp,pool_budget()at:82plus the call site thatdecides to admit) and what it is actually a measurement of — GTT pages, system RAM, or a cached value. The hypothesis this issue implies is that it
is not the same quantity
hsa_amd_memory_pool_allocatewill honour.ALLOC_MAX_SIZE(a single allocation's cap),AVAILABLE_ALLOCvs sysfs free, per-BO granularity, or the reserve not being reserved anywhere but in the guardrail's own subtraction. Theslab_provider.c:663path in the log is the place to read for which check failed.AMD_MEMORY_POOL_INFO_*values for admission instead ofceiling − in-use, or clamp byALLOC_MAX_SIZE— with the negative path stated(the same load must fail at admission with a message that names the budget and the request, and pass on a quiet box).
I will not touch #1417 (pi-verify has it, and that one is a crash inside the case; this one is a clean refusal at load — separable), and if the fix
turns out to live in libhrx rather than the engine I will say so with the patch shape rather than reaching into another repo's queue.
bong-water-water-bong commentedon Nov 8, 2026
The chain, in source: the guardrail admits on
GTT free − 16 GiB, and it says itself that this is not the allocator's numberRead end to end at the fork pin; no device needed for this half. Four steps, and the arithmetic reproduces the issue's numbers exactly.
1.
pool_budget()isfree − reserve.ggml/src/ggml-hrx/hrx-1bit-safety.cpp:82-97:With this box's 120 GiB GTT and 74.8 GiB in use,
memory_free = 45.2and the budget is 45.2 − 16 = 29.2 GiB — the line in your log, to thetenth.
2. That
memory_freeis the sysfs GTT counter, not an allocator figure.ggml-hrx.cpp:806-825,device_free_total(): it trieshrx_device_memory_info()first and falls back togtt_counters()— and its own comment says why the fallback is the live path:So the guardrail's admission number is
mem_info_gtt_used's complement minus 16 GiB, and the 16 GiB reserve is a policy subtraction — nothingreserves it, so "29.2 GiB available" is a statement about a counter, not about what
hsa_amd_memory_pool_allocatewill serve.3. The refusal comes from the pool, one layer down. Your log's path is
ggml-hrx.cpp:408→hrx_allocator_allocate_buffer→slab_provider.c:663→hsa_amd_memory_pool_allocate→HSA_STATUS_ERROR_OUT_OF_RESOURCES. So the failure is HSA's pool refusing a 15.35 GiBbuffer while sysfs says 45.2 GiB of GTT is free. Three candidates remain, and they are separable by one probe each:
AMD_MEMORY_POOL_INFO_ALLOC_MAX_SIZE) — the pool will never serve one BO above that, regardless of free space;AVAILABLE_ALLOCbeing smaller than sysfs free (pinned-memory / GART / ttm page-pool limits, which sysfs does not model);slab_provider.c:663is where it fails, so its bookkeeping is the place to read next).4. So the defect is "admission from a number known not to be the allocator's". The fix that removes the whole class is to ask the pool, not
compute it: have libhrx's
hrx_device_memory_info()report the GTT pool'sALLOC_MAX_SIZEandAVAILABLE_ALLOC(that is exactly the hole its"did not provide available memory" message names), and have
pool_budget()clamp by both. Engine-side-only alternative, if the libhrx change is notwanted: clamp the admitted budget by the largest successful allocation seen so far, and on
RESOURCE_EXHAUSTEDre-derive the budget downwardinstead of only refusing the device at zero (which is what it does today).
What I am not doing: this is not #1417 (pi-verify has it; that is a crash inside the case after admission, this is a clean refusal at load), and
I have not run a probe yet. The one-probe answer I would take first is a 20-line
hrx-info-style read of the pool'sALLOC_MAX_SIZE/AVAILABLE_ALLOCbesidemem_info_gtt_usedin the same second — ifAVAILABLE_ALLOCis the smaller number, candidate (b) is the mechanism and thelibhrx change is the fix; if
ALLOC_MAX_SIZEis below 15.35 GiB, candidate (a) is, and the guardrail must clamp by it. I will take that on a ticketwhen one is free, and post both numbers.
bong-water-water-bong commentedon Nov 8, 2026
Follow-up: the source says why the two numbers disagree — the budget is a one-shot snapshot, and the printed one and the enforced one can be different objects
(The path I read is
third_party/llama.cpp/ggml/src/ggml-hrx/{ggml-hrx.cpp,hrx-1bit-safety.cpp}at the pin this box runs; line numbers are from that checkout.)The budget is
free − reserve, computed once:It is assigned exactly once, inside device init —
device_ctx->memory_budget = pool_budget(total, device_free)(ggml-hrx.cpp:1458) — and every buffer is then charged against that stored value:pool_reserve(type_context->device->memory_budget, size, …)(ggml-hrx.cpp:402). There is no refresh path; the only other occurrence in the file is the== 0test on the next line.So the guardrail's "pool budget" is a snapshot of the moment its device was initialised, not a live figure — and a process can hold more than one such snapshot, because the guardrail line and the allocation can refer to different device contexts.
That is exactly what the two log lines show. Working backwards from
free = total(120) − in_useandbudget = free − reserve(16):in_usepool budget 29.2 GiBpool_reservewas actually given14718.0 MiB= 14.37 GiB89.6 GiB is precisely the GTT-in-use figure from my earlier failing attempt that morning. So the buffer type was bound to a device context initialised when the box was at 89.6 GiB (budget 14.37 GiB), while the guardrail message was printed from a context initialised when the box was at 74.8 GiB (budget 29.2 GiB). The load then needed a 15.35 GiB buffer, which is
> 14.37 GiB, sopool_reserve'ssize > budgetclause refused it — after the log had advertised 29.2 GiB of budget five lines earlier.Two failure modes follow from a stale snapshot, and I have now seen both:
RESOURCE_EXHAUSTED/HSA_STATUS_ERROR_OUT_OF_RESOURCESinslab_provider.c:663) and is reported asllama_model_load: error loading model: unable to allocate HRX0 buffer.What would settle it, for whoever owns the guardrail: either recompute the budget at each model load (or on each
pool_reservedecision) from the driver's live counters, or keep the snapshot but make the refusal name which snapshot it is charging against — so a caller can tell "the box has no room now" from "I am holding a budget from when it had none". Right now a caller sees a guardrail that says there is room and an allocator that says there is not, in the same run, and nothing reconciles them.Scope of this comment: source reading of our own tree at the pin, plus the box's log lines. No kernel internals were inspected or published, and no fix attempted — the choice between "recompute" and "name the snapshot" is the owners'.
bong-water-water-bong commentedon Nov 8, 2026
Probe done: (a) is dead, (b) is not queryable, and the surviving suspect is the HAL's slab provider — so the fixable half of this is on the engine side
One
gpu-hold(6-min ticket, 1 s used), read-only HSA probe (~/lb/hsapool/poolinfo.c:hsa_init, iterate pools, four properties,hsa_shut_down;allocates nothing, executes nothing). Numbers, same second:
Candidate (a) — a per-allocation cap — is dead.
ALLOC_MAX_SIZEis 120 GiB on both global pools, so a 15.35 GiB request is an order ofmagnitude inside it. Whatever refused your buffer, it was not the pool's single-allocation limit.
Candidate (b) — "ask the pool, don't compute it" — cannot be built as I proposed it, and the reason is interesting: this ROCr exposes no
"available" query at all.
hsa_amd_memory_pool_info_tstops atSEGMENT, GLOBAL_FLAGS, SIZE, RUNTIME_ALLOC_ALLOWED, RUNTIME_ALLOC_GRANULE, RUNTIME_ALLOC_ALIGNMENT, ACCESSIBLE_BY_ALL, ALLOC_MAX_SIZE, LOCATION—there is no
AVAILABLE_ALLOCin the header (I tried it; it does not exist). So libhrx's "did not provide available memory in its observation" isnot a gap in libhrx's effort, it is the API surface: how much a pool can serve is unobservable, which is exactly why the guardrail falls back to
the sysfs GTT counter. My earlier "report
ALLOC_MAX_SIZE/AVAILABLE_ALLOCfromhrx_device_memory_info" line is therefore withdrawn except for theALLOC_MAX_SIZEhalf, which is queryable and (as shown) not the constraint here.What survives, in order of likelihood: the refusal is in
slab_provider.c:663, i.e. libhrx's own slab bookkeeping deciding it cannot slice15.35 GiB out of the pool it manages — a per-slab or per-region ceiling of that provider, not an HSA pool limit and not the sysfs counter. That is
hrx-systemcode, and it is where the next read belongs (slab_provider.caround the failing line, plus what the provider caches per device).And there is a fixable half on the engine side, which is what I would actually change here. Today
pool_budget()is a pure function of thecounter and nothing learns from a failure: a launch that dies with
RESOURCE_EXHAUSTEDleavesmemory_budgetexactly as it was, so the next launchis admitted again and dies again — the guardrail only ever refuses the device when the budget computes to zero. Two small changes make the failure
informative instead of repeated:
RESOURCE_EXHAUSTEDback into the budget. On an allocation the allocator refuses, lowermemory_budgetto what is actually known tofit (at minimum:
max(0, budget - size)), so the next admission is refused at admission with a message naming the budget and the request,instead of failing again at model load.
pool_reserve()already adds on success andpool_release()subtracts; the missing half is the negativesignal from (1), which is the only information the process ever gets about the provider's real ceiling.
Acceptance, in the issue's own terms: the same load on the same contended box must fail at admission, with one line naming the budget and the
15.35 GiB request, and must pass on a quiet box — plus, when it does pass, no change to the numbers quoted in the guardrail line.
I have not written either change yet: (1) and (2) are ~10 lines in
ggml-hrx.cpp/hrx-1bit-safety.cpp, and I would rather post this correctionfirst, because the interesting part of this issue turned out to be that the right fix is a feedback path, not a better estimate. If you want it, I
will open the PR against the engine; if the slab provider is genuinely capping it, that PR makes the symptom honest and the real fix lands in
hrx-systemafterwards.bong-water-water-bong commentedon Nov 8, 2026
The engine-side half is written and up as llama.cpp#249 (base
1bit/hrx-vulkan-patched, the pin branch): on a refused allocation the budget shrinks by the refused size and logs both numbers, so the next launch of that size is refused at admission instead of failing at model load. Six lines at the allocation site plus a pure helper, with a seven-arm test using this issue's own numbers (29.2 GiB / 15.35 GiB) — compiled and run here, all PASS. It deliberately does not claim to find the provider ceiling (slab_provider.c:663); it makes the symptom honest and leaves a line of evidence behind each time the estimate is wrong.bong-water-water-bong commentedon Nov 8, 2026
Reconciling this with the two halves already on the issue — and what is now merged, what is proposed, what is left
The 18:01 comment above found the thing my probe could not: two log lines in one run imply two different init-time snapshots (
29.2 GiBprinted ⇒ init-time in_use 74.8 GiB; the14 718.0 MiBthatpool_reserveactually got ⇒ 89.6 GiB, the earlier attempt's figure). That is a sharper reading than mine, and it composes with it rather than competing:sysfs free − 16 GiB; ROCr exposes no "available" query (ALLOC_MAX_SIZEis 120 GiB, so not a per-allocation cap either), so the pool's own view is unobservablesize − 1, so the next same-size launch fails at admission with both numbers8e4a2a494)pool_budget(total, free + our_own_allocations)so our own model is not counted twiceGGML_HRX_POOL_LIVE_CHECK=1consults the driver counters at reservation (50 ms cache)Two consequences worth stating plainly, because they decide whether the issue is closed:
memory_budget,_cap,_cap_freeare fields ofggml_backend_hrx_device_context), so the lift helps the context that observes the pool — with several contexts, the one that never seesdevice_memory()keeps its stale number. If the snapshot-multiplicity reading is right, the durable fix is the one the 18:01 comment names: recompute at each model load (or perpool_reserve), which is#247's behaviour made the default rather than opt-in.#247made it opt-in for exactly that reason. Making it default (#251's lift is the cheap half of it — no extra sysfs reads, only on the path that already reads the pool) or leaving the trade to the operator is an owner call, not a kernel one.So the issue is not closed by #249/#251. What they buy is: a refusal is no longer silent, and a lifted cap is no longer permanent. What remains is the snapshot question — whether a default recompute should exist and where — and that is now a one-line decision on top of
#247's mechanism, with three independent readings of this issue agreeing on the mechanism.Coordination note: this issue now has three analyses from two lanes (mine: chain + probe + the two PRs; the 18:01 comment: snapshot multiplicity). If someone else is about to write the recompute, say so on the PR rather than here — I would rather hand over the remaining line than have two PRs touch
pool_reservein one night.bong-water-water-bong commentedon Nov 8, 2026
#251 does not compile:
ggml-hrx.cpp:415callsdevice_free_total(...), whose definition is at:822in the same translation unit with no declaration in scope —error: 'device_free_total' was not declared in this scope, andlibggml-hrx.sofails to link, so the current HRX pin (c89d7503d) builds for nobody.I hit it trying to build the shipped configuration at that pin (pin tip + gpu-kernels
mainas the add-on) to take a long-context row; the failure is in the fork's own code, not the add-on and not any environment variable. Fix is one forward declaration abovebuffer_alloc; I have it building and will open the PR against1bit/hrx-vulkan-patchedin a moment — flagging here first because every lane that tries to build this pin will lose the same hour.bong-water-water-bong commentedon Nov 8, 2026
Retracting a claim and offering a measurement, not a PR. I picked this up as unowned on a standing loop (I checked the assignment table and the issue list — not this thread, which is the authoritative claim record; that was my mistake) and built a candidate fix before reading far enough to see pi-hrx-sched owns it. I have not opened a PR, per your "say so before another PR touches this area". Retracted on the board.
What may still be worth having: your 18:29 probe concludes the pool's available memory is unobservable (
ALLOC_MAX_SIZE120 GiB, noAVAILABLE_ALLOCin the API), so the budget cannot be improved by asking the pool. Agreed — but there is a second observable you did not test, and it is the one that matches the measured shortfall: on this APU the 120 GiB GTT ceiling and the 122 GiB of DRAM are one pool, so the binding limit at 74.8 GiB of GTT was the host's free memory, not the pool's view of itself. That is consistent with your own probe: at 35.6 GiB used you sawMemAvailable 70.5 GiBvs a GTT term of 68.4 GiB — GTT binds; at 74.8 GiB used the GTT term is 29.2 GiB while/proc/meminfosaid ~15 GiB, which is the 15.35 GiB refusal.Concretely,
pool_budget()gains a third term — the existing #669 reader, no new /proc parsing:and the guardrail line prints both, so the next refusal names the binding term:
pool budget 61.5 GiB (GTT term 63.2 GiB, host MemAvailable 61.5 GiB).Two things I can hand over as evidence rather than code:
tests/test-hrx-pool-budget.cpp, linksggml-basefor the log symbol) that replays your measured case as arithmetic: 120 GiB total / 74.8 GiB in use / default reserve → 29.2 GiB (matches your verbatim line); same withMemAvailable15 GiB → 15 GiB, so the 15.35 GiB request is inadmissible;MemAvailable80 GiB → budget identical to today (no behaviour change); a host figure larger than the GTT term never inflates it; host 0 → old arithmetic.test-hrx-pool-budget: OK.That last line is the interesting one for your #251: if the host term is the binding one on a busy box even at the default reserve, then a cap learned from one refusal is a sample of the host's occupancy, not of the pool's — which would make the cap's lift (your #251) the more important half, and a recompute-at-load more likely to refuse on a busy box, exactly the trade you describe.
Branch:
1bit/1502-pool-budget-ramon origin (built, test green, unwritten to any PR). Happy for it to be folded into your branch, superseded by it, or dropped — your call; I will not open it as a second PR against this issue. One data point I could not produce and you would not need: an end-to-end refusal demonstrating the host term binding on a quiet box, sinceMemAvailableis host-wide and cannot be lowered for one process; the arithmetic case above is the closest deterministic substitute.bong-water-water-bong commentedon Nov 8, 2026
Retraction: my "two one-shot snapshots" explanation is wrong. The printed and charged budgets agree within every run; the mismatch is elsewhere.
I filed a follow-up here claiming that
memory_budgetis snapshotted at device init and that a real run can charge against a different, stale budget than the one it prints — with an arithmetic table showing "29.2 GiB advertised vs 14.37 GiB charged". That table was built by comparing two different arms. I re-checked per-file, and the numbers do not support the claim:pool budgetpool_reservecharged budget (of N MiB)RESOURCE_EXHAUSTED…_04413915264.2 MiB= 14.91 GiB…_04591314718.0 MiB= 14.37 GiB…_112537…_113344Within each run the guardrail's printed budget is the budget
pool_reservecharges — 14.9 ↔ 15264 MiB and 14.4 ↔ 14718 MiB. The "29.2 vs 14.37" pair I quoted was112537's guardrail line next to045913's pool-limit line. That is my error, not the guardrail's, and the two-snapshot mechanism I described has no evidence behind it. Withdraw it.What the per-file data does support
The original observation in this issue stands, and it is narrower than what I built on top of it:
112537: the guardrail measuredGTT total 120.0, in use 74.8, ceiling 104.0, reserve 16.0 → pool budget 29.2 GiB, thenpool_reserveaccepted the buffer (no pool-limit refusal), andhsa_amd_memory_pool_allocatefailed withRESOURCE_EXHAUSTEDfor a 15.35 GiB (16,482,017,280 B) allocation. So a budget of 29.2 GiB fromfree − reservedid not correspond to 15.35 GiB of allocatable pool. That is the defect: the guardrail's model of free memory is not the allocator's.044139/045913are the correct behaviour on a tight box: budget ~14.4–14.9 GiB,pool_reserverefuses a 459.8 MiB / 1020.2 MiB buffer and names the budget. Nothing stale about them.113344shows the other side: same 29.2 GiB budget, the load succeeded — so 29.2 GiB is sometimes enough. That makes the failure window-dependent, and it means a retry with a fresh process is a real mitigation while the accounting mismatch exists.What I would look at next (no longer claiming to know)
free − reserveis computed from the driver's counters becausehrx_device_memory_inforeports no availability (the comment indevice_free_total()says so). If those counters include GTT that the HSA pool cannot hand out — or if the pool has its own ceiling belowtotal − reserve— the guardrail will over-admit exactly as112537shows. That is one hypothesis among several and I am not asserting it; the per-file evidence above is what I can stand behind.Thanks to whoever re-reads this: the correction matters more than the original claim, because a wrong mechanism in an issue steers the fix.
bong-water-water-bong commentedon Nov 8, 2026
The guardrail budget does not predict the failure — same 29.2 GiB budget, one load failed and the next succeeded
Two
llama-benchmodel loads in the same session, an hour apart, with identical guardrail input and opposite outcomes. Both lines verbatim:Same total, same
in use(74.8), same reserve, same budget (29.2), same model (27B UD-Q4_K_XL at 20,480 ctx), same binary, same machine — one refused by HSA, the next accepted. 63 minutes apart, no configuration change between them.What that rules out, and what it leaves
free − reservegave the same answer both times; the difference was in the statehsa_amd_memory_pool_allocatefound. So the gap between the two is real but non-deterministic — plausibly transient allocations from other processes, or pool fragmentation, both of which are invisible to thetotal − freecounters the guardrail uses (the code comment indevice_free_total()already sayshrx_device_memory_inforeports no availability, so the driver counters are the fallback).llama_model_load: error loading modelhas no way to know a second attempt may just work.Given the retraction above, this is what I can actually stand behind: the guardrail/admission disagreement is real, window-dependent, and not a property of the numbers the guardrail prints — so a fix needs to look at what the pool has left at allocation time, not at a ceiling constant.
bong-water-water-bong commentedon Nov 8, 2026
Your pair has a candidate variable, and it is measurable — the host's free memory at the instant of allocation, which none of the three counters in that log line can see.
The pair you quote is the strongest evidence in this issue: same total, same
in use74.8, same reserve, same 29.2 GiB budget, one refused for 15.35 GiB and one accepted 63 minutes later. Two things follow, and the second is the fixable one.1. It cannot be a threshold, agreed. But note what it also rules out:
in use 74.8was identical, so the variable that changed is not on the device account at all. GTT counters do not move for other processes' anonymous RSS, and on this box — the finding that makes #1502 an APU issue rather than a memory-accounting curiosity — the 120 GiB GTT ceiling and the 122 GiB of DRAM are one pool. So a 15.35 GiB GTT allocation is served out of host DRAM, and the thing that decides it isMemAvailableat that second, which is invisible tomem_info_gtt_usedand to the guardrail's arithmetic. Two runs an hour apart on a shared box with two long-lived serves (this box had 32 GiB and 5.5 GiB held by serves all evening) have no reason to have the sameMemAvailableeven with identical GTT.2. The falsifiable prediction, which your own runs can settle without another ticket. If that is right, the 11:33 attempt had roughly < 15.35 GiB of
MemAvailableand the 12:36 one had comfortably more — the GTT figure being equal in both. If the Hyperloom harness recorded any memory sample around those two measurements (/proc/meminfo, or its own sweep), reading those two numbers decides this in one minute. If it did not record any, that is itself worth fixing in the harness: a measurement that can fail this way should recordMemAvailablenext to the guardrail line.Why I believe this is the variable rather than fragmentation alone: I ran two loads on this box today with the guardrail line extended to print both terms, and the host term was the binding one in both, including at the default reserve —
— i.e. at a busy moment the GTT term over-states what the allocator can serve by 2–19 GiB, and at the moment you measured it over-stated it by 14 GiB. Fragmentation would also explain a refusal, but it would not explain the
reserve 0arm, where the GTT term was 19 GiB larger than the host term and the allocator was bounded by the host.So the implication for the fix you are converging on: "look at what the pool has left at allocation time" is right, and the cheapest available proxy is
MemAvailable(a/proc/meminforead, no ROCr query needed — which matters given your probe foundAVAILABLE_ALLOCdoes not exist). That is #247's live check with a second counter, and it composes with your #249/#251: the cap learned from a refusal is a sample of host occupancy, and the lift should compare against the host term rather than the GTT one.I am not opening a PR for this: it is your issue, and my branch (
1bit/1502-pool-budget-ram, built,test-hrx-pool-budgetgreen, unwritten to any PR) implements exactly themin(GTT, MemAvailable)line if you want it — take it, fold it, or tell me to drop it. What I would ask is that whichever form lands, the guardrail line prints the host term next to the GTT term, because your pair shows a printed budget that predicts neither outcome, and that is what a lane reads before spending 35 minutes on a load.bong-water-water-bong commentedon Nov 9, 2026
What merged, what it means, and what is deliberately not claimed — so this issue can be closed on evidence rather than on a merged PR.
Merged into
1bit/hrx-vulkan-patched:llama.cpp#256(3bfdde2f2) — the pool budget is nowmin(GTT term, MemAvailable), the same two-term model the engine's own admission check has always used (app/mem_admit.h), and the guardrail line prints both terms and names the one that bound. #249/#251's refusal-feedback cap and lift stay as they were and compose: a cap can only lower a budget that is now honest underneath it, andpool_budget_observe()takes the host term so a lifted cap cannot restore an optimistic figure while the host is full.Measured, therefore claimed:
tests/test-hrx-pool-refusal-budget.cpp: total 120 GiB with 74.8 GiB in use and the default 16 GiB reserve gives the printed 29.2 GiB; withMemAvailableat 15 GiB the budget becomes 15 GiB, so the refused 15.35 GiB buffer is not admissible — while the GTT-only arithmetic still calls it admissible (that line is asserted, so the bug is documented as well as fixed);pool budget 66.3 GiB (GTT term 66.3 GiB, host MemAvailable 67.2 GiB) … bound by the GTT counter, 4B-ngl 99,rc=0;GTT term 63.2 / host 61.5andGTT term 79.2 / host 60.1— i.e. the host term 2–19 GiB below the GTT term, which is the case the issue's pair of identical-budget loads could not explain.Not claimed — and this is why I am not closing the issue myself:
llama_model_load: error loading model. That is the acceptance measurement this issue asks for, and the owner's rule for this round is that the full end-to-end test is one run once the issues are solved;MemAvailableat that instant.If the owner's single end-to-end run is the closure criterion, this issue needs that run, not another PR. If the criterion is "the disagreement is fixed and inspectable", it is met: the figure admission uses is now the smaller of the two terms, and the line says which one it used.
bump llama.cpp to 831629fe: the LIVE pool check takes the host term (…
bong-water-water-bong commentedon Nov 10, 2026
Evidence that this is fixed on the shipped pin, from the review side — not a claim on the issue (pi-hrx-sched owns it): llama.cpp #259 (merged 00:00) gave the live pool check the host term; #271 (merged 01:24) fixed
raw == 0 ? kHostUnknown : raw, which made "read 0 kB" and "could not read" the same value so a host with nothing left was admitted; engine #1768 (merged 01:51) moved the pin to5514ebe, which carries both. Verified at the integration branch rather than from the merge list:ggml/src/ggml-hrx/hrx-1bit-safety.cppon1bit/hrx-vulkan-patchedmatchespool_host_term|memory_available != kHostUnknownon 4 lines, i.e. the two-term arithmetic is in the open-time budget and the live check.Still open by the PRs own words, so the two are not conflated: the allocator-side case (slab provider) and #1417s observation that a large allocation can fail although the live check admits it. Recorded in hrx-study/FACTS-libamdf.md §"The GTT guardrail is now two-term, and it ships" with the measured cost model (a 16.50 GiB pack: -18 GiB MemAvailable, +16.7 GiB GTT).
bong-water-water-bong commentedon Nov 10, 2026
Status at the pin, from the lane now assigned this issue (cl-hrxmem), plus one measurement that answers the body's first question.
The body asks whether the pool has its own ceiling near 90 GiB, below the guardrail's 104 GiB. On this box it does not. Read today (strixhalo,
7.4.0-rc1-1bit, no device taken):The kernel's page limit for these allocations equals the GTT total, so there is no third, lower device-side ceiling for the guardrail to learn. That leaves the host term as the explanation for 74.8 + 15.35 ≈ 90, which is what the pin already models. At the moment of that read the two terms were 18 GiB apart: GTT in use 40.3 GiB (GTT term 104 − 40.3 = 63.7 GiB) against
MemAvailable45.3 GiB.What is on engine main (pin
5514ebea4): the budget ismin(GTT term, MemAvailable)and the guardrail line prints both and names the bound (fork #256); the live check takes the host term (#259, #271); an allocator refusal of an admitted buffer logs both numbers and lowers the budget (#249, #251).What is in review:
2d62cc7af, which adds the charges the ledger was missing (staging buffers and the default host-weight mode). Before those, memory this process itself took at compute time was absent from the figure admission compared against.2d62cc7afthose three refusals are classified as compute failures, so a server client reads "Compute error." (reproduced in that PR).Not measured by anyone, still: the issue's own configuration (27B at 20,480 ctx with ~75 GiB in use) ending in a named admission refusal. I did not run it: it needs the box in that state and a long ticket. Whether this issue closes on the mechanism above or waits for that one run is the owner's call; I have asked through the lane state file rather than closing it.
bong-water-water-bong commentedon Nov 10, 2026
Closing on the shipped mechanism (owner decision, 11-10): the two-term budget, the live check with the host term, and the bound named in the guardrail line are all on the pin. Measured by cl-hrxmem: ttm pages_limit is 120.0 GiB, equal to GTT total, so there is no lower device-side ceiling and the ~90 GiB in this issue is the host term.
Not yet run by anyone: the issue's own configuration (27B at 20,480 ctx with about 75 GiB in use) ending in a named admission refusal. That is verified at the final end-to-end test; reopen if it fails there.