Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ggml/src/ggml-hrx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,8 @@ ggml_add_backend_library(ggml-hrx
runtime/graph-program-cache.h
runtime/host-memory.cpp
runtime/host-memory.h
runtime/host-memory-available.cpp
runtime/host-memory-available.h
runtime/host-memory-ternary.cpp
runtime/host-memory-ternary.h
dispatch/ternary-q4-0.h
Expand Down
65 changes: 65 additions & 0 deletions ggml/src/ggml-hrx/runtime/host-memory-available.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2026 bong-water-water-bong
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "host-memory-available.h"

#include <cstdio>
#include <cstring>
#include <limits>

namespace ggml::hrx {
namespace {

constexpr size_t kMeminfoBytes = 16 * 1024;

} // namespace

size_t mem_available_from_meminfo(const char * meminfo) {
if (meminfo == nullptr) {
return 0;
}
for (const char * line = meminfo; *line != '\0';) {
if (std::strncmp(line, "MemAvailable:", sizeof("MemAvailable:") - 1) == 0) {
unsigned long long kib = 0;
if (std::sscanf(line + sizeof("MemAvailable:") - 1, " %llu", &kib) != 1) {
return 0;
}
if (kib > std::numeric_limits<size_t>::max() / 1024) {
return 0;
}
return static_cast<size_t>(kib) * 1024;
}
const char * next = std::strchr(line, '\n');
if (next == nullptr) {
return 0;
}
line = next + 1;
}
return 0;
}

size_t host_memory_available_bytes() {
FILE * file = std::fopen("/proc/meminfo", "rb");
if (file == nullptr) {
return 0;
}
char body[kMeminfoBytes] = {};
const size_t read = std::fread(body, 1, sizeof(body) - 1, file);
std::fclose(file);
body[read] = '\0';
return mem_available_from_meminfo(body);
}

} // namespace ggml::hrx
30 changes: 30 additions & 0 deletions ggml/src/ggml-hrx/runtime/host-memory-available.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2026 bong-water-water-bong
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <cstddef>

namespace ggml::hrx {

// Bytes of "MemAvailable" in a /proc/meminfo body; 0 when the field is absent, unparsable, or would overflow.
// Split from the read so the warning in host-memory.cpp can be tested without a device or a live /proc.
size_t mem_available_from_meminfo(const char * meminfo);

// Live MemAvailable on this host; 0 when /proc/meminfo cannot be read. 0 means "unknown": a caller that wants to warn
// about memory must stay quiet rather than guess a threshold (engine #669).
size_t host_memory_available_bytes();

} // namespace ggml::hrx
18 changes: 18 additions & 0 deletions ggml/src/ggml-hrx/runtime/host-memory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "ggml-quants.h"
#include "hrx-interop-utils.h"
#include "hrx_runtime.h"
#include "runtime/host-memory-available.h"
#include "runtime/host-memory-ternary.h"

#include <algorithm>
Expand Down Expand Up @@ -2013,6 +2014,21 @@ HostWeightAcquireResult HostWeightCache::acquire(hrx_device_t device
stats_.allocation_count = entries_.size();
stats_.resident_bytes += materialized_length;
resident_bytes_ += materialized_length;
// engine #669: the limit is off by default, so nothing stops the residents growing to the size of the model and
// the box dies with no line saying why. One warning, naming the size and the flag, once they pass what the box
// had available when the first weight was materialized. Never a refusal: the default stays unbounded.
if (limit_ == 0 && !oversize_warned_) {
if (available_at_first_weight_ == 0) {
available_at_first_weight_ = host_memory_available_bytes();
}
if (available_at_first_weight_ != 0 && resident_bytes_ > available_at_first_weight_) {
oversize_warned_ = true;
GGML_LOG_WARN("hrx: %.1f MiB of host weights are resident, more than the %.1f MiB this box had "
"available when the first one was materialised: GGML_HRX_HOST_WEIGHT_LIMIT_MB is unset "
"(the default), so residency is unbounded and grows with the model (engine #669)\n",
resident_bytes_ / (1024.0 * 1024.0), available_at_first_weight_ / (1024.0 * 1024.0));
}
}
}
result.lease = HostWeightLease(entry);
return result;
Expand All @@ -2028,6 +2044,8 @@ void HostWeightCache::clear() {
entries_.clear();
last_use_.clear();
resident_bytes_ = 0;
available_at_first_weight_ = 0;
oversize_warned_ = false;
stats_ = {};
if (arena_ != nullptr) {
hrx_buffer_release(arena_);
Expand Down
5 changes: 5 additions & 0 deletions ggml/src/ggml-hrx/runtime/host-memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ class HostWeightCache {
size_t resident_bytes_ = 0;
uint64_t use_clock_ = 0;
bool limit_resolved_ = false;
// engine #669: with the limit unset (the default) nothing bounds host-weight residency, so an OOM reads like an
// ordinary allocation failure. The first materialization records what the box had available at that moment, and
// once the residents pass it the cache says so - once, with the number and the flag.
size_t available_at_first_weight_ = 0;
bool oversize_warned_ = false;
std::mutex evict_mutex_;

// One device buffer holds every resident weight (engine #669). Materializing each weight with its own
Expand Down
6 changes: 6 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,12 @@ if (TARGET ggml-hrx)
target_include_directories(test-hrx-arena-refusal PRIVATE ../ggml/src/ggml-hrx)
add_test(NAME test-hrx-arena-refusal COMMAND test-hrx-arena-refusal)

# MemAvailable parsing for the unbounded-residency warning: pure, no device and no /proc (engine #669)
add_executable(test-hrx-host-memory-available test-hrx-host-memory-available.cpp
../ggml/src/ggml-hrx/runtime/host-memory-available.cpp)
target_include_directories(test-hrx-host-memory-available PRIVATE ../ggml/include ../ggml/src ../ggml/src/ggml-hrx)
add_test(NAME test-hrx-host-memory-available COMMAND test-hrx-host-memory-available)

add_executable(test-hrx-buffer test-hrx-buffer.cpp)
target_link_libraries(test-hrx-buffer PRIVATE ggml-hrx ggml hrx::hrx)
target_include_directories(test-hrx-buffer PRIVATE ../ggml/include ../ggml/src ../ggml/src/ggml-hrx)
Expand Down
78 changes: 78 additions & 0 deletions tests/test-hrx-host-memory-available.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2026 bong-water-water-bong
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Host test for host-memory-available.h: the MemAvailable the unbounded-residency warning compares against
// (engine #669). Pure: no device, no HRX header, and the /proc body is supplied by the test.

#include "runtime/host-memory-available.h"

#include <cstdio>
#include <cstdlib>
#include <cstring>

#define REQUIRE(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "%s:%d: requirement failed: %s\n", __FILE__, __LINE__, #condition); \
std::abort(); \
} \
} while (false)

using ggml::hrx::host_memory_available_bytes;
using ggml::hrx::mem_available_from_meminfo;

static const size_t MiB = size_t{ 1 } << 20;

int main() {
// A real /proc/meminfo shape: MemAvailable is neither the first line nor the last.
const char * body =
"MemTotal: 128140180 kB\n"
"MemFree: 901888 kB\n"
"MemAvailable: 55939156 kB\n"
"Buffers: 12345 kB\n"
"Cached: 9876543 kB\n";
REQUIRE(mem_available_from_meminfo(body) == size_t{ 55939156 } * 1024);

// Whitespace between the colon and the number is a single field width in meminfo; the parse must not depend on it.
REQUIRE(mem_available_from_meminfo("MemAvailable: 2048 kB\n") == 2048 * 1024);
REQUIRE(mem_available_from_meminfo("MemAvailable:\t2048 kB\n") == 2048 * 1024);

// No field, no answer - and a lookalike key must not answer for it.
REQUIRE(mem_available_from_meminfo("MemTotal: 1024 kB\nMemFree: 512 kB\n") == 0);
REQUIRE(mem_available_from_meminfo("MemAvailableX: 4096 kB\n") == 0);
REQUIRE(mem_available_from_meminfo("MemAvailable: 4096 kB\nMemFree: 1 kB\n") == 4096 * 1024);

// Junk, an empty value, an absent terminator, and an impossible size are "unknown", never a guess.
REQUIRE(mem_available_from_meminfo("MemAvailable: banana kB\n") == 0);
REQUIRE(mem_available_from_meminfo("MemAvailable:\n") == 0);
REQUIRE(mem_available_from_meminfo("MemAvailable: 4096 kB") == 4096 * 1024);
REQUIRE(mem_available_from_meminfo("MemAvailable: 18446744073709551615 kB\n") == 0);
REQUIRE(mem_available_from_meminfo("MemAvailable: -2048 kB\n") == 0);
REQUIRE(mem_available_from_meminfo(nullptr) == 0);
REQUIRE(mem_available_from_meminfo("") == 0);

// The live reader on this host: whoever calls it must get either a plausible number or 0 ("unknown", stay quiet).
const size_t live = host_memory_available_bytes();
std::printf("host_memory_available_bytes() = %zu bytes (%.1f MiB)\n", live, double(live) / double(MiB));
FILE * meminfo = std::fopen("/proc/meminfo", "rb");
if (meminfo != nullptr) {
std::fclose(meminfo);
REQUIRE(live > 0);
REQUIRE(live < size_t{ 1 } << 50);
}

std::printf("test-hrx-host-memory-available: all requirements passed\n");
return 0;
}