Governed compute for unified-memory AI hardware — machines where CPU and GPU share one pool and there is no separate VRAM allocation to bounce off. Over-commit that pool and the box thrashes and wedges, SSH and ping included, before the OOM killer gets a turn. Compute does not run inference. It supervises the servers that do: admission control against both a declared budget and what the machine actually has free, a 1 Hz watchdog that stops the newest model before thrash, Scenes activated as one transactional unit with rollback, process ownership bound to (boot_id, pid, start_time_ticks, pgid) so a reused PID can never be group-killed, a protocol-transparent gateway, an MCP server, and a read-only HTTP API for dashboards. Registry footprints in this release are measured on a live node rather than estimated. One binary, six direct dependencies. Apache-2.0. Generated by scripts/publish-compute.sh, which refuses to publish a tree it cannot prove clean.
This commit is contained in:
+431
@@ -0,0 +1,431 @@
|
||||
//! Lumbridge Compute — safe AI workload orchestration for accelerator nodes.
|
||||
|
||||
mod agent;
|
||||
mod config;
|
||||
mod eval;
|
||||
mod gateway;
|
||||
mod governor;
|
||||
mod http;
|
||||
mod lifecycle;
|
||||
mod mcp;
|
||||
mod mem;
|
||||
mod proc;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use config::{find_scene, load_scenes, Registry, Scene};
|
||||
use proc::State;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "lumbridge-compute",
|
||||
version,
|
||||
about = "Lumbridge Compute — safe AI workload orchestration for accelerator nodes."
|
||||
)]
|
||||
struct Cli {
|
||||
/// Root dir containing registry/ and scenes/ (default: $LUMBRIDGE_COMPUTE_ROOT, $KUDA_ROOT, or current dir)
|
||||
#[arg(long, global = true)]
|
||||
root: Option<PathBuf>,
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Cmd {
|
||||
/// Governor status: memory, budget, running set, headroom
|
||||
Status,
|
||||
/// Manage the model registry
|
||||
Model {
|
||||
#[command(subcommand)]
|
||||
cmd: ModelCmd,
|
||||
},
|
||||
/// Manage scenes (bundles of models)
|
||||
Scene {
|
||||
#[command(subcommand)]
|
||||
cmd: SceneCmd,
|
||||
},
|
||||
/// Run reproducible capability and serving-performance evaluations
|
||||
Eval {
|
||||
#[command(subcommand)]
|
||||
cmd: EvalCmd,
|
||||
},
|
||||
/// Run the memory watchdog in the foreground (kills the newest model before OOM-wedge)
|
||||
Watchdog {
|
||||
/// Kill the newest model if MemAvailable dips below this many GB
|
||||
#[arg(long, default_value_t = governor::WATCHDOG_FLOOR_GB)]
|
||||
floor: f64,
|
||||
},
|
||||
/// Run the stable streaming gateway in the foreground
|
||||
Gateway {
|
||||
#[arg(long, default_value = "127.0.0.1:8011")]
|
||||
listen: String,
|
||||
#[arg(long, default_value = "127.0.0.1:8001")]
|
||||
upstream: String,
|
||||
},
|
||||
/// Serve the Governor as a read-only JSON API over HTTP
|
||||
///
|
||||
/// A second transport over the same operations the MCP server exposes, for a web UI. It
|
||||
/// never mutates: no activation, no stop, no eval run.
|
||||
Api {
|
||||
/// Loopback by default. This API has no authentication unless --token is set, and it
|
||||
/// reveals which models this node runs, so widening the bind is an explicit act.
|
||||
#[arg(long, default_value = "127.0.0.1:8012")]
|
||||
listen: String,
|
||||
/// Browser origin permitted to read this API. Repeatable. Empty means no browser may
|
||||
/// read it; `*` is deliberately not supported, because any page you visit would then
|
||||
/// be able to fingerprint this machine.
|
||||
#[arg(long = "allow-origin")]
|
||||
allow_origin: Vec<String>,
|
||||
/// Require `Authorization: Bearer <token>` on every request.
|
||||
#[arg(long)]
|
||||
token: Option<String>,
|
||||
},
|
||||
/// Serve the Governor to AI agents as MCP tools over stdio
|
||||
Mcp {
|
||||
/// Let an agent perform a real Scene transition, not just plan one.
|
||||
/// Off by default: the agent writes its own tool arguments, so the only
|
||||
/// meaningful gate on a production switch is one the operator sets here.
|
||||
#[arg(long)]
|
||||
allow_activate: bool,
|
||||
},
|
||||
/// Run the resident supervisor: resume desired Scene, gateway, and memory floor
|
||||
Agent {
|
||||
#[arg(long, default_value = "127.0.0.1:8011")]
|
||||
listen: String,
|
||||
#[arg(long, default_value = "127.0.0.1:8001")]
|
||||
upstream: String,
|
||||
#[arg(long, default_value_t = governor::WATCHDOG_FLOOR_GB)]
|
||||
floor: f64,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ModelCmd {
|
||||
/// List registered models and their live state
|
||||
Ls,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum SceneCmd {
|
||||
/// List scenes with total footprint
|
||||
Ls,
|
||||
/// Show a scene: models, footprints, and the Governor's admission verdict
|
||||
Show { name: String },
|
||||
/// Activate a scene: stop what's not in it, admit + start what is
|
||||
Activate {
|
||||
name: String,
|
||||
/// Print the plan without changing anything
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
/// Adopt an already-running exact Scene and bind legacy PIDs to process identities
|
||||
Adopt { name: String },
|
||||
/// Resume the persisted desired Scene, falling back to the previous known-good Scene
|
||||
Resume,
|
||||
/// Stop all Lumbridge Compute-managed models
|
||||
Deactivate,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum EvalCmd {
|
||||
/// List available evaluation suites
|
||||
Ls,
|
||||
/// Run a suite against an OpenAI-compatible endpoint
|
||||
Run {
|
||||
suite: String,
|
||||
#[arg(long, default_value = "http://127.0.0.1:8001/v1")]
|
||||
base_url: String,
|
||||
#[arg(long, default_value = "brain")]
|
||||
model: String,
|
||||
/// Override the suite's repetitions per case
|
||||
#[arg(long)]
|
||||
repeat: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
fn root_dir(cli: &Cli) -> PathBuf {
|
||||
cli.root
|
||||
.clone()
|
||||
.or_else(|| {
|
||||
std::env::var("LUMBRIDGE_COMPUTE_ROOT")
|
||||
.ok()
|
||||
.map(PathBuf::from)
|
||||
})
|
||||
.or_else(|| std::env::var("KUDA_ROOT").ok().map(PathBuf::from))
|
||||
.unwrap_or_else(|| PathBuf::from("."))
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let root = root_dir(&cli);
|
||||
match &cli.cmd {
|
||||
Cmd::Status => cmd_status(&root),
|
||||
Cmd::Model { cmd } => match cmd {
|
||||
ModelCmd::Ls => cmd_model_ls(&root),
|
||||
},
|
||||
Cmd::Scene { cmd } => match cmd {
|
||||
SceneCmd::Ls => cmd_scene_ls(&root),
|
||||
SceneCmd::Show { name } => cmd_scene_show(&root, name),
|
||||
SceneCmd::Activate { name, dry_run } => lifecycle::activate(&root, name, *dry_run),
|
||||
SceneCmd::Adopt { name } => lifecycle::adopt(&root, name),
|
||||
SceneCmd::Resume => lifecycle::resume(&root),
|
||||
SceneCmd::Deactivate => lifecycle::deactivate(&root),
|
||||
},
|
||||
Cmd::Eval { cmd } => match cmd {
|
||||
EvalCmd::Ls => eval::list(&root),
|
||||
EvalCmd::Run {
|
||||
suite,
|
||||
base_url,
|
||||
model,
|
||||
repeat,
|
||||
} => eval::run(&root, suite, base_url, model, *repeat),
|
||||
},
|
||||
Cmd::Watchdog { floor } => cmd_watchdog(&root, *floor),
|
||||
Cmd::Gateway { listen, upstream } => gateway::run(listen, upstream),
|
||||
Cmd::Api {
|
||||
listen,
|
||||
allow_origin,
|
||||
token,
|
||||
} => http::run(
|
||||
listen,
|
||||
http::Config {
|
||||
root: root.clone(),
|
||||
allowed_origins: allow_origin.clone(),
|
||||
token: token.clone(),
|
||||
},
|
||||
),
|
||||
Cmd::Mcp { allow_activate } => mcp::run(&root, *allow_activate),
|
||||
Cmd::Agent {
|
||||
listen,
|
||||
upstream,
|
||||
floor,
|
||||
} => agent::run(&root, listen, upstream, *floor),
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_status(root: &Path) -> Result<()> {
|
||||
let reg = Registry::load(root)?;
|
||||
let m = mem::read()?;
|
||||
let committed = governor::committed_gb(®);
|
||||
let running = governor::running_ids(®);
|
||||
let budget = governor::DEFAULT_BUDGET_GB;
|
||||
// The binding constraint, not the generous one: admission enforces the declared
|
||||
// budget AND observed memory, so reporting only the declared headroom would promise
|
||||
// room the next `scene activate` is going to refuse.
|
||||
let headroom = governor::headroom_gb(committed, budget, Some(m.available_gb)).max(0.0);
|
||||
let declared_headroom = (budget - committed - governor::SAFETY_MARGIN_GB).max(0.0);
|
||||
let managed = State::load_checked(root)?;
|
||||
|
||||
println!("Lumbridge Compute · governor");
|
||||
println!(
|
||||
" memory {:.1} GB total · {:.1} GB available",
|
||||
m.total_gb, m.available_gb
|
||||
);
|
||||
println!(
|
||||
" budget {:.0} GB (safety margin {:.0} · watchdog floor {:.0})",
|
||||
budget,
|
||||
governor::SAFETY_MARGIN_GB,
|
||||
governor::WATCHDOG_FLOOR_GB
|
||||
);
|
||||
println!(
|
||||
" committed {committed:.1} GB across {} model(s)",
|
||||
running.len()
|
||||
);
|
||||
if headroom < declared_headroom {
|
||||
println!(
|
||||
" headroom {headroom:.1} GB admittable (budget allows {declared_headroom:.1}; \
|
||||
MemAvailable is the tighter limit)"
|
||||
);
|
||||
} else {
|
||||
println!(" headroom {headroom:.1} GB admittable");
|
||||
}
|
||||
println!();
|
||||
println!(
|
||||
" scene active={} desired={} fallback={}",
|
||||
managed.active_scene.as_deref().unwrap_or("-"),
|
||||
managed.desired_scene.as_deref().unwrap_or("-"),
|
||||
managed.last_known_good_scene.as_deref().unwrap_or("-")
|
||||
);
|
||||
if let Some(error) = &managed.last_error {
|
||||
println!(" last error {error}");
|
||||
}
|
||||
|
||||
if running.is_empty() {
|
||||
println!(" (no registered models currently serving)");
|
||||
} else {
|
||||
println!(" running:");
|
||||
for id in &running {
|
||||
let mdl = ®.models[id];
|
||||
let tag = if managed
|
||||
.procs
|
||||
.get(id)
|
||||
.is_some_and(|process| process.owned_alive())
|
||||
{
|
||||
"compute"
|
||||
} else {
|
||||
"ext "
|
||||
};
|
||||
println!(
|
||||
" ● [{tag}] {:<12} {:>5.0} GB :{:<5} {}",
|
||||
id,
|
||||
mdl.footprint_gb,
|
||||
port_str(mdl.serve.port),
|
||||
mdl.name
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_model_ls(root: &Path) -> Result<()> {
|
||||
let reg = Registry::load(root)?;
|
||||
println!(
|
||||
"{:<12} {:>6} {:<6} {:<6} MODEL",
|
||||
"ID", "GB", "STATE", "PORT"
|
||||
);
|
||||
for (id, m) in ®.models {
|
||||
println!(
|
||||
"{:<12} {:>6.0} {:<6} {:<6} {}",
|
||||
id,
|
||||
m.footprint_gb,
|
||||
if governor::is_running(m) {
|
||||
"up"
|
||||
} else {
|
||||
"down"
|
||||
},
|
||||
port_str(m.serve.port),
|
||||
m.name
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_scene_ls(root: &Path) -> Result<()> {
|
||||
let reg = Registry::load(root)?;
|
||||
let scenes = load_scenes(root)?;
|
||||
if scenes.is_empty() {
|
||||
println!("no scenes in {}/scenes", root.display());
|
||||
return Ok(());
|
||||
}
|
||||
println!("{:<12} {:>6} {:<30} MODELS", "SCENE", "GB", "DESCRIPTION");
|
||||
for s in &scenes {
|
||||
let total = scene_footprint(s, ®);
|
||||
println!(
|
||||
"{:<12} {:>6.0} {:<30} {}",
|
||||
s.metadata.name,
|
||||
total,
|
||||
truncate(&s.metadata.description, 30),
|
||||
s.models.join(", ")
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_scene_show(root: &Path, name: &str) -> Result<()> {
|
||||
let reg = Registry::load(root)?;
|
||||
let scene = find_scene(root, name)?;
|
||||
let budget = scene.budget_gb.unwrap_or(governor::DEFAULT_BUDGET_GB);
|
||||
|
||||
println!(
|
||||
"scene {} (v{})",
|
||||
scene.metadata.name, scene.metadata.version
|
||||
);
|
||||
if !scene.metadata.description.is_empty() {
|
||||
println!(" {}", scene.metadata.description);
|
||||
}
|
||||
println!(" budget {budget:.0} GB\n models:");
|
||||
|
||||
let mut total = 0.0;
|
||||
let mut missing = Vec::new();
|
||||
for id in &scene.models {
|
||||
match reg.models.get(id) {
|
||||
Some(m) => {
|
||||
total += m.footprint_gb;
|
||||
let state = if governor::is_running(m) {
|
||||
"up"
|
||||
} else {
|
||||
"down"
|
||||
};
|
||||
println!(
|
||||
" {:<12} {:>5.0} GB [{:<4}] {}",
|
||||
id, m.footprint_gb, state, m.name
|
||||
);
|
||||
}
|
||||
None => {
|
||||
missing.push(id.clone());
|
||||
println!(" {id:<12} ? [MISSING from registry]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let needed = total + governor::SAFETY_MARGIN_GB;
|
||||
println!(
|
||||
"\n total footprint {total:.1} GB (+{:.0} safety = {needed:.1} GB)",
|
||||
governor::SAFETY_MARGIN_GB
|
||||
);
|
||||
let verdict = if !missing.is_empty() {
|
||||
format!("✗ {} model(s) missing from registry", missing.len())
|
||||
} else if needed <= budget {
|
||||
format!("✓ fits — {:.1} GB to spare", budget - needed)
|
||||
} else {
|
||||
format!("✗ exceeds budget by {:.1} GB", needed - budget)
|
||||
};
|
||||
println!(" admission {verdict}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cmd_watchdog(root: &Path, floor: f64) -> Result<()> {
|
||||
println!(
|
||||
"Lumbridge Compute watchdog — killing the newest model if MemAvailable < {floor:.1} GB. Ctrl-C to stop."
|
||||
);
|
||||
loop {
|
||||
let m = mem::read()?;
|
||||
if m.available_gb < floor {
|
||||
let mut state = State::load_checked(root)?;
|
||||
match state.newest_alive() {
|
||||
Some((id, p)) => {
|
||||
eprintln!(
|
||||
"watchdog: MemAvailable {:.1} GB < floor {:.1} — killing newest '{id}' (pid {})",
|
||||
m.available_gb, floor, p.pid
|
||||
);
|
||||
proc::stop_owned(&p)?;
|
||||
state.procs.remove(&id);
|
||||
state.save(root)?;
|
||||
}
|
||||
None => eprintln!(
|
||||
"watchdog: MemAvailable {:.1} GB < floor {:.1} but no Lumbridge Compute-managed model to kill!",
|
||||
m.available_gb, floor
|
||||
),
|
||||
}
|
||||
}
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- helpers ---------------------------------------------------------------
|
||||
|
||||
fn scene_footprint(s: &Scene, reg: &Registry) -> f64 {
|
||||
s.models
|
||||
.iter()
|
||||
.filter_map(|id| reg.models.get(id))
|
||||
.map(|m| m.footprint_gb)
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn port_str(p: Option<u16>) -> String {
|
||||
p.map(|p| p.to_string()).unwrap_or_else(|| "-".to_string())
|
||||
}
|
||||
|
||||
fn truncate(s: &str, n: usize) -> String {
|
||||
if s.chars().count() <= n {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}…",
|
||||
s.chars().take(n.saturating_sub(1)).collect::<String>()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user