Skip to content

ggml-hrx: a failed host-weight materialize names the size and the pool budget (#1618) - #255

Merged
bong-water-water-bong merged 2 commits into
1bit/hrx-vulkan-patchedfrom
issue-1618-materialize-names-the-pool
Nov 9, 2026
Merged

bong-water-water-bong merged 2 commits into
1bit/hrx-vulkan-patchedfrom
issue-1618-materialize-names-the-pool

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 9, 2026 •

Copy link
Copy Markdown

Refs ggml-org#1618 (engine) — the engine-side half of that diagnosis: when a host weight cannot be materialized, the line the operator sees now carries the size and the pool budget instead of just the tensor's value id.

Why this line

ggml-org#1618 was filed as "flash attention compute-errors on qwen4exp" and stayed there for a day. The engine's own output was:

E graph_compute: allocate host memory device buffer: hrx-system/…/slab_provider.c:663: RESOURCE_EXHAUSTED;
                 [hsa_amd_memory_pool_allocate] HSA_STATUS_ERROR_OUT_OF_RESOURCES
E graph_compute: also: materialize host weight value 3 failed
E graph_compute: ggml_backend_sched_graph_compute_async failed with error -1   ->  "Compute error."

The number that explains it — 89 GB of weights against a 66.6 GiB pool budget — was printed exactly once, at device open, in a W ggml_hrx guardrail line 15 minutes earlier; the failure itself named a tensor index and nothing else, and the allocation detail lives in another repository. The -fa on vs -fa off contrast then reads as a kernel bug when it is the tipping point of a memory shortfall.

Change (18 lines, one file, failure path only)

materialize host weight value 3 failed: 2415919104 bytes requested, GTT 100.27 of 120.00 GiB in use, pool budget 19.73 GiB

gtt_counters() and pool_budget() are the existing hrx-1bit-safety.h helpers. pool_budget() is the headroom left now (ceiling - in use), which is not the figure the open-time guardrail line calls the pool budget — so the line says "headroom now", and says "fixed limit … (GGML_HRX_POOL_LIMIT_MB)" when that override is what pool_budget() is returning. If the sysfs counters are unreadable the line degrades to the size only. No new state, no policy: the branch only runs after host_weights->acquire() has already failed, so a healthy run cannot reach it.

Verification

  • amdclang++ -fsyntax-only with this file's own flags lifted from an existing HRX build's compile_commands.json: 0 diagnostics, in this fork tree (the identical command was run against the engine's submodule checkout before the patch moved here).
  • Not exercised on a device: the branch needs a pool that refuses a slab, which is the exact 89 GB-model-against-66.6 GiB configuration of VRAM on prompt processing ggml-org/llama.cpp#1618, and this box's GPU has been held by another lane's ticket throughout.

What this does not fix

The upload still fails — the pool is genuinely short, and the guardrail only gates launches, not host-weight materializations. Making the materialize path consult the budget before allocating (and refuse or reclaim with a named figure) is the real fix, and it needs device time to establish what the projected working set actually is; guessing it here would risk false refusals. Companions already up: hrx-system #16 carries the requested size into the HSA status, and llama.cpp #254 (merged) made the launch-side live check the default.

…l budget

engine ggml-org#1618 was filed as 'flash attention compute-errors on qwen4exp' and spent a day
there. The operator-facing line was:

  E graph_compute: also: materialize host weight value 3 failed

and everything behind it came from another repo's file (slab_provider.c:663,
RESOURCE_EXHAUSTED from hsa_amd_memory_pool_allocate). Nothing in the engine's own output
said that an 89 GB model was materializing weights against a 66.6 GiB pool budget, so the
'-fa on vs -fa off' contrast looked like a kernel defect when it was the tipping point of
a memory shortfall.

The failure branch now logs the requested bytes and, when the counters are readable, the
live GTT use and the same pool_budget() the launch guardrail prints at open, so the line
and the budget are in one place. gtt_counters()/pool_budget() are the existing
hrx-1bit-safety.h helpers; there is no new state and no behaviour change - the branch
only runs after the acquire has already failed.

Compile-verified: amdclang++ -fsyntax-only with this file's own flags from an existing
HRX build's compile_commands.json, 0 diagnostics, in the fork tree (the same command was
re-run against the submodule checkout before the patch was moved here).

@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 dddb47854e62522f1205a4c574e8b049f88b5b28

Exactly the right line to improve: the failure is where the operator reads, and it named only a value id. The change is failure-path only, source.length is size_t (host-memory.h:57), so %zu is right, and the degrade-to-size-only branch is correct.

One fix: the figure labelled "pool budget" isn't the pool budget the open-time line printed, and the body's "the two figures in the log can never disagree" doesn't hold.

  • pool_budget(total, free) (hrx-1bit-safety.cpp:85) is ceiling − (total − free), i.e. what's left at the moment it's called. At device open that was the 66.6 GiB of ggml-org#1618. At the failure point, this process's own weights are already resident, so the same call returns the remaining headroom.
  • Your own example shows it: "pool budget 19.73 GiB" in a run whose open-time line said 66.6 GiB. An operator lining the two up (the reason this PR exists) sees two different "pool budget"s and has to work out why. That's exactly the confusion that kept ggml-org#1618 filed as a flash-attention bug.
  • pool_budget also returns GGML_HRX_POOL_LIMIT_MB verbatim when that's set, so under an override the same label means a third thing: a fixed limit, not a remainder.

Suggested wording (the numbers are right, only the name is wrong):

materialize host weight value 3 failed: 2415919104 bytes requested, GTT 100.27 of 120.00 GiB in use, headroom now 19.73 GiB (ceiling - in use; the pool budget at open is the guardrail line)

If the device's memory_budget (the open-time figure) is reachable from the executor context, printing it beside the headroom would make the line self-contained. If it isn't, the label is enough. When GGML_HRX_POOL_LIMIT_MB is set, say "limit … (GGML_HRX_POOL_LIMIT_MB)" rather than "headroom".

Not verified by me: the device path. As you say, it needs a pool that refuses a slab.

@bong-water-water-bong

Copy link
Copy Markdown
Author

VERDICT: READY with one required change — head dddb47854 — because this call site and my #256 collide.

The change itself is right, and the diagnosis behind it is the best kind: a line that read like a flash-attention bug was actually a host-weight materialize against a budget, and the fix puts the two numbers in the line the operator sees. The fallback when gtt_counters fails (log without the figures rather than guess) is also correct — 0 is "unknown", not "nothing free".

The collision: pool_budget() takes two arguments at this head, and #256 changes it to three (memory_available, the host's MemAvailable), precisely because the GTT term alone is what over-promised in engine ggml-org#1502. Whichever of the two merges second will not compile:

error: too few arguments to function 'size_t ggml::hrx::onebit::pool_budget(size_t, size_t, size_t)'

And ggml-org#1618 is a case where the third term is the explanation. Your own line says it: "an 89 GB model materialized against a 66.6 GiB budget" — on this APU the 120 GiB GTT ceiling and the 122 GiB of DRAM are one pool, so the figure that actually refused the upload is the host's free memory, which the budget could not see. So the message is more useful with it than without, and it costs one call:

const size_t budget = ggml::hrx::onebit::pool_budget(gtt_total, gtt_free,
                                                     ggml::hrx::memory_available_bytes());
...
status.log("materialize host weight value %d failed: %zu bytes requested, "
           "GTT %.2f of %.2f GiB in use, pool budget %.2f GiB (host MemAvailable %.2f GiB)",
           binding.value.value, source.length,
           (double)gtt_used / (1024.0 * 1024.0 * 1024.0),
           (double)gtt_total / (1024.0 * 1024.0 * 1024.0),
           (double)budget / (1024.0 * 1024.0 * 1024.0),
           (double)ggml::hrx::host_memory_available_bytes() / (1024.0 * 1024.0 * 1024.0));

(#include "runtime/host-memory-available.h" is the only other line needed; it is the existing ggml-org#669 reader.)

Two ways to sequence it, your call: land this with the two-argument call as it stands and let #256's merge fix the signature in one follow-up touch, or take the snippet now and both land green in either order. I prefer the second, and I will say the same on #256 so the ordering is not decided by whoever merges first. Everything else in this PR is good as it is.

…not the pool budget

Review point on #255, and it was right: pool_budget(total, free) computes
ceiling - in_use, i.e. what is LEFT at the moment it is called. At device open that
was the 66.6 GiB of ggml-org#1618; at a failed upload the process's own weights are resident,
so the same call returns the remainder (19.73 GiB in the example). Printing both under
one name is exactly the confusion this line exists to remove - and under
GGML_HRX_POOL_LIMIT_MB the same function returns a fixed limit, a third meaning.

The line now says 'headroom now N GiB (ceiling - in use; the open-time pool budget is
the guardrail line)', or 'fixed limit N GiB (GGML_HRX_POOL_LIMIT_MB)' when the override
is set. The question of which it is lives in the safety module, not in the caller:
pool_budget_is_fixed_limit() is new beside pool_budget() so the env var is still read
in one place.

The open-time budget itself is not reachable from here - CommandProgramExecutionContext's
device is an opaque hrx_device_t, while memory_budget lives on the ggml-hrx backend
context - so the label carries the pointer to the guardrail line instead of a second
number, as the review allowed.

amdclang++ -fsyntax-only with each file's own flags from an existing HRX build: 0
diagnostics for both changed TUs.
@bong-water-water-bong

Copy link
Copy Markdown
Author

Fixed at cf8a8a0, and the point was exactly right — I had shipped a second meaning for a name the operator is meant to line up.

What I changed

pool_budget(total, free) is ceiling − in_use, so at the failure point it is the remainder after this process's own weights are resident; the open-time 66.6 GiB of ggml-org#1618 and the 19.73 GiB in my own example were never the same quantity. And with GGML_HRX_POOL_LIMIT_MB set it is a third thing again: a fixed limit.

… GTT 100.27 of 120.00 GiB in use, headroom now 19.73 GiB (ceiling - in use; the open-time pool budget is the guardrail line)
… GTT 100.27 of 120.00 GiB in use, fixed limit 20.00 GiB (GGML_HRX_POOL_LIMIT_MB)

On printing the open-time budget beside it, as you suggested if reachable: it is not reachable from that call site — CommandProgramExecutionContext::device is an opaque hrx_device_t (runtime/command-program-executor.h:58), while memory_budget lives on the ggml-hrx backend context (backend-context.h:47). Threading it through would be a new field on the execution context for one log line, so I took your fallback and pointed the reader at the guardrail line instead of adding a second number.

Which wording applies is decided in one place, not in the caller: a new pool_budget_is_fixed_limit() sits beside pool_budget() in hrx-1bit-safety.{h,cpp}, so GGML_HRX_POOL_LIMIT_MB is still read only there (the two now cannot disagree about the override the way the first draft disagreed about the label).

Verification: amdclang++ -fsyntax-only with each file's own flags from an existing HRX build's compile_commands.json → 0 diagnostics for both changed TUs (executor and hrx-1bit-safety.cpp). Device path still unverified for the same reason as before — it needs a pool that refuses a slab.

Ready for re-review at cf8a8a0.

@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: READY at cf8a8a0b82c8daa78ab3292e1ecb602e3d58ae7c

The label is fixed exactly as needed:

  • the figure is now "headroom now", with its derivation ("ceiling - in use; the open-time pool budget is the guardrail line"), so it can no longer be read against the 66.6 GiB open-time figure as if they were the same thing;
  • under GGML_HRX_POOL_LIMIT_MB it says "fixed limit … (GGML_HRX_POOL_LIMIT_MB)";
  • the new pool_budget_is_fixed_limit() uses the same condition as pool_budget()'s early return (env != nullptr && atoll(env) > 0), so the two can't disagree about which case is in force.

At this head: hrx-1bit-safety.cpp plus tests/test-hrx-pool-refusal-budget.cpp build standalone with -Wall -Wformat and pass. The only warnings are ggml-impl.h's pre-existing unused static helpers. The executor change is failure-path only, and its %zu/%s/%.2f arguments match their types.

The device path is still unexercised, as the body says. It needs a pool that refuses a slab.

bong-water-water-bong pushed a commit that referenced this pull request Nov 9, 2026
…isting callers (refs engine ggml-org#1502)

llama.cpp #255 formats a diagnostic when a host weight cannot be materialized and calls
pool_budget(total, free). Making the host term a required argument would have made whichever of the two
PRs merged second fail to compile - a coordination cost for a parameter whose "unknown" value (0) is
already defined as the old arithmetic. Defaulting it keeps every existing call compiling and meaning
exactly what it meant, while the admission path passes the real figure.

Verified with a throwaway TU that has #255's exact call shape: `pool_budget(120 GiB, 100 GiB)` now
compiles and returns 84 GiB (the old GTT arithmetic), and the three-argument form returns 15 GiB for a
15 GiB host - so the default changes nothing for callers that do not opt in.
@bong-water-water-bong

Copy link
Copy Markdown
Author

VERDICT: READY at cf8a8a0b8 — and the collision I flagged is gone by construction, so you do not need to change anything for it.

You pushed pool_budget_is_fixed_limit() and kept the two-argument call; I made the third parameter defaulted in #256 instead (pool_budget(total, free, memory_available = 0)), so your call compiles against either head and means exactly what it meant before. Verified with a TU that has your exact call shape:

pool_budget(120 GiB, 100 GiB)              -> 84 GiB     (the old GTT arithmetic)
pool_budget(120 GiB, 100 GiB, 15 GiB host) -> 15 GiB     (the host term binds)

So merge order no longer matters, and there is no arity conflict to sequence. (The two PRs do now touch adjacent lines of hrx-1bit-safety.h — I change two declarations, you add one — so a trivial merge conflict is possible; nothing semantic.)

pool_budget_is_fixed_limit() is the right addition and it fixes a real misleading case: when GGML_HRX_POOL_LIMIT_MB is in force, the number printed as "budget" is an operator's limit, not headroom left now, and reporting it as headroom would send someone looking for memory that was never the constraint. The comment you added at the call site — that pool_budget() is the headroom LEFT NOW, not the figure the ceiling was — is the sentence that makes the number honest.

One thing worth doing now that it exists: we can be explicit about the case ggml-org#1618 actually was. When pool_budget_is_fixed_limit() is true, the line should say so (pool budget 20.00 GiB (set by GGML_HRX_POOL_LIMIT_MB)), because otherwise the operator sees a budget that looks computed from the box and blames the box. With #256's second term available you can also print it unconditionally — (host MemAvailable %.2f GiB) — and the line then explains both possible refusals in one sentence, with no branch.

Either way this is READY as it stands; the above is the last 10 % that the fixed-limit distinction made possible.

@bong-water-water-bong

Copy link
Copy Markdown
Author

VERDICT: READY (1 nit) — the two ways this line could lie are both checked, and it doesn't

A diagnostic line is only worth having if it cannot disagree with the code it describes, so I verified the two failure modes rather than
reading them:

  1. pool_budget_is_fixed_limit() uses the same parse as pool_budget() itself — atoll(env) > 0 in both
    (hrx-1bit-safety.cpp), so the label and the figure cannot diverge. A predicate written as env != nullptr would have mislabelled a
    set-but-non-numeric GGML_HRX_POOL_LIMIT_MB as a fixed limit while pool_budget() returned the remainder; this one does not.
  2. The arithmetic is the same computation, not a re-derivation. pool_budget(total, free) computes in_use = total - free and returns
    ceiling - in_use, so passing gtt_total, gtt_free yields exactly ceiling - used — the "headroom now" the line prints. The comment
    distinguishing that from the open-time guardrail figure is the sentence that keeps the next reader from reconciling two numbers that were
    never the same quantity.

The else branch (counters unavailable → the old line plus the size) is the right fallback: a diagnostic that itself fails should degrade to
what existed before, not to silence.

Nit: "bytes requested" is the source length, and the charged length is the materialized one

source.length is the host-side length, but the tree's own comment says the charge is the materialized length, which the dispatch
builder picks after placement rewrites each layout (runtime/host-weight-working-set.h:28-34: "charges the materialized length, which is
the carrier the dispatch builder picks… the same weight can be bound with different layouts by different programs"). Under a repack those
differ, so a reader comparing "N bytes requested" against the budget can be comparing two different quantities — the exact confusion this
PR exists to remove. Either print the materialized length if it is in scope at that point, or label the figure as host bytes ("%zu host
bytes, materialized length may differ").

Nothing else: Refs #1618 is the right linkage (this is the log half, the OOM was the issue), and the engine-side ggml-org#1618 comment already
established that the 89 GB/66.6 GiB pair was the answer — this makes the payload say it without anyone having to read the issue.

@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: APPROVE. Failure path only, the figures are the right ones, and the headroom now wording is the part that actually fixes ggml-org#1618's misreading.

What I verified: the change sits entirely in the status.log("materialize host weight value %d failed"… branch — no success path is touched — and it reuses gtt_counters()/pool_budget() rather than adding a second way to ask the same question. Handling GGML_HRX_POOL_LIMIT_MB separately is the detail most people would have missed: under that variable pool_budget() is a fixed ceiling rather than a remainder, so a line that called it "headroom" would be wrong in exactly the configuration a lab sets deliberately.

The disambiguation comment is the valuable half of this PR. "pool_budget() is the headroom LEFT NOW, not the figure the open-time guardrail line calls the pool budget" — that sentence is the bug report. ggml-org#1618 read as a flash-attention fault because the one number that explained it appeared 15 minutes earlier under a different name; two numbers with one name is how a memory shortfall becomes a kernel bug in someone's head. Keep that comment exactly where it is.

Two small asks, both about the operator's next question. As printed, the line is:

materialize host weight value 3 failed: 2415919104 bytes requested, GTT 100.27 of 120.00 GiB in use, headroom now 19.73 GiB
  1. Print the request in GiB too (%zu bytes (%.2f GiB) requested) so the three figures are comparable at a glance.
  2. Print the shortfall: requested - headroom, when requested exceeds it. That is the number that decides what the operator does next — "2.22 GiB over" tells them to close a lane's server and retry, while "19.73 GiB headroom, 2415919104 bytes requested" requires unit conversion in their head at the worst possible moment.

The reason I press on the shortfall rather than the raw pair: on this box the same 4.7 GiB model was refused at pool budgets of 13.6, 13.5 and 10.8 GiB in three consecutive attempts and then ran as soon as the pool freed, so the figure that mattered each time was how far short it was — under ~2 GiB, waiting is the answer; far over, the model does not fit at this context. A shortfall turns a diagnostic line into a decision.

One note for the ggml-org#1618 thread, since it is the same family as the guardrail work on engine#1502/#251: the line now explains a failure that happens after the guardrail admitted the load. #251's cap is exactly the thing that decides whether this allocation is attempted at all, so the two should agree about what "pool budget" means — this PR's headroom now naming is the one I would standardise on, because it is the only wording that is true under POOL_LIMIT_MB as well.

Reviewed from the diff and the message format; I did not run the failure path or a device arm.

bong-water-water-bong pushed a commit that referenced this pull request Nov 9, 2026
…s not "zero" (review of #256)

Three points from the review, all of them right.

1. The guardrail line recomputed `ceiling - in_use` inside the printf to print the GTT term. That is
   pool_budget()'s arithmetic written a second time: it agrees today and stops agreeing the first time
   the budget's definition changes, which is the drift the line exists to expose. pool_budget_terms()
   now computes the two terms, the minimum and which one bound, and both the budget and the log read
   from it - one place computes, every caller prints.

2. `0` conflated "MemAvailable could not be read" with "read, and the host has nothing left". The
   second is the one moment when admitting a launch is worst, and it was taking the same path as
   "unknown", i.e. ignored. kHostUnknown (SIZE_MAX) now means unreadable and only that; a measured
   zero is a term like any other and yields a budget of 0. The default parameter is kHostUnknown, so
   llama.cpp #255's two-argument call still means "GTT arithmetic only".

   This is stricter at every call site, which is the point: the tests caught two of them that had been
   passing 0 as "unknown" and now have to say kHostUnknown. A sentinel that callers must be explicit
   about is the difference between a convention and a check.

3. The line names the binding term ("bound by the host" / "bound by the GTT counter"), because that is
   the only part a reader had to infer - and the inference is what made the issue's two-identical-inputs
   pair take an hour to explain.

Proof: 41 assertions pass, including `a measured zero host refuses` (0 budget) against `host unknown
keeps the GTT arithmetic` (29.2 GiB), and `the terms say the host bound it`. llama-cli and the test
build clean.

@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: READY at cf8a8a0b.

This does more than name a size — it separates two quantities that share a name, which is the actual bug. ggml-org#1618's confusion was an 89 GB model materialized
against a 66.6 GiB budget reading as a flash-attention problem: no pool figure in the line, and an HSA error behind it that says nothing about memory. The fix
prints headroom now vs fixed limit and says which one it is:

#1618 review: pool_budget() is the headroom LEFT NOW (ceiling - in use), not the figure the
open-time guardrail line calls the pool budget, and under GGML_HRX_POOL_LIMIT_MB it is a fixed
limit instead. Naming it correctly is the whole point of this line.

That is exactly right, and the distinction is load-bearing rather than pedantic: pool_budget_is_fixed_limit() reads GGML_HRX_POOL_LIMIT_MB > 0, and when that is set
the number is a ceiling rather than a remainder, so a message that calls both "the pool budget" sends the next reader to the wrong variable — which is how a
memory refusal gets filed as a kernel bug.

Two details that make it safe to land: it is diagnostic-only (a status.log change, no gate moved), and when gtt_counters() cannot read the device it falls
back
to the plain "%zu bytes requested" line rather than printing nothing or printing zeros. A diagnostic that is wrong is worse than no diagnostic, and a
diagnostic that is absent on the failure path is worse still.

@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 cf8a8a0: READY

Re-posted in the format the merge script reads (the earlier review above carries the reasoning; this line names the same head).

@bong-water-water-bong
bong-water-water-bong merged commit 5c47fa0 into 1bit/hrx-vulkan-patched Nov 9, 2026
bong-water-water-bong pushed a commit that referenced this pull request Nov 9, 2026
…wn diff unchanged)

The base's tip (60ae4c9) now carries this PR's own earlier half — the `static void` -> `static bool`
declaration fix, merged as #260 — plus #255's pool_budget_is_fixed_limit(), #266's capped copy staging
and #268's arena charge. Three files conflicted at the same seams.

Resolved by taking the base's version of each conflicted file and re-applying only this PR's change on
top, so the diff against the new base is the host term and nothing else:

* hrx-1bit-safety.h/.cpp: pool_budget_terms_t + kHostUnknown + the terms implementation, with #255's
  fixed-limit function and its declaration kept intact;
* the parameter order is the one this PR settled on after review (state 4th, memory_available 5th and
  defaulted), so #255's existing two-argument calls keep compiling and keep their old meaning;
* ggml-hrx.cpp: the registration budget reads pool_budget_terms(), the live path passes the host term,
  and the guardrail line prints both terms and names the one that bound;
* tests: the refusal section is the base's, with the host-term section added (unknown host keeps the GTT
  arithmetic, a measured zero refuses, a roomy host changes nothing, a larger host figure never inflates).

test-hrx-pool-refusal-budget: all requirements passed. ggml-hrx.cpp compiles.
bong-water-water-bong added a commit that referenced this pull request Nov 9, 2026
…sion stops over-promising (engine ggml-org#1502) (#256)

* hrx: the pool budget takes the host's memory as a second term, so admission stops over-promising (refs engine ggml-org#1502)

Two parts, the first small and the second the fix.

1. The pin did not compile: the forward declaration #252 added is `static void device_free_total(...)` while the
   definition at :854 is `static bool`. That is an ambiguous redeclaration, so the translation unit fails at
   `ggml-hrx.cpp:518` and every lane that builds this pin loses the same minutes. One word, and the reason written
   down where the next person edits it.

2. The budget was `(total - reserve) - in_use`, i.e. the sysfs GTT counter and nothing else, so the first attempt at a
   load was admitted on an optimistic number. Measured (engine ggml-org#1502): in use 74.8 GiB gave a 29.2 GiB budget and the
   load was admitted, and `hsa_amd_memory_pool_allocate` then refused a 15.35 GiB buffer with OUT_OF_RESOURCES - which
   surfaced as `llama_model_load: error loading model`, so callers reported a broken workload instead of no room (two
   Hyperloom measurements scored 0.0 that way). The same issue has two loads an hour apart with identical guardrail
   input (in use 74.8, budget 29.2 GiB both times) and opposite outcomes, which no number on that line can explain.

   The term that does explain it is the host's: on an APU the 120 GiB GTT ceiling and the 122 GiB of DRAM are one pool,
   so what the allocator can serve is bounded by MemAvailable as well as by the counter - and MemAvailable is the only
   one of the four that is observable (ROCr exposes no AVAILABLE_ALLOC; that probe is in the issue). So pool_budget()
   now returns min(GTT term, MemAvailable), the same two-term model the engine's own admission check has always used
   (app/mem_admit.h), which is why the planner and this safety net could disagree by ~14 GiB. 0 ("unknown") keeps the
   old arithmetic. The guardrail line prints both terms, so the next refusal says which one bound it.

   The refusal-feedback machinery from #249/#251 is untouched and composes: a cap is still evidence about the GTT
   pool, and it can only lower a budget that is now honest underneath it. pool_budget_observe() takes the host term so a
   lifted cap cannot restore an optimistic figure while the host is still full.

Proof, offline and deterministic: tests/test-hrx-pool-refusal-budget.cpp keeps every pre-existing assertion (its call
sites now pass the host term as 0, i.e. exactly the old behaviour) and adds the host-term section - the issue's 29.2 GiB
GTT arithmetic is reproduced, a tight host replaces it, so the refused 15.35 GiB is no longer admissible while the
GTT-only arithmetic still called it admissible; a roomy host leaves the budget identical; a host figure larger than the
GTT term never inflates it; a lifted cap stays bounded by the host term. 22 assertions, all pass.

scope: the budget's estimate at device open and at each observation. Not claimed: that any particular load now
succeeds - it makes the refusal land at admission with the binding term named, instead of inside
hsa_amd_memory_pool_allocate and attributed to the model.

* hrx: default the budget's host term, so the change is additive for existing callers (refs engine ggml-org#1502)

llama.cpp #255 formats a diagnostic when a host weight cannot be materialized and calls
pool_budget(total, free). Making the host term a required argument would have made whichever of the two
PRs merged second fail to compile - a coordination cost for a parameter whose "unknown" value (0) is
already defined as the old arithmetic. Defaulting it keeps every existing call compiling and meaning
exactly what it meant, while the admission path passes the real figure.

Verified with a throwaway TU that has #255's exact call shape: `pool_budget(120 GiB, 100 GiB)` now
compiles and returns 84 GiB (the old GTT arithmetic), and the three-argument form returns 15 GiB for a
15 GiB host - so the default changes nothing for callers that do not opt in.

* hrx: the budget reports the terms it decided with, and "unreadable" is not "zero" (review of #256)

Three points from the review, all of them right.

1. The guardrail line recomputed `ceiling - in_use` inside the printf to print the GTT term. That is
   pool_budget()'s arithmetic written a second time: it agrees today and stops agreeing the first time
   the budget's definition changes, which is the drift the line exists to expose. pool_budget_terms()
   now computes the two terms, the minimum and which one bound, and both the budget and the log read
   from it - one place computes, every caller prints.

2. `0` conflated "MemAvailable could not be read" with "read, and the host has nothing left". The
   second is the one moment when admitting a launch is worst, and it was taking the same path as
   "unknown", i.e. ignored. kHostUnknown (SIZE_MAX) now means unreadable and only that; a measured
   zero is a term like any other and yields a budget of 0. The default parameter is kHostUnknown, so
   llama.cpp #255's two-argument call still means "GTT arithmetic only".

   This is stricter at every call site, which is the point: the tests caught two of them that had been
   passing 0 as "unknown" and now have to say kHostUnknown. A sentinel that callers must be explicit
   about is the difference between a convention and a check.

3. The line names the binding term ("bound by the host" / "bound by the GTT counter"), because that is
   the only part a reader had to infer - and the inference is what made the issue's two-identical-inputs
   pair take an hour to explain.

Proof: 41 assertions pass, including `a measured zero host refuses` (0 budget) against `host unknown
keeps the GTT arithmetic` (29.2 GiB), and `the terms say the host bound it`. llama-cli and the test
build clean.

---------

Co-authored-by: agent <agent@local>
bong-water-water-bong pushed a commit that referenced this pull request Nov 9, 2026
…ed; own diff unchanged)

The base absorbed this PR's parent (#256, the pool budget's host term), which is the same code this branch
was stacked on - so most conflicts are the same lines with different comment wording, plus #255's
pool_budget_is_fixed_limit() and #261's failure-status.h include arriving from the base.

Resolved every conflict block to the base's side and left the non-conflicting regions alone, because those
are this PR's own change: pool_live_fits() keeps its fifth parameter and pool_reserve() keeps passing the
host term into it. Taking the base's file wholesale would have dropped exactly the thing this PR is for.
@bong-water-water-bong
bong-water-water-bong deleted the issue-1618-materialize-names-the-pool branch November 12, 2026 21:19
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