use crate::error::{RealizarError, Result};
use crate::gguf::GGUFModel;
const MIB: u64 = 1024 * 1024;
pub const FILE_FACTOR_PERCENT: u64 = 110;
pub const EMBEDDING_FACTOR_PERCENT: u64 = 60;
pub const AVAILABLE_PERCENT: u64 = 90;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostMemory {
Available(u64),
CgroupHeadroom(u64),
TotalOnly(u64),
Unknown,
}
impl HostMemory {
#[must_use]
pub fn measure() -> Self {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok();
if let Some((available, _total)) =
meminfo.as_deref().and_then(crate::capacity::parse_meminfo)
{
return Self::bound(available, cgroup_headroom_here());
}
crate::contract_gate::system_memory_bytes().map_or(Self::Unknown, Self::TotalOnly)
}
#[must_use]
pub fn bound(available: u64, cgroup_headroom: Option<u64>) -> Self {
match cgroup_headroom {
Some(headroom) if headroom < available => Self::CgroupHeadroom(headroom),
_ => Self::Available(available),
}
}
}
#[must_use]
pub fn cgroup_level_headroom(max: &str, current: &str, stat: &str) -> Option<u64> {
let max = max.trim();
if max == "max" {
return None;
}
let current = current.trim().parse::<u64>().ok()?;
let used = current.saturating_sub(cgroup_reclaimable_file(stat));
Some(max.parse::<u64>().ok()?.saturating_sub(used))
}
#[must_use]
pub fn cgroup_reclaimable_file(stat: &str) -> u64 {
stat.lines()
.filter_map(|line| line.split_once(' '))
.filter(|(key, _)| matches!(*key, "active_file" | "inactive_file"))
.filter_map(|(_, value)| value.trim().parse::<u64>().ok())
.fold(0, u64::saturating_add)
}
#[must_use]
pub fn cgroup_headroom_under(root: &std::path::Path, relative: &str) -> Option<u64> {
let mut dir = root.to_path_buf();
let levels = std::iter::once(String::new()).chain(
relative
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned),
);
levels
.filter_map(|part| {
dir.push(part);
let read = |f: &str| std::fs::read_to_string(dir.join(f)).ok();
cgroup_level_headroom(
&read("memory.max")?,
&read("memory.current")?,
&read("memory.stat").unwrap_or_default(),
)
})
.min()
}
fn cgroup_headroom_here() -> Option<u64> {
let cgroup = std::fs::read_to_string("/proc/self/cgroup").ok()?;
let relative = cgroup.lines().find_map(|l| l.strip_prefix("0::"))?;
cgroup_headroom_under(std::path::Path::new("/sys/fs/cgroup"), relative)
}
#[must_use]
pub fn host_build_need(file_bytes: u64, embedding_f32_bytes: u64) -> u64 {
let percent_of = |bytes: u64, percent: u64| {
u64::try_from(u128::from(bytes) * u128::from(percent) / 100).unwrap_or(u64::MAX)
};
percent_of(file_bytes, FILE_FACTOR_PERCENT)
.saturating_add(percent_of(embedding_f32_bytes, EMBEDDING_FACTOR_PERCENT))
}
#[must_use]
pub fn qwen35_embedding_f32_bytes(model: &GGUFModel) -> u64 {
model
.tensors
.iter()
.find(|t| t.name == "token_embd.weight")
.map_or(0, |t| {
t.dims
.iter()
.try_fold(4u64, |acc, &d| acc.checked_mul(d))
.unwrap_or(u64::MAX)
})
}
pub fn host_build_verdict(
file_bytes: u64,
embedding_f32_bytes: u64,
memory: HostMemory,
) -> Result<()> {
let need = host_build_need(file_bytes, embedding_f32_bytes);
let terms = format!(
"~{} MiB (1.10 x file {} MiB + 0.60 x f32 token-embedding size {} MiB)",
need / MIB,
file_bytes / MIB,
embedding_f32_bytes / MIB
);
let (measured, what) = match memory {
HostMemory::Available(bytes) => (bytes, "available"),
HostMemory::CgroupHeadroom(bytes) => (
bytes,
"left under this process's cgroup memory.max (below MemAvailable)",
),
HostMemory::TotalOnly(bytes) => (
bytes,
"of total RAM (this platform reports no MemAvailable)",
),
HostMemory::Unknown => {
return Err(RealizarError::HostRamRefused(format!(
"cannot measure host RAM on this platform ({}), so the guard cannot be \
evaluated; refusing to build {terms}. An unknown memory limit is not an \
unlimited one (#2568, #4947)",
std::env::consts::OS
)));
},
};
let limit = u128::from(measured) * u128::from(AVAILABLE_PERCENT) / 100;
if u128::from(need) > limit {
return Err(RealizarError::HostRamRefused(format!(
"the Qwen3.5 host build needs {terms}, more than 90% of the {} MiB {what} \
({} MiB), and would be OOM-killed. Free memory or use a smaller model (#4947)",
measured / MIB,
limit / u128::from(MIB)
)));
}
Ok(())
}
pub fn admit_qwen35_host_build(
model: &GGUFModel,
file_bytes: u64,
memory: HostMemory,
) -> Result<()> {
host_build_verdict(file_bytes, qwen35_embedding_f32_bytes(model), memory)
}
#[cfg(test)]
#[path = "host_capacity_tests.rs"]
mod tests;