Repository navigation
ggml-hrx: a failed host-weight materialize names the size and the pool budget (#1618) - #255
Conversation
…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
left a comment
There was a problem hiding this comment.
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) isceiling − (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_budgetalso returnsGGML_HRX_POOL_LIMIT_MBverbatim 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.
|
VERDICT: READY with one required change — head 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 The collision: 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));( 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.
|
Fixed at What I changed
On printing the open-time budget beside it, as you suggested if reachable: it is not reachable from that call site — Which wording applies is decided in one place, not in the caller: a new Verification: Ready for re-review at |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
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_MBit says "fixed limit … (GGML_HRX_POOL_LIMIT_MB)"; - the new
pool_budget_is_fixed_limit()uses the same condition aspool_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.
…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.
|
VERDICT: READY at You pushed So merge order no longer matters, and there is no arity conflict to sequence. (The two PRs do now touch adjacent lines of
One thing worth doing now that it exists: we can be explicit about the case ggml-org#1618 actually was. When Either way this is READY as it stands; the above is the last 10 % that the fixed-limit distinction made possible. |
VERDICT: READY (1 nit) — the two ways this line could lie are both checked, and it doesn'tA diagnostic line is only worth having if it cannot disagree with the code it describes, so I verified the two failure modes rather than
The Nit: "bytes requested" is the source length, and the charged length is the materialized one
Nothing else: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
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
- Print the request in GiB too (
%zu bytes (%.2f GiB) requested) so the three figures are comparable at a glance. - 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.
…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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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).
…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.
…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>
…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.
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:
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_hrxguardrail line 15 minutes earlier; the failure itself named a tensor index and nothing else, and the allocation detail lives in another repository. The-fa onvs-fa offcontrast then reads as a kernel bug when it is the tipping point of a memory shortfall.Change (18 lines, one file, failure path only)
gtt_counters()andpool_budget()are the existinghrx-1bit-safety.hhelpers.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 whatpool_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 afterhost_weights->acquire()has already failed, so a healthy run cannot reach it.Verification
amdclang++ -fsyntax-onlywith this file's own flags lifted from an existing HRX build'scompile_commands.json: 0 diagnostics, in this fork tree (the identical command was run against the engine's submodule checkout before the patch moved here).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.