use anyhow::{Context, Result, bail};
pub fn load_average_1m() -> Option<f64> {
let out = std::process::Command::new("uptime").output().ok()?;
let text = String::from_utf8(out.stdout).ok()?;
let tail = text.split("load average").nth(1)?;
tail.split(|c: char| !c.is_ascii_digit() && c != '.')
.find(|s| !s.is_empty())
.and_then(|s| s.parse().ok())
}
pub fn self_cpu_seconds() -> Option<f64> {
let out = std::process::Command::new("ps")
.args(["-o", "cputime=", "-p", &std::process::id().to_string()])
.output()
.ok()?;
let t = String::from_utf8(out.stdout).ok()?;
let mut secs = 0.0f64;
for part in t.trim().split(':') {
secs = secs * 60.0 + part.parse::<f64>().ok()?;
}
Some(secs)
}
pub fn foreign_load(self_cores: f64) -> Option<f64> {
load_average_1m().map(|l| (l - self_cores).max(0.0))
}
pub fn foreign_gpu_procs() -> Option<Vec<String>> {
let out = std::process::Command::new("nvidia-smi")
.args([
"--query-compute-apps=pid,used_memory,process_name",
"--format=csv,noheader",
])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
Some(parse_compute_apps(&text, std::process::id()))
}
pub fn parse_compute_apps(text: &str, me: u32) -> Vec<String> {
let me = me.to_string();
text.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.filter(|l| l.split(',').next().map(str::trim) != Some(me.as_str()))
.map(str::to_owned)
.collect()
}
pub fn ensure_quiet_machine() -> Result<bool> {
let overridden = std::env::var("OSFKB_SCOREBOARD_IGNORE_LOAD").as_deref() == Ok("1");
let mut quiet = true;
match foreign_gpu_procs() {
Some(procs) if !procs.is_empty() => {
quiet = false;
eprintln!(
"quiet-machine guard: {} foreign GPU process(es):",
procs.len()
);
for p in &procs {
eprintln!(" {p}");
}
if !overridden {
bail!(
"the GPUs are not idle: {} foreign compute process(es) are resident, so \
absolutes would be distorted (CPU load average cannot see this). Wait for a \
clear GPU or set OSFKB_SCOREBOARD_IGNORE_LOAD=1 for a non-pinnable \
exploratory run.",
procs.len()
)
}
}
Some(_) => eprintln!("quiet-machine guard: GPUs idle (no foreign compute procs) — ok"),
None => eprintln!("quiet-machine guard: no nvidia-smi; GPU contention UNCHECKED"),
}
let cores = std::thread::available_parallelism()
.map(std::num::NonZero::get)
.unwrap_or(8) as f64;
match load_average_1m() {
None => eprintln!("quiet-machine guard: load average unavailable; CPU load UNCHECKED"),
Some(load) => {
let limit = cores / 2.0;
if load < limit {
eprintln!(
"quiet-machine guard: load {load:.2} < {limit:.1} ({cores:.0} cores) — ok"
);
} else {
quiet = false;
if !overridden {
bail!(
"machine is busy (1-min load {load:.2} ≥ {limit:.1} on {cores:.0} cores): \
absolutes would be distorted. Wait for quiet or set \
OSFKB_SCOREBOARD_IGNORE_LOAD=1 for a non-pinnable exploratory run."
)
}
eprintln!("quiet-machine guard: load {load:.2} ≥ {limit:.1} ({cores:.0} cores)");
}
}
}
if !quiet {
eprintln!("quiet-machine guard OVERRIDDEN — these numbers are exploratory, not pinnable");
}
Ok(quiet)
}
pub fn nvidia_clock_note() {
let Ok(out) = std::process::Command::new("nvidia-smi")
.args([
"--query-gpu=index,clocks.sm,clocks_throttle_reasons.active",
"--format=csv,noheader",
])
.output()
else {
return;
};
if out.status.success() {
let text = String::from_utf8_lossy(&out.stdout);
for line in text.lines() {
eprintln!("nvidia clocks: {line}");
}
}
}
pub fn read_pins(path: &str) -> Result<serde_json::Value> {
match std::fs::read(path) {
Ok(bytes) => serde_json::from_slice(&bytes).with_context(|| format!("parse {path}")),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(serde_json::json!({"rows": {}})),
Err(e) => Err(e).with_context(|| format!("read {path}")),
}
}
pub fn write_pins(path: &str, pins: &serde_json::Value) -> Result<()> {
if let Some(dir) = std::path::Path::new(path).parent() {
std::fs::create_dir_all(dir).ok();
}
std::fs::write(path, serde_json::to_vec_pretty(pins)?).with_context(|| format!("write {path}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_compute_apps_excludes_self_and_blank_lines() {
let text = "1234, 512 MiB, python\n\n 5678, 9 GiB, train.py \n";
let got = parse_compute_apps(text, 1234);
assert_eq!(got, vec!["5678, 9 GiB, train.py".to_string()]);
assert!(parse_compute_apps("", 1).is_empty());
}
}