diff --git a/ggml/src/ggml-hrx/hrx-1bit-safety.cpp b/ggml/src/ggml-hrx/hrx-1bit-safety.cpp index 47cd43b5d170..04ae93e239d5 100644 --- a/ggml/src/ggml-hrx/hrx-1bit-safety.cpp +++ b/ggml/src/ggml-hrx/hrx-1bit-safety.cpp @@ -14,6 +14,7 @@ // limitations under the License. #include "hrx-1bit-safety.h" +#include "runtime/host-memory-available.h" // MemAvailable for the live check's host term (engine #1502) #include "ggml-impl.h" @@ -247,9 +248,15 @@ pool_budget_state_t pool_budget_observe(size_t memory_total, size_t free_bytes, // Does |size| fit the live pool with |margin| of headroom? Pure, so the test drives it: the numbers come from the driver's // counters, and the arithmetic that matters is the underflow guard (|used| can exceed |total| transiently) and the margin // saturation (|margin| is an operator input and may be absurd). -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; - const size_t wanted = size + (margin > SIZE_MAX - size ? SIZE_MAX - size : margin); +bool pool_live_fits(size_t size, size_t total, size_t used, size_t margin, size_t memory_available) { + size_t live_free = used >= total ? 0 : total - used; + // engine #1502: the GTT counter alone over-promises on this APU, because the pool allocation is served from host RAM. + // Without this term the live check admits a request the open-time budget would have refused -- the reported shape was + // a 15.35 GiB request, a GTT counter showing room, and ~15 GiB of host memory available. + if (memory_available != kHostUnknown && memory_available < live_free) { + live_free = memory_available; + } + const size_t wanted = size + (margin > SIZE_MAX - size ? SIZE_MAX - size : margin); return wanted <= live_free; } @@ -260,7 +267,9 @@ bool pool_reserve(size_t budget, size_t size, const char * what) { size_t in_use = 0; if (pool_live_counters(total, in_use)) { const size_t margin = pool_live_margin_bytes(); - if (!pool_live_fits(size, total, in_use, margin)) { + const size_t host_raw = host_memory_available_bytes(); + const size_t host_available = host_raw == 0 ? kHostUnknown : host_raw; + if (!pool_live_fits(size, total, in_use, margin, host_available)) { const size_t live_free = in_use >= total ? 0 : total - in_use; GGML_LOG_ERROR("%s: HRX pool limit: %s of %.1f MiB does not fit the live device budget " "(%.1f MiB free - %.1f MiB margin of %.1f MiB); this is the live check, on by default since " diff --git a/ggml/src/ggml-hrx/hrx-1bit-safety.h b/ggml/src/ggml-hrx/hrx-1bit-safety.h index 05de425b4d4c..959e030e90da 100644 --- a/ggml/src/ggml-hrx/hrx-1bit-safety.h +++ b/ggml/src/ggml-hrx/hrx-1bit-safety.h @@ -98,7 +98,11 @@ bool gtt_counters(size_t & total, size_t & used); // Charge `size` against `budget`; false (with one log line naming the sizes) when it does not fit. // True when |size| fits the live pool (|total|, |used| from the driver's counters) with |margin| of headroom. Pure, so the // test drives it; the underflow and margin-saturation guards live here rather than in the caller. -bool pool_live_fits(size_t size, size_t total, size_t used, size_t margin); +// The live check takes the host term too (engine #1502): the counters report GTT, but a hsa_amd_memory_pool_allocate on +// this APU is served by host RAM, so `min(GTT free, MemAvailable)` is the honest figure -- the same two-term model the +// open-time budget uses. kHostUnknown disables the term (an unreadable /proc must not refuse anything). +bool pool_live_fits(size_t size, size_t total, size_t used, size_t margin, + size_t memory_available = kHostUnknown); bool pool_reserve(size_t budget, size_t size, const char * what); diff --git a/tests/test-hrx-pool-refusal-budget.cpp b/tests/test-hrx-pool-refusal-budget.cpp index 86b36e2a3334..1bbcd2f7e7dc 100644 --- a/tests/test-hrx-pool-refusal-budget.cpp +++ b/tests/test-hrx-pool-refusal-budget.cpp @@ -124,6 +124,18 @@ int main() { expect_true("a 15.35 GiB buffer fits 45 GiB free with the default margin", pool_live_fits(refused, gtt, gtt - 45 * GiB, margin_256)); + + // The host term (engine #1502): the GTT counters alone admit a request the host cannot serve. These arms are the + // reported shape -- a large request, a GTT counter showing room, and little MemAvailable left. + expect_true("live: the request the issue reports is refused once the host term is known", + !pool_live_fits(16 * GiB, gtt, gtt - 45 * GiB, margin_256, /*memory_available=*/2 * GiB)); + expect_true("live: the same request passes on the GTT figure alone (the over-promise being fixed)", + pool_live_fits(16 * GiB, gtt, gtt - 45 * GiB, margin_256, kHostUnknown)); + expect_true("live: an unreadable MemAvailable (kHostUnknown) never refuses on its own", + pool_live_fits(16 * GiB, gtt, gtt - 45 * GiB, margin_256, kHostUnknown) == + pool_live_fits(16 * GiB, gtt, gtt - 45 * GiB, margin_256)); + expect_true("live: a host figure LARGER than GTT free does not lift the GTT term", + !pool_live_fits(16 * GiB, gtt, gtt - 16 * GiB, margin_256, 200 * GiB)); // The case the live check exists for, built from the report's own numbers: the open-time budget was // 104 - 74.8 = 29.2 GiB, so a 20 GiB buffer is ADMITTED by the snapshot. If a neighbour then grew the box to // 105 GiB in use (15 GiB free), the snapshot still admits it and the failure lands in the allocator; the live