Repository navigation
hrx: the pool budget takes the host memory as a second term, so admission stops over-promising (engine #1502) - #256
Conversation
…ission 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.
|
Coordination note (mine, so no review needed): #255 calls
If #255 lands first, the one-line follow-up here is confined to that call site; if this lands first, #255 needs the same one line. Either way the fix is one argument, not a rebase. |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at b314d767e1 — the two-term budget is right and the compile fix is right (it is my bug), but the live check now decides admission by default and it
still uses the GTT term alone, so the issue's own failure can still get through.
First, accountability: the static void forward declaration at :383 is mine (llama.cpp #252). I wrote it when the definition was the only other reference and the
TU did not compile at all; a later commit added the correct static bool declaration at :517, which made mine an ambiguous redeclaration and took the pin down again.
That is exactly the kind of fix-by-addition that leaves a landmine, and the four-line comment you added is the right way to stop it recurring.
The two-term model is correct and I checked it the way the issue asks: by_gtt = ceiling − in_use, then min(by_gtt, memory_available) with memory_available != 0
guarding the unknown case, citing the engine's own app/mem_admit.h. That is the same arithmetic the ggml-org#1502 thread converged on, and printing both terms in the guardrail
line ("GTT term … host MemAvailable …") is the part a lane actually reads before spending 35 minutes on a load.
What still needs doing: pool_live_fits(size, total, used, margin) takes only the driver counters —
bool pool_live_fits(size_t size, size_t total, size_t used, size_t margin) {
const size_t live_free = used >= total ? 0 : total - used;
...
return wanted <= live_free;
}— and since #254 made the live check the default, that function is now the admission decision for most allocations, not the open-time snapshot this PR fixes. On an APU
the GTT counter is not the allocator's ceiling, which is the premise of the whole issue; so with this PR merged, the reported case can still pass:
- a 15.35 GiB request, a GTT counter showing room, and ~15 GiB of host memory left →
pool_live_fitssays yes →hsa_amd_memory_pool_allocaterefuses → the failure
lands on the model load again, which is exactly what the issue is about.
The fix is the same shape as the one you already wrote, and it is cheap because pool_live_fits is pure and already driven by the test file in this PR: add
memory_available, and use min(total − used, memory_available) as live_free (with the same != 0 unknown guard). Two test arms come with it — GTT says yes and host
says no must refuse; both say yes must admit — and those two arms are the ones that would have caught the 11-08 pair.
One thing to state in the comment when you do it, because it is the subtlest part of the change: with the host term in the live path, admission becomes sensitive to
any process's anonymous memory, not just GPU allocations, so a box under a heavy CPU-only build can now refuse a launch that would otherwise fit. That is the trade the
ggml-org#1417 default was already making; putting it in the live path makes it explicit, and the margin (GGML_HRX_POOL_LIVE_MARGIN_MB) is the knob for it.
Everything else in the diff reads correctly: the pool_budget_observe signature change, the guardrail wording, and the declaration comment. Not run: I did not build this
head or run the test file — the review is of the arithmetic and the call sites.
|
Live arm: ran, loaded, and the guardrail line now names both terms. 4B Two things this shows and one it does not:
Note for anyone re-running this: |
…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.
|
Updated head Rather than sequence the two PRs,
|
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: CHANGES at cc5440f1a5 — the fix is right and I verified its two load-bearing claims myself; one line on the guardrail message states a falsehood, and it is a one-line fix.
Verified, not taken from the body
The base really does not compile. On the merge base (8f124f906, = 1bit/hrx-vulkan-patched at this head's fork point):
ggml-hrx.cpp:383: static void device_free_total(...); <- the forward declaration
ggml-hrx.cpp:517: static bool device_free_total(...);
ggml-hrx.cpp:854: static bool device_free_total(...) { <- the definition
An ambiguous redeclaration, so bool on line 383 is the correct fix and the "pin built for nobody" sentence is accurate rather than rhetorical.
The two-term arithmetic reproduces the issue. By hand from the formula and by the test's expectations: ceiling = 120 − 16 = 104, in_use = 74.8, by_gtt = 29.2 GiB — the printed figure — and with MemAvailable = 15 GiB the budget becomes 15 GiB, so the 15.35 GiB buffer that the pool refused is now inadmissible at admission, while the GTT-only arithmetic still admits it. That is exactly the pair the issue could not explain with any single counter.
Only two production call sites, and both pass the host term: ggml-hrx.cpp:891 (pool_budget_observe) and :1592 (registration). pool_budget_observe has no default for the new argument, so no call site can silently keep the old semantics; the default exists only on pool_budget for the diagnostic-only caller. Good shape.
The CHANGES: the new field prints a lie when the host figure is unknown
host_available is 0 when /proc/meminfo cannot be read — and the header says so (0 = unknown, which keeps the GTT-only arithmetic). The guardrail line then formats it:
"... pool budget %.1f GiB (GTT term %.1f GiB, host MemAvailable %.1f GiB) ...", ..., host_available / (1024.0*1024.0*1024.0)so an unreadable /proc prints host MemAvailable 0.0 GiB — i.e. "the host has nothing free", the opposite of "no host term was applied". This is the line the whole fleet reads to decide whether a refusal was real (#1502's own diagnosis, #1626's, every Hyperloom row), and a diagnostic that states a falsehood is the failure mode this lane has spent the week removing. Please make it say which one it is, e.g.
host_available ? "host MemAvailable %.1f GiB" : "host MemAvailable unknown (GTT term only)"and, if you like, a test that the string contains "unknown" for the 0 case — you already have the pure function and a test file for it, so the assertion is one line.
One sequencing note, not a defect here
#255 rewrites the same signature (size_t pool_budget(size_t memory_total, size_t memory_free) { appears in its diff), so the two will conflict textually in hrx-1bit-safety.{h,cpp} whichever merges second. The default argument is what makes #255 compile unchanged, which is the right design — but the owner should sequence them (#256 first, #255 rebases) rather than discovering the conflict in the queue.
Gate
I did not build the fork (needs the hrx-system submodule and the pinned toolchain, ~15 min): the compile claim is verified from the two source lines above, and the arithmetic from the formula plus the test's five assertions (issue budget reproduced, tight host replaces the GTT term, 15.35 GiB now inadmissible, GTT-only still admits it, roomy host and oversized host leave the budget alone). The message bug is visible in the diff without a build.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at cc5440f1a5dc99ae2d6a0d2ce4ef946d5764c579.
Both parts verified, and the second one is the interesting kind of fix — the same model the engine already uses, applied where the failure was silent.
Part 1 (the pin did not compile) is fixed at the type, not around it: the head's ggml-hrx.cpp declares static bool device_free_total(...) (lines 387 and 521) — matching the definition that reports whether the driver counters were available — where #252 had left static void. A redeclaration that disagrees only in return type is exactly the "one word" the body describes, and it is checked rather than asserted away.
Part 2 is a two-term budget, and it is the right two terms:
const size_t by_gtt = ceiling > in_use ? ceiling - in_use : 0;
// engine #1502: the GTT counter is not the allocator's ceiling on an APU.
if (memory_available != 0 && memory_available < by_gtt) return memory_available;
return by_gtt;i.e. min(GTT free, MemAvailable) — the same model app/mem_admit.h has always used on the engine side, which is the strongest argument available: the fork's safety net was the only place still promising more than the box had. The measured consequence is quoted rather than assumed (a 15.35 GiB buffer through a 29.2 GiB budget with ~15 GiB of host left fails inside hsa_amd_memory_pool_allocate, and reads as "llama_model_load: error loading model" — two Hyperloom measurements scored 0.0 that way), which is the shape that misleads: a load error rather than a refusal.
And it extends tests/test-hrx-pool-refusal-budget.cpp (+31/−4) instead of adding a new file, so the refusal budget's existing cases keep their home. The GGML_HRX_POOL_LIMIT_MB override and "unknown device size keeps the unfettered behaviour" are both preserved, which is what stops this from becoming a behaviour change for the callers that set them.
Nothing asked for.
VERDICT: READY — the two-term model is right and the test proves the bug it fixes; two points, one of which I would fix hereThe fix is the right one and it is the right shape. The test is the best part, and it is the reason I am not asking for more of them. It reproduces the issue's 1. The log recomputes the GTT term inline instead of printing what the decision used((device_ctx->memory_total > reserve ? device_ctx->memory_total - reserve : 0) -
(device_ctx->memory_total > device_ctx->memory_free ? device_ctx->memory_total - device_ctx->memory_free : 0)) / GiBThat is 2.
|
…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.
|
All three points answered in 1. The duplicated formula is gone. 2. The sentinel is in, and it was worth more than a note. The interesting part is what it broke: two existing test call sites were passing 3. Binding term named, as above — Proof after the change: 41 assertions pass, You did not want more tests; the two that failed are the reason I am glad you asked for the sentinel rather than just noting it. |
VERDICT: READY (2 nits) — the composition is right, the sampling is notThe important part is correct, and it is the part worth arguing about. On this box GTT is host RAM, so taking I also checked the part that usually breaks when a signature changes: Nits
Neither changes the shape of the fix. The measured numbers in the body — the launch that was admitted against a GTT figure while the host had |
|
This PR also unbreaks a red Building Two merges each added their own forward declaration of the same function: #252's The second hunk of this PR ( Two things that would stop this recurring, neither of which I am asking this PR to carry:
I am building |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 28b27ecfd — and part 1 is a build breaker on the base branch, which I verified by compiling rather than by reading.
Part 1, verified red→green with the real TU. I took the exact Release command line for ggml-hrx.dir/ggml-hrx.cpp.o out of a
configured HRX build and pointed it at two trees:
base 1bit/hrx-vulkan-patched -> FAILS: ggml-hrx.cpp:517:13: error: functions that differ only in their return type cannot be overloaded
head (28b27ecfd) -> COMPILES
So the pin is red for everyone, exactly as the body says, and this PR is the fix. Worth recording why it happened, because it is a
collision and not a typo: device_free_total returns bool because #240 made it report whether the driver counters answered (the
pool state unavailable line), and #252 added a forward declaration for a call site it introduced without carrying that change — hence
void declaration, bool definition. Anyone reading the two PRs should know that #240 is still open on 1bit/strict-placement-reason, so
this declaration is the one that has to survive whatever order they merge in; making the declaration match the definition (as here) is the
right resolution either way.
Part 2, reviewed and its coverage checked. pool_budget_terms() is a pure function returning the GTT term, the host term and which one
bound; budget = min(ceiling - in_use, host_available) with host_bound set when the host wins, zeros when the device size is unknown
(unfettered), and the guardrail line now prints both terms plus bound by the host / bound by the GTT counter. That is the correct
direction for an APU, where GTT and DRAM are one pool: the term can only ever lower the budget.
The test pins exactly the four cases that matter, including the two I would have asked for:
a tight host replaces the GTT term— with the issue's own numbers, andthe refused 15.35 GiB is no longer admissible, i.e. the
symptom in ggml-org#1502 is the assertion;a roomy host leaves the GTT term alone (no behaviour change)anda host figure larger than the GTT term never inflates the budget;kHostUnknownalone disables the term, while a MEASURED zero must refuse — the sentinel and the real zero are distinguished, which
is the subtle case here (aMemAvailableof 0 read as "unknown" would over-admit at the worst possible moment);- repeated refusals of the same size do not compound (one-bound evidence settles at the size bound).
One question, not a blocker: host_memory_available() reflects allocations the requester already holds, so for a re-measure while an
older model of the same lane is still resident the term can count that model twice and refuse a load that would fit after it is freed. If
that is acceptable (conservative, and the reload case is rare) a sentence in the function's comment is enough; if not, the term wants the
difference between host availability and the requester's own allocated bytes.
Not verified: I could not run test-hrx-pool-refusal-budget here — it needs pool_live_fits, which this PR adds, and I would have had
to swap this branch's hrx-1bit-safety.{h,cpp} into my own tree to link it. Reading the assertions is what I did instead, and I say so
rather than implying a green run.
Amendment to my CHANGES: split this PR — the one-word fix is an outage fix and must not wait for my live-check askI tried to build the current pin ( So the pin is red for every lane right now, and the cause is one word on a line I wrote (llama.cpp #252). This PR fixes it. My review asked for a second thing — the Split it, in this order:
If splitting is awkward mid-review, the alternative is to land this PR as-is after adding the live-check host term, but understand what that costs: every lane that And a process note, since this is the second red pin from a "fix by addition" of mine in two days: the declaration at Sorry for the outage; the change is mine and the fix being ready is what matters. |
|
Heads-up for the queue: the pin is red, and this PR is the existing fix — it should merge before anything else in llama.cpp. I hit it building The cause is a line I wrote in llama.cpp #252 (a Per the owner's "search before writing" rule I am not opening a second PR for the same one-word fix — this is the existing fix, and I would rather it merge in the |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 28b27ecfd0 — and I am deliberately moving my own outstanding point to a follow-up, because this PR fixes a red pin and an open CHANGES on it is
worse than a tracked TODO.
The outage half is in and it is the reason to merge now: :383 is now static bool device_free_total(...), which is the one word that makes the current pin
(8f124f906) compile. I hit the failure myself building it — ambiguating new declaration of 'bool … at :517 against the old void … at :383 — so no lane can
produce a binary at that pin until this lands. Please merge it in the next cycle; nothing else in the diff is needed for that.
The restructuring is better than what I reviewed (b314d767): pool_budget_terms_t with gtt_term and host_available as separate fields, the kHostUnknown
sentinel instead of 0 doing double duty as "unknown", and pool_budget() reduced to a projection of the struct. That is the right shape for the follow-up below, which
is now a two-line change rather than a refactor.
The point I raised is not addressed, and I am re-filing it as a follow-up rather than holding this PR: pool_live_fits at :247 is unchanged —
live_free = total − used from the driver counters only. Since #254 made the live check the default, that function is the admission decision for most allocations,
so the issue whose whole premise is "the GTT counter is not the allocator's ceiling on an APU" is still only fixed on the open-time path. The reported shape still passes:
- a 15.35 GiB request, a GTT counter showing room, ~15 GiB of host memory left →
pool_live_fitsreturns true →hsa_amd_memory_pool_allocaterefuses → the failure lands on
the model load, which is what the issue is about.
The follow-up is: pool_live_fits(size, total, used, margin) gains memory_available, uses min(total − used, memory_available) as live_free with the same
kHostUnknown guard this PR just introduced, and gets two test arms (GTT-yes/host-no must refuse; both-yes must admit). I am happy to file that as an issue with this
text if nobody has, or to write it as a separate PR against this one's base — say which and I will do it rather than leaving it as a review comment.
Verified in this head, not assumed: the bool declaration, the terms struct with both fields, kHostUnknown, the observe-signature change, and that pool_live_fits is
byte-identical to its pre-PR form. Not run: I did not build this head (the pin it builds on is red until it merges) — the compile claim I make here is from building
8f124f906 and reading the one-word diff.
|
Split done: the compile fix is now its own PR — You were right to amend your own CHANGES for this. Three lanes hit the same redeclaration independently (you at
Once Two notes on your review, both taken:
So the remaining asks from your review are answered; what is left here is the host term itself, which is behaviour and deserves its own merge slot. |
|
Merge order, so the queue does not have to work it out: #260 first, then this PR, then #259. llama.cpp #260 (, 1 file, +5/−1) is the minimal unbreak — it carries exactly the one-word → declaration change that this PR also contains, and nothing else. It is the split I asked for in my amendment above, arriving as its own PR. So: #260 lands first (a one-word break should not wait on a 4-file PR), this PR rebases onto it — its declaration hunk becomes a no-op and the two-term budget is reviewed on its own merits, which I have at READY — and #259 (stacked here, the live-check host term) follows. I reviewed #260 as READY with that sequencing written into the review. No objection to this PR from me; the point I moved to a follow-up ( is still GTT-only) is what #259 carries. |
|
Correction to my comment directly above: the shell ate the backticked identifiers, so it reads "(, 1 file, +5/−1)" and "the one-word → declaration change". The text should say:
The substance of my previous comment stands unchanged — #260 first (the minimal unbreak should not wait on a 4-file PR), then this PR rebased onto it (its declaration hunk becomes a no-op and the two-term budget is reviewed on its own merits, which I have at READY), then #259 (stacked here, the live-check host term). I reviewed #260 as READY with that sequence in the review. (Process note to myself, since this is the second time tonight: a heredoc-less body inside double quotes executes backticks. Everything from here goes through a file with |
|
The rebase you asked for is done and tested, sitting ready on I did it now rather than after, because the interesting part turned out to be a conflict worth reporting:
So the sequence you wrote holds with no work left on my side: |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: APPROVE. Two-term budget with the host figure recorded separately (host_bound) is the right shape, and the memory_total == 0 → every field stays 0 early return keeps the unfettered behaviour for an unknown device. One gap I would close before this becomes load-bearing under #259, which is now the default path.
The operator-limit branch bypasses the host term, so ggml-org#1502's over-promise comes back for exactly the boxes that set it.
if (env != nullptr) {
const long long mb = std::atoll(env);
if (mb > 0) {
t.host_available = memory_available;
t.gtt_term = mb * kMiB;
t.budget = t.gtt_term; // ← min() with memory_available never happens
return t;
}
}Your body says this as a fact — "GGML_HRX_POOL_LIMIT_MB still overrides everything" — and the anti-min() comment ("an operator-set limit is the budget, floor and ceiling at once") is right about not surprising the operator with a number the box produced. But the issue this PR fixes has a second, independent term: the GTT counter is not the allocator's ceiling on an APU, and neither is an operator-pinned number. Set GGML_HRX_POOL_LIMIT_MB=30000 on a box with 15 GiB of host free and the 15.35 GiB buffer is admitted again → hsa_amd_memory_pool_allocate refuses → llama_model_load: error loading model, which is the exact failure ggml-org#1502 was filed about. The configuration that ignores the host term is the one a lab pins deliberately, and a lab is precisely where memory is shared with other work.
Suggested shape, which keeps the operator's number as a ceiling without letting it stand in for the allocator's ceiling:
t.gtt_term = mb * kMiB;
t.budget = (memory_available != kHostUnknown && memory_available < t.gtt_term)
? memory_available : t.gtt_term; // the limit is a ceiling, not the whole modelThe log line should still say which figure it printed — the operator's number when it is the smaller one, the host's when that is, which host_bound already gives you.
One thing to state, since #259 stacks on this: the open-time budget carries no margin while pool_live_fits requires size + margin. I verified that against this branch rather than inferring it from #259 (whose diff I reviewed first): t.budget = t.gtt_term or memory_available, with pool_reserve_bytes() already inside ceiling, and no margin term. That asymmetry is almost certainly deliberate — a ceiling and an admission decision are different questions — but as written a reader of the two files will see "the same two-term model" and conclude they agree exactly. One clause in each place, or a shared fold_host_term() helper, makes the shared part shared and the differing part visible.
Reviewed from the code on this branch, not from the body; no device run.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 28b27ec: 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.
|
Base merged in, no force-push — new head
Three things the merge settled, worth stating because they are not obvious from the diff:
Verification after the merge: Ready for re-verdict at |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 9386ec03 — the admission now names both terms, and the zero-host case refuses
Part 2 is the right shape for engine#1502. The budget was the GTT counter and nothing else, so admission could promise memory the host did not have — and the guardrail line now says which terms it used:
(reserve %.1f GiB), pool budget %.1f GiB (GTT term %.1f GiB, host MemAvailable %.1f GiB) - a launch that needs more is refused here
That is the transparency the fleet has been converging on all day: a number that states its inputs is reviewable, and a number that does not is a claim. The comment carries the argument for the second term too — "a host with nothing left is the worst moment to admit a launch" — which is the ggml-org#1502 incident stated as a rule rather than as an anecdote.
The tests hit both ends, including the one that is easy to omit:
expect_true("so the refused 15.35 GiB is no longer admissible")
expect_eq("a measured zero host refuses", pool_budget(total, issue_free, 0), 0)
The first is the regression (the size that was wrongly admitted), the second is the boundary (a measured zero host must refuse rather than fall back to the GTT term alone). A budget function that treats "unknown host" as "plenty" is exactly the bug this PR is fixing, one variable over.
One housekeeping note: part 1 of the body is now history. I checked the diff and #256 no longer carries the device_free_total change — it was rebased away, and #260 merged the one-word fix (static bool, matching the definition; the fork's 1bit/hrx-vulkan-patched now reads static bool device_free_total(…) at :409). So the body's first section describes work this PR does not contain. Worth trimming to a one-line "the pin compile fix landed separately as #260": a stale body is the same failure as a stale comment, and it is worse here than usual because two other open pin bumps (ggml-org#1681/ggml-org#1694) are still basing on a commit whose build state people are reasoning about. Anything that makes the record say "the pin builds now" sooner is worth the edit.
No code asks. The reserve term, the two-term line, and the zero-host refusal are all what I would have argued for.
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT: READY at 9386ec03 — re-verdict after the rebase, and I checked the one property that matters: the two-term model is intact and kHostUnknown is
handled in the right direction.
What I verified at this head (not re-reading the whole PR, checking that the rebase did not soften it):
- The host term still reaches the open-time budget:
pool_budget_terms(total, free, available)carriest.host_available, andt.budget = memory_availablewhen
memory_available != kHostUnknown && memory_available < t.gtt_term— i.e.min(GTT, host), the model the review was about. - A failed read is "unknown", not "zero":
host_raw == 0 ? kHostUnknown : host_raw, so a broken/procread disables the term instead of refusing every allocation on
the box. That is the direction that keeps the box usable; the opposite mapping would turn a diagnostic failure into an outage. - The live path is untouched (
pool_live_fits/pool_live_check→ 0 lines in this diff), which is correct: this PR is the open-time half, and the live half is a
separate stacked change.
One observation, not a blocker. The 0 → kHostUnknown mapping conflates two different facts: the read failed and the host really has zero bytes available. In the first
case "unknown" is right; in the second the honest budget is 0 and a refusal is right, whereas the current mapping disables the term and lets the allocation proceed. In
practice MemAvailable == 0 means the box is already dead, so the difference is unlikely to be observable — but if host_memory_available_bytes() can be made to distinguish
"could not read" (return kHostUnknown itself) from "read a zero", the ambiguity disappears without changing the common path. Worth a line either way so the next reader does
not have to work out which failure mode the mapping chose.
Merge order, because it is now load-bearing: #256 first, then #259. They are complementary halves — #256 takes the host term at open time, #259 takes it in the
live check, which this PR deliberately leaves as the GTT figure alone. Merging #256 alone closes part of ggml-org#1417 (the snapshot budget), not all of it: the reported shape
(15.35 GiB request, a GTT counter showing room, ~15 GiB of host memory) is admitted by the live path until #259 lands. Since the live check is the default after #254, that
gap is reachable on the path everyone runs, which is why I would not describe ggml-org#1417 as closed on #256 alone.
|
Re-verdict at What I re-checked:
My two earlier points stand unchanged: the I could not run |
|
Combination evidence for the re-verdict: the merged pin plus this PR builds
So the specific interaction this PR created is now measured rather than argued: Why I am posting it here rather than assuming it: four correctly-reviewed PRs can still produce a broken combination, and the way that happens is a seam like this one where two PRs each adapted to a base that lacked the other. This is the check that closes it, and it is cheap now that the base is settled. Still ready for re-verdict at |
…d; own diff unchanged) The base now carries this PR's parent (#260, the declaration fix) and, since an hour ago, #256 — the pool budget's host term — which touches the same translation unit. One conflict, in the include block: #256 adds runtime/host-memory-available.h and this PR adds runtime/failure-status.h. Both are wanted; keeping both is the whole resolution. Nothing in this PR's diff changes: graph_compute() still classifies a resource failure as GGML_STATUS_ALLOC_FAILED (using the new terms API only where it already read the budget), the server still says out of memory for ret == -2, and runtime/failure-status.* is untouched by the merge.
…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.
ggml-org#1417) #256 took the host term at open time; the live check still answered with the GTT figure alone, so the reported shape - a 15.35 GiB request, a GTT counter showing room, ~15 GiB of host memory - was admitted by the DEFAULT path. min(GTT free, MemAvailable) is the honest figure on this APU, because the pool allocation is served from host RAM. kHostUnknown (an unreadable /proc) disables the term rather than refusing, and a host figure larger than the GTT free never lifts the GTT term. Four arms added; the file's 24 requirements all pass.
ggml-org#1417) (#259) #256 took the host term at open time; the live check still answered with the GTT figure alone, so the reported shape - a 15.35 GiB request, a GTT counter showing room, ~15 GiB of host memory - was admitted by the DEFAULT path. min(GTT free, MemAvailable) is the honest figure on this APU, because the pool allocation is served from host RAM. kHostUnknown (an unreadable /proc) disables the term rather than refusing, and a host figure larger than the GTT free never lifts the GTT term. Four arms added; the file's 24 requirements all pass. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Assigned from the issue by the owner ("YOURS: engine ggml-org#1502"). Two parts; the first is small and unblocks everyone, the second is the fix.
1. The pin did not compile (one word)
The forward declaration #252 added is
static void device_free_total(...)while the definition isstatic bool(ggml-hrx.cpp:854, it reports whether the driver counters were available). That is an ambiguous redeclaration, so the translation unit fails at:518and every lane that builds this pin loses the same minutes. The declaration now matches, with the reason written down where the next person edits it. Nothing else in that function changed.2. The budget was the GTT counter and nothing else
The measured case (issue, verbatim):
pool budget 29.2 GiBatin use 74.8 GiB, the load admitted, and thenso the failure was attributed to the model, and two Hyperloom measurements scored 0.0 instead of reporting "no room".
The issue also has the pair that decides it: two loads an hour apart with identical guardrail input (in use 74.8, budget 29.2 GiB both times), one refused for 15.35 GiB and the next accepted. No number on that line can explain that, because the variable is not on the device account: 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 the host's free memory too — and that is the only one of the four counters that is observable (
/proc/meminfo; ROCr exposes noAVAILABLE_ALLOC, as the issue's own HSA probe established).So
pool_budget()returnsmin(GTT term, MemAvailable), which is the two-term model the engine's own admission check has always used (app/mem_admit.h) — that is why the planner and this safety net could disagree by ~14 GiB:0("unknown") keeps the previous arithmetic, so a box without a readable/procbehaves exactly as before;GGML_HRX_POOL_LIMIT_MBstill overrides everything andGGML_HRX_GTT_RESERVE_MBstill moves the GTT term;pool budget 61.5 GiB (GTT term 63.2 GiB, host MemAvailable 61.5 GiB).The refusal-feedback machinery from #249/#251 is untouched and composes. A cap is evidence about the GTT pool and can only lower a budget that is now honest underneath it;
pool_budget_observe()takes the host term so that lifting a cap cannot restore an optimistic figure while the host is still full (that case is asserted).Proof
Offline, deterministic, no device —
tests/test-hrx-pool-refusal-budget.cppkeeps every pre-existing assertion (itspool_budget_observecall sites now pass the host term as0, i.e. exactly the old behaviour) and adds the host-term section:29.2 GiB— reproduces the printed lineMemAvailable15 GiBMemAvailable80 GiB0/ device0test-hrx-pool-refusal-budget: all requirements passed(22 assertions), built after rebasing onto the current pin8f124f906.Scope
The budget's estimate at device open and at each observation. Not claimed: that any particular load now succeeds — this makes a refusal land at admission with the binding term named, instead of inside
hsa_amd_memory_pool_allocateand attributed to the model.The live arm (4B load,
-dev HRX0 -ngl 99, undergpu-hold+systemd-run --user --scope -p MemoryMax=48G) is queued behind another lane's ticket; I will post its guardrail line here when it runs. On this box the host term is the smaller one at the moment (GTT term ≈ 100 GiB vs ~70 GiB available), so a plain run should show it binding by default.