use std::collections::HashMap;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::{Duration, Instant};
use tokio::process::{Child, Command};
use tokio::sync::Mutex;
use tracing::{info, warn};
use crate::vllm_runtime;
const HEALTH_TIMEOUT: Duration = Duration::from_secs(3);
const READY_DEADLINE: Duration = Duration::from_secs(120);
const STALL_TIMEOUT: Duration = Duration::from_secs(180);
const READY_POLL: Duration = Duration::from_millis(500);
struct ManagedServer {
child: Child,
port: u16,
last_used: Instant,
}
impl ManagedServer {
fn endpoint(&self) -> String {
format!("http://127.0.0.1:{}", self.port)
}
fn is_alive(&mut self) -> bool {
matches!(self.child.try_wait(), Ok(None))
}
}
pub struct VllmServerPool {
servers: Mutex<HashMap<String, ManagedServer>>,
idle_ttl: Duration,
}
impl VllmServerPool {
pub fn new(idle_ttl: Duration) -> Self {
Self {
servers: Mutex::new(HashMap::new()),
idle_ttl,
}
}
pub async fn ensure(
&self,
model_id: &str,
runtime_model: &str,
family: &str,
) -> Result<String, String> {
let mut servers = self.servers.lock().await;
if let Some(s) = servers.get_mut(model_id) {
if s.is_alive() {
s.last_used = Instant::now();
return Ok(s.endpoint());
}
warn!(
model = model_id,
"supervised vllm-mlx server died; respawning"
);
servers.remove(model_id);
}
let runtime = vllm_runtime::ensure_runtime()
.await
.map_err(|e| format!("vllm-mlx runtime unavailable: {e}"))?;
let mut disable_xet = false;
loop {
let port = alloc_loopback_port()?;
let endpoint = format!("http://127.0.0.1:{port}");
info!(
model = model_id,
runtime_model, port, disable_xet, "starting supervised vllm-mlx server"
);
let child = spawn_server(&runtime.server, runtime_model, port, disable_xet, family)
.map_err(|e| format!("failed to spawn vllm-mlx serve: {e}"))?;
let mut server = ManagedServer {
child,
port,
last_used: Instant::now(),
};
match wait_ready(&endpoint, &mut server, runtime_model).await {
Ok(()) => {
info!(model = model_id, endpoint = %endpoint, "vllm-mlx server ready");
servers.insert(model_id.to_string(), server);
return Ok(endpoint);
}
Err(NotReady::Stalled(reason)) if !disable_xet => {
warn!(
model = model_id,
reason, "vllm-mlx startup stalled; retrying with HuggingFace Xet disabled"
);
disable_xet = true;
}
Err(other) => return Err(other.into_message()),
}
}
}
pub async fn evict_idle(&self) -> usize {
let mut servers = self.servers.lock().await;
let now = Instant::now();
let ttl = self.idle_ttl;
let mut stale: Vec<String> = Vec::new();
for (k, s) in servers.iter_mut() {
if now.duration_since(s.last_used) > ttl || !s.is_alive() {
stale.push(k.clone());
}
}
for k in &stale {
info!(model = %k, "evicting idle vllm-mlx server");
servers.remove(k); }
stale.len()
}
pub async fn len(&self) -> usize {
self.servers.lock().await.len()
}
pub async fn is_empty(&self) -> bool {
self.servers.lock().await.is_empty()
}
}
fn alloc_loopback_port() -> Result<u16, String> {
let listener = TcpListener::bind("127.0.0.1:0")
.map_err(|e| format!("could not allocate a local port: {e}"))?;
listener
.local_addr()
.map(|a| a.port())
.map_err(|e| format!("could not read allocated port: {e}"))
}
fn spawn_server(
server_bin: &Path,
runtime_model: &str,
port: u16,
disable_xet: bool,
family: &str,
) -> std::io::Result<Child> {
let (out, err) = log_sinks(port);
let mut cmd = Command::new(server_bin);
cmd.arg("serve")
.arg(runtime_model)
.arg("--port")
.arg(port.to_string())
.arg("--enable-auto-tool-choice")
.arg("--tool-call-parser")
.arg("auto")
.stdin(Stdio::null())
.stdout(out)
.stderr(err)
.kill_on_drop(true);
if let Some(parser) = reasoning_parser_for(family) {
cmd.arg("--reasoning-parser").arg(parser);
}
if disable_xet {
let (key, value) = XET_DISABLE_ENV;
cmd.env(key, value);
}
cmd.spawn()
}
fn reasoning_parser_for(family: &str) -> Option<&'static str> {
let f = family.to_ascii_lowercase();
if f.contains("qwen") {
Some("qwen3")
} else if f.contains("gemma") {
Some("gemma4")
} else if f.contains("glm") {
Some("glm4")
} else if f.contains("deepseek") {
Some("deepseek_r1")
} else if f.contains("gpt") && f.contains("oss") {
Some("gpt_oss")
} else {
None
}
}
const XET_DISABLE_ENV: (&str, &str) = ("HF_HUB_DISABLE_XET", "1");
fn log_sinks(port: u16) -> (Stdio, Stdio) {
let open = |suffix: &str| {
let path = log_path(port, suffix)?;
std::fs::create_dir_all(path.parent()?).ok()?;
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.ok()
.map(Stdio::from)
};
match (open("stdout"), open("stderr")) {
(Some(o), Some(e)) => (o, e),
_ => (Stdio::null(), Stdio::null()),
}
}
fn log_path(port: u16, suffix: &str) -> Option<PathBuf> {
car_home::root().map(|root| {
root.join("logs")
.join(format!("vllm-mlx-{port}.{suffix}.log"))
})
}
async fn wait_ready(
endpoint: &str,
server: &mut ManagedServer,
runtime_model: &str,
) -> Result<(), NotReady> {
let start = Instant::now();
let log = log_path(server.port, "stderr");
let cache = crate::registry::huggingface_repo_dir(runtime_model);
let xet = xet_cache_root();
let mut last_mark = progress_mark(log.as_deref(), &cache, &xet);
let mut last_progress = Instant::now();
loop {
if vllm_runtime::health_ok(endpoint, HEALTH_TIMEOUT).await {
return Ok(());
}
if !server.is_alive() {
return Err(NotReady::Other(format!(
"vllm-mlx server exited during startup (see ~/.car/logs/vllm-mlx-{}.stderr.log)",
server.port
)));
}
match progress_mark(log.as_deref(), &cache, &xet) {
Some(mark) => {
if Some(mark) != last_mark {
last_mark = Some(mark);
last_progress = Instant::now();
}
if last_progress.elapsed() > STALL_TIMEOUT {
return Err(NotReady::Stalled(format!(
"vllm-mlx server made no progress for {}s and never became healthy at \
{endpoint} (see ~/.car/logs/vllm-mlx-{}.stderr.log)",
STALL_TIMEOUT.as_secs(),
server.port
)));
}
}
None => {
if start.elapsed() > READY_DEADLINE {
return Err(NotReady::Stalled(format!(
"vllm-mlx server did not become healthy at {endpoint} within {}s",
READY_DEADLINE.as_secs()
)));
}
}
}
tokio::time::sleep(READY_POLL).await;
}
}
#[derive(Debug)]
enum NotReady {
Stalled(String),
Other(String),
}
impl NotReady {
fn into_message(self) -> String {
match self {
NotReady::Stalled(m) | NotReady::Other(m) => m,
}
}
}
fn file_len(path: &Path) -> Option<u64> {
std::fs::metadata(path).ok().map(|m| m.len())
}
fn progress_mark(log: Option<&Path>, cache_dir: &Path, xet_root: &Path) -> Option<u64> {
let log_bytes = log.and_then(file_len);
let cache_bytes = dir_bytes(cache_dir);
let xet_activity = newest_dir_mtime(xet_root);
match (log_bytes, cache_bytes, xet_activity) {
(None, None, None) => None,
(a, b, c) => Some(
a.unwrap_or(0)
.saturating_add(b.unwrap_or(0))
.saturating_add(c.unwrap_or(0)),
),
}
}
fn xet_cache_root() -> PathBuf {
crate::registry::huggingface_cache_root()
.parent()
.map(|hf| hf.join("xet"))
.unwrap_or_else(|| PathBuf::from("xet"))
}
fn newest_dir_mtime(dir: &Path) -> Option<u64> {
if !dir.is_dir() {
return None;
}
fn mtime_secs(path: &Path) -> u64 {
std::fs::metadata(path)
.ok()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs())
.unwrap_or(0)
}
let mut newest = mtime_secs(dir);
let mut level = vec![dir.to_path_buf()];
for _ in 0..2 {
let mut next = Vec::new();
for d in &level {
let Ok(entries) = std::fs::read_dir(d) else {
continue;
};
for entry in entries.flatten() {
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
let path = entry.path();
newest = newest.max(mtime_secs(&path));
next.push(path);
}
}
}
level = next;
}
Some(newest)
}
fn dir_bytes(dir: &Path) -> Option<u64> {
if !dir.is_dir() {
return None;
}
let mut total = 0u64;
let mut stack = vec![dir.to_path_buf()];
let mut depth = 0;
while let Some(current) = stack.pop() {
let Ok(entries) = std::fs::read_dir(¤t) else {
continue;
};
for entry in entries.flatten() {
match entry.metadata() {
Ok(m) if m.is_file() => total = total.saturating_add(m.len()),
Ok(m) if m.is_dir() && depth < 2 => stack.push(entry.path()),
_ => {}
}
}
depth += 1;
}
Some(total)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alloc_port_returns_distinct_usable_ports() {
let a = alloc_loopback_port().unwrap();
let b = alloc_loopback_port().unwrap();
assert_ne!(a, 0);
assert_ne!(b, 0);
assert!(TcpListener::bind(("127.0.0.1", a)).is_ok());
}
#[tokio::test]
async fn evict_idle_on_empty_pool_is_zero() {
let pool = VllmServerPool::new(Duration::from_secs(300));
assert_eq!(pool.evict_idle().await, 0);
assert_eq!(pool.len().await, 0);
}
#[tokio::test]
async fn spawns_and_health_waits_a_stand_in_server() {
let Some(python) = vllm_runtime::which("python3") else {
eprintln!("SKIP: python3 not available");
return;
};
let dir = std::env::temp_dir().join(format!("car-vllm-pool-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let script = dir.join("fake-vllm-mlx");
std::fs::write(
&script,
format!(
"#!{}\n\
import sys, http.server\n\
port = int(sys.argv[sys.argv.index('--port') + 1])\n\
class H(http.server.BaseHTTPRequestHandler):\n\
\x20 def do_GET(self):\n\
\x20 self.send_response(200); self.end_headers(); self.wfile.write(b'ok')\n\
\x20 def log_message(self, *a): pass\n\
http.server.HTTPServer(('127.0.0.1', port), H).serve_forever()\n",
python.display()
),
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let port = alloc_loopback_port().unwrap();
let child = spawn_server(&script, "dummy/model", port, false, "qwen3").expect("spawn");
let mut server = ManagedServer {
child,
port,
last_used: Instant::now(),
};
let endpoint = server.endpoint();
wait_ready(&endpoint, &mut server, "test-org/stand-in-model")
.await
.expect("stand-in server should become healthy");
assert!(vllm_runtime::health_ok(&endpoint, Duration::from_secs(2)).await);
assert!(server.is_alive());
drop(server);
let _ = std::fs::remove_dir_all(&dir);
}
}
#[cfg(test)]
mod readiness_tests {
use super::*;
#[test]
fn stall_timeout_is_the_bound_not_total_elapsed_time() {
assert!(
STALL_TIMEOUT >= Duration::from_secs(120),
"a stall bound shorter than a slow model-load step would reintroduce \
spurious startup failures"
);
}
#[test]
fn log_path_is_stable_and_suffix_keyed() {
let (out, err) = (log_path(4242, "stdout"), log_path(4242, "stderr"));
if let (Some(o), Some(e)) = (out, err) {
assert_ne!(o, e, "stdout and stderr must not share a file");
assert!(o.ends_with("vllm-mlx-4242.stdout.log"), "{}", o.display());
assert!(e.ends_with("vllm-mlx-4242.stderr.log"), "{}", e.display());
assert_eq!(e.parent(), o.parent());
}
}
#[test]
fn a_silent_log_still_counts_as_progress_while_weights_land() {
let dir = tempfile::tempdir().unwrap();
let log = dir.path().join("server.stderr.log");
std::fs::write(&log, b"Fetching 13 files: 77%").unwrap();
let cache = dir.path().join("models--org--big");
std::fs::create_dir_all(cache.join("blobs")).unwrap();
let no_xet = dir.path().join("no-xet");
let before = progress_mark(Some(&log), &cache, &no_xet).expect("observable");
std::fs::write(
cache.join("blobs").join("shard.incomplete"),
vec![0u8; 4096],
)
.unwrap();
let after = progress_mark(Some(&log), &cache, &no_xet).expect("observable");
assert!(
after > before,
"weights landing must register as progress even with a silent log: {before} -> {after}"
);
}
#[test]
fn directory_activity_is_visible_without_any_byte_growth() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("xet");
std::fs::create_dir_all(root.join("cas-server").join("chunk-cache")).unwrap();
let before = newest_dir_mtime(&root).expect("existing dir is observable");
std::fs::create_dir_all(root.join("cas-server").join("staging")).unwrap();
let after = newest_dir_mtime(&root).expect("still observable");
assert!(
after >= before,
"mtime must not go backwards: {before} -> {after}"
);
assert!(newest_dir_mtime(&dir.path().join("absent")).is_none());
}
#[test]
fn progress_is_unobservable_only_when_neither_source_exists() {
let dir = tempfile::tempdir().unwrap();
let missing_log = dir.path().join("nope.log");
let missing_cache = dir.path().join("nope-cache");
let missing_xet = dir.path().join("nope-xet");
assert_eq!(
progress_mark(Some(&missing_log), &missing_cache, &missing_xet),
None,
"with nothing to watch, the caller must fall back to an absolute deadline"
);
std::fs::create_dir_all(&missing_cache).unwrap();
assert!(
progress_mark(Some(&missing_log), &missing_cache, &missing_xet).is_some(),
"an existing cache dir is observable even before any bytes arrive"
);
std::fs::create_dir_all(dir.path().join("real-xet")).unwrap();
assert!(
progress_mark(None, &missing_cache, &dir.path().join("real-xet")).is_some(),
"an observable Xet cache must make progress measurable on its own"
);
}
#[test]
fn file_len_reports_growth_and_tolerates_a_missing_file() {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("x.log");
assert_eq!(
file_len(&f),
None,
"missing file must not look like progress"
);
std::fs::write(&f, b"loading").unwrap();
let first = file_len(&f).expect("written file has a length");
std::fs::write(&f, b"loading... fetching shard 2 of 7").unwrap();
let second = file_len(&f).expect("still readable");
assert!(
second > first,
"growth must be observable: {first} -> {second}"
);
}
}
#[cfg(test)]
mod xet_fallback_tests {
use super::*;
#[tokio::test]
async fn tool_calling_flags_reach_the_spawned_process() {
let Some(sh) = vllm_runtime::which("sh") else {
return;
};
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("argv.txt");
let script = dir.path().join("fake-vllm-mlx");
std::fs::write(
&script,
format!(
"#!{}\nprintf '%s\\n' \"$@\" > {}\n",
sh.display(),
out.display()
),
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let mut child = spawn_server(&script, "org/model", 1234, false, "qwen3.8").expect("spawn");
let _ = child.wait().await;
let argv: Vec<String> = std::fs::read_to_string(&out)
.unwrap()
.lines()
.map(str::to_string)
.collect();
assert!(
argv.contains(&"--enable-auto-tool-choice".to_string()),
"tool calling must be enabled or the advertised tool_use capability is a lie: {argv:?}"
);
let parser = argv
.iter()
.position(|a| a == "--tool-call-parser")
.and_then(|i| argv.get(i + 1));
assert_eq!(
parser.map(String::as_str),
Some("auto"),
"--enable-auto-tool-choice requires a parser; `auto` avoids a \
per-architecture table CAR would have to maintain: {argv:?}"
);
assert_eq!(argv.first().map(String::as_str), Some("serve"));
assert!(argv.contains(&"org/model".to_string()));
assert!(argv.contains(&"1234".to_string()));
}
#[tokio::test]
async fn disable_xet_reaches_the_spawned_process() {
let Some(sh) = vllm_runtime::which("sh") else {
return;
};
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("env.txt");
let script = dir.path().join("fake-vllm-mlx");
std::fs::write(
&script,
format!(
"#!{}\nprintenv {} > {} 2>&1 || echo UNSET > {}\n",
sh.display(),
XET_DISABLE_ENV.0,
out.display(),
out.display()
),
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let mut child = spawn_server(&script, "dummy/model", 1, false, "qwen3").expect("spawn");
let _ = child.wait().await;
assert_eq!(
std::fs::read_to_string(&out).unwrap().trim(),
"UNSET",
"the default path must leave Xet enabled — it is normally the faster one"
);
let mut child = spawn_server(&script, "dummy/model", 1, true, "qwen3").expect("spawn");
let _ = child.wait().await;
assert_eq!(
std::fs::read_to_string(&out).unwrap().trim(),
XET_DISABLE_ENV.1,
"the stalled-start retry must actually disable Xet in the child"
);
}
}
#[cfg(test)]
mod reasoning_parser_tests {
use super::*;
#[test]
fn both_family_spellings_map_to_the_same_parser() {
for family in ["qwen3.8", "qwen3.5", "qwen3_5_moe", "qwen3", "Qwen3.6"] {
assert_eq!(
reasoning_parser_for(family),
Some("qwen3"),
"family `{family}` should select the qwen3 reasoning parser"
);
}
assert_eq!(reasoning_parser_for("gemma4_unified"), Some("gemma4"));
assert_eq!(reasoning_parser_for("glm4_moe_lite"), Some("glm4"));
assert_eq!(reasoning_parser_for("glm4.7"), Some("glm4"));
assert_eq!(reasoning_parser_for("deepseek_v3"), Some("deepseek_r1"));
}
#[test]
fn an_unknown_family_selects_no_parser() {
for family in ["llama", "mistral-nemo", "phi3", "", "something-new"] {
assert_eq!(
reasoning_parser_for(family),
None,
"unknown family `{family}` must not be given a guessed parser"
);
}
}
#[tokio::test]
async fn the_reasoning_parser_reaches_the_spawned_process() {
let Some(sh) = vllm_runtime::which("sh") else {
return;
};
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("argv.txt");
let script = dir.path().join("fake-vllm-mlx");
std::fs::write(
&script,
format!(
"#!{}\nprintf '%s\\n' \"$@\" > {}\n",
sh.display(),
out.display()
),
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
}
let read_argv = |path: &std::path::Path| -> Vec<String> {
std::fs::read_to_string(path)
.unwrap()
.lines()
.map(str::to_string)
.collect()
};
let mut child = spawn_server(&script, "org/m", 1, false, "qwen3.8").expect("spawn");
let _ = child.wait().await;
let argv = read_argv(&out);
let parser = argv
.iter()
.position(|a| a == "--reasoning-parser")
.and_then(|i| argv.get(i + 1));
assert_eq!(parser.map(String::as_str), Some("qwen3"), "{argv:?}");
let mut child = spawn_server(&script, "org/m", 1, false, "llama").expect("spawn");
let _ = child.wait().await;
let argv = read_argv(&out);
assert!(
!argv.contains(&"--reasoning-parser".to_string()),
"an unknown family must spawn without the flag: {argv:?}"
);
}
}