use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, mpsc};
use std::time::{Duration, Instant};
const BATCH_DEADLINE: Duration = Duration::from_secs(30);
const DEADLINE_ENV: &str = "SEMLITH_ACCEL_DEADLINE_MS";
const HELLO_DEADLINE: Duration = Duration::from_secs(20 * 60);
const FIRST_COMPILE: Duration = Duration::from_secs(40);
const START_CAP: Duration = Duration::from_secs(60 * 60);
const WORKER_IDLE: Duration = Duration::from_secs(60);
const WORKER_DEPTH: usize = 2;
pub const MIN_COSINE_FP16: f32 = 0.999;
const MIN_COSINE_INT8: f32 = 0.97;
pub const ACCEL_ENV: &str = "SEMLITH_ACCEL";
pub const FAULT_ENV: &str = "SEMLITH_FAULT_ACCEL";
const FIXTURE_TEXTS: &str = include_str!("../tests/fixtures/gpu/known-answer.json");
const FIXTURE_VECTORS: &[u8] = include_bytes!("../tests/fixtures/gpu/known-answer.f32");
const DIM: usize = 384;
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct Switches {
pub cpu: Option<bool>,
pub gpu: Option<bool>,
pub cuda: Option<bool>,
pub ane: Option<bool>,
pub trt: Option<bool>,
pub openvino: Option<bool>,
pub llama: Option<bool>,
pub gpu_beside_ane: Option<bool>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Enabled {
pub cpu: bool,
pub gpu: bool,
pub cuda: bool,
pub ane: bool,
pub trt: bool,
pub openvino: bool,
pub llama: bool,
pub gpu_beside_ane: bool,
pub worker: bool,
pub source: &'static str,
}
impl Enabled {
pub fn named(&self) -> Vec<String> {
let mut lanes = Vec::new();
if self.cpu {
lanes.push("cpu".to_string());
}
for spec in SPECS {
if spec.id != "worker" && self.lane(spec.id) {
lanes.push(if spec.experimental {
format!("{} (experimental)", spec.id)
} else {
spec.id.to_string()
});
}
}
lanes
}
pub fn lane(&self, id: &str) -> bool {
match id {
"cpu" => self.cpu,
"gpu" => self.gpu,
"cuda" => self.cuda,
"ane" => self.ane,
"trt" => self.trt,
"openvino" => self.openvino,
"llama" => self.llama,
"worker" => self.worker,
_ => false,
}
}
}
pub fn enabled() -> Enabled {
if let Ok(list) = std::env::var(ACCEL_ENV) {
let has = |name: &str| list.split(',').any(|item| item.trim() == name);
return Enabled {
cpu: has("cpu"),
gpu: has("gpu"),
cuda: has("cuda"),
ane: has("ane"),
trt: has("trt"),
openvino: has("openvino"),
llama: has("llama"),
gpu_beside_ane: has("gpu-beside-ane"),
worker: has("worker"),
source: "set by the environment",
};
}
let saved = crate::home::Settings::load().accelerators;
Enabled {
cpu: saved.cpu.unwrap_or(true),
gpu: saved.gpu.unwrap_or(true),
cuda: saved.cuda.unwrap_or(false),
ane: saved.ane.unwrap_or(true),
trt: saved.trt.unwrap_or(false),
openvino: saved.openvino.unwrap_or(false),
llama: saved.llama.unwrap_or(false),
gpu_beside_ane: saved.gpu_beside_ane.unwrap_or(false),
worker: false,
source: if saved == Switches::default() {
"default"
} else {
"saved"
},
}
}
pub struct Spec {
pub id: &'static str,
pub label: &'static str,
pub variant: &'static str,
pub experimental: bool,
}
pub const SPECS: &[Spec] = &[
Spec {
id: "ane",
label: "Neural Engine",
variant: "fp16-ane",
experimental: false,
},
Spec {
id: "gpu",
label: "GPU",
variant: "fp16-webgpu",
experimental: false,
},
Spec {
id: "cuda",
label: "CUDA",
variant: "fp16-cuda",
experimental: true,
},
Spec {
id: "trt",
label: "TensorRT for RTX",
variant: "fp16-trt",
experimental: true,
},
Spec {
id: "openvino",
label: "OpenVINO",
variant: "openvino",
experimental: true,
},
Spec {
id: "llama",
label: "llama.cpp",
variant: "gguf-f16",
experimental: true,
},
Spec {
id: "worker",
label: "CPU worker",
variant: "int8-cpu",
experimental: false,
},
];
pub fn spec(id: &str) -> Option<&'static Spec> {
SPECS.iter().find(|s| s.id == id)
}
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(tag = "state", rename_all = "lowercase")]
pub enum Status {
Idle,
Starting,
Compiling {
percent: u8,
#[serde(skip_serializing_if = "Option::is_none")]
eta_ms: Option<u64>,
},
Active,
Downloading {
percent: u8,
#[serde(skip_serializing_if = "Option::is_none")]
eta_ms: Option<u64>,
},
Unavailable {
reason: String,
},
Failed {
reason: String,
},
}
enum Job {
Warm,
Batch {
batch: Vec<crate::session::Ids>,
reply: mpsc::Sender<std::result::Result<Vec<Vec<f32>>, String>>,
},
}
pub struct Lane {
pub id: &'static str,
pub experimental: bool,
status: Mutex<Status>,
device: Mutex<Option<String>>,
variant: Mutex<&'static str>,
samples: Mutex<std::collections::VecDeque<(Instant, u64)>>,
chunks: AtomicU64,
jobs: Mutex<Option<mpsc::Sender<Job>>>,
began: Mutex<Option<(Instant, Instant, u8)>>,
expected: Mutex<Option<Duration>>,
}
impl Lane {
fn new(spec: &Spec) -> Self {
Self {
id: spec.id,
experimental: spec.experimental,
status: Mutex::new(Status::Idle),
device: Mutex::new(None),
variant: Mutex::new(spec.variant),
samples: Mutex::new(std::collections::VecDeque::new()),
chunks: AtomicU64::new(0),
jobs: Mutex::new(None),
began: Mutex::new(None),
expected: Mutex::new(None),
}
}
pub fn token_budget(&self) -> (usize, usize, usize) {
match self.id {
"cuda" | "trt" => (2_048, 16_384, 65_536),
"ane" => (1_024, 4_096, 12_288),
_ => (512, 4_096, 16_384),
}
}
pub fn rows_per_call(&self) -> usize {
match self.variant() {
"fp16-ane" => 4,
"fp16-coreml-gpu" => 8,
_ => 1,
}
}
pub fn variant(&self) -> &'static str {
*self.variant.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn status(&self) -> Status {
let status = self
.status
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
let began = *self.began.lock().unwrap_or_else(|e| e.into_inner());
let expected = *self.expected.lock().unwrap_or_else(|e| e.into_inner());
let compiling = matches!(status, Status::Compiling { .. });
let left = || {
let (start, step, at) = began?;
if at == 0 && compiling {
let whole = expected?.as_millis() as u64;
return Some(
whole
.saturating_sub(start.elapsed().as_millis() as u64)
.max(1_000),
);
}
if at == 0 || at >= 100 {
return None;
}
let per = step.duration_since(start).as_millis() as u64 / u64::from(at);
let whole = per * u64::from(100 - at);
Some(
whole
.saturating_sub(step.elapsed().as_millis() as u64)
.max(1_000),
)
};
match status {
Status::Compiling { percent, .. } => Status::Compiling {
percent,
eta_ms: left(),
},
Status::Downloading { percent, .. } => Status::Downloading {
percent,
eta_ms: left(),
},
other => other,
}
}
fn set(&self, status: Status) {
let mut current = self.status.lock().unwrap_or_else(|e| e.into_inner());
let phase = |s: &Status| match s {
Status::Compiling { .. } => 1,
Status::Downloading { .. } => 2,
_ => 0,
};
let (was, now) = (phase(¤t), phase(&status));
let percent = match &status {
Status::Compiling { percent, .. } | Status::Downloading { percent, .. } => *percent,
_ => 0,
};
let mut began = self.began.lock().unwrap_or_else(|e| e.into_inner());
if now != was {
*began = (now != 0).then(|| (Instant::now(), Instant::now(), percent));
} else if let Some((start, _, at)) = *began
&& percent != at
{
*began = Some((start, Instant::now(), percent));
}
drop(began);
*current = status;
}
pub fn coming(&self) -> bool {
matches!(
self.status(),
Status::Idle | Status::Starting | Status::Compiling { .. } | Status::Downloading { .. }
)
}
pub fn usable(&self) -> bool {
!matches!(
self.status(),
Status::Unavailable { .. } | Status::Failed { .. }
)
}
pub fn ready(&self) -> bool {
self.status() == Status::Active
}
pub fn wake(self: &Arc<Self>) {
{
let mut status = self.status.lock().unwrap_or_else(|e| e.into_inner());
if *status != Status::Idle {
return;
}
*status = Status::Starting;
}
self.send(Job::Warm);
}
pub fn reset(&self) {
if matches!(
self.status(),
Status::Failed { .. } | Status::Unavailable { .. }
) {
self.set(Status::Idle);
}
}
pub fn submit(
self: &Arc<Self>,
batch: Vec<crate::session::Ids>,
) -> mpsc::Receiver<std::result::Result<Vec<Vec<f32>>, String>> {
let (reply, answer) = mpsc::channel();
self.send(Job::Batch { batch, reply });
answer
}
fn send(self: &Arc<Self>, job: Job) {
let mut jobs = self.jobs.lock().unwrap_or_else(|e| e.into_inner());
if jobs.is_none() {
let (tx, rx) = mpsc::channel();
let lane = Arc::clone(self);
if std::thread::Builder::new()
.name(format!("semlith-lane-{}", self.id))
.spawn(move || dispatch(lane, rx))
.is_ok()
{
*jobs = Some(tx);
}
}
let refused = match jobs.as_ref() {
Some(tx) => match tx.send(job) {
Ok(()) => None,
Err(mpsc::SendError(job)) => {
*jobs = None;
Some((job, "the lane's dispatcher has gone"))
}
},
None => Some((job, "the lane's dispatcher could not start")),
};
if let Some((Job::Batch { reply, .. }, why)) = refused {
let _ = reply.send(Err(why.into()));
}
}
pub fn count(&self, chunks: usize) {
self.chunks.fetch_add(chunks as u64, Ordering::Relaxed);
let mut samples = self.samples.lock().unwrap_or_else(|e| e.into_inner());
samples.push_back((Instant::now(), self.chunks.load(Ordering::Relaxed)));
while samples
.front()
.is_some_and(|(at, _)| at.elapsed() > Duration::from_secs(10))
&& samples.len() > 1
{
samples.pop_front();
}
}
pub fn rate(&self) -> f64 {
let samples = self.samples.lock().unwrap_or_else(|e| e.into_inner());
let Some(&(last, to)) = samples.back() else {
return 0.0;
};
if last.elapsed() > RATE_IDLE {
return 0.0;
}
let Some(&(since, from)) = samples
.iter()
.rev()
.find(|(at, _)| at.elapsed() >= RATE_WINDOW)
.or(samples.front())
else {
return 0.0;
};
let span = since.elapsed().as_secs_f64();
if span < 0.5 {
return 0.0;
}
(to - from) as f64 / span
}
}
const RATE_WINDOW: Duration = Duration::from_secs(3);
const RATE_IDLE: Duration = Duration::from_millis(1500);
static CPU: OnceLock<Arc<Lane>> = OnceLock::new();
fn cpu_lane() -> &'static Arc<Lane> {
CPU.get_or_init(|| {
Arc::new(Lane::new(&Spec {
id: "cpu",
label: "CPU",
variant: "int8-cpu",
experimental: false,
}))
})
}
pub fn count_cpu(chunks: usize) {
cpu_lane().count(chunks);
}
static LANES: OnceLock<Vec<Arc<Lane>>> = OnceLock::new();
static MANAGED: AtomicBool = AtomicBool::new(false);
pub fn manage() {
MANAGED.store(true, Ordering::Relaxed);
}
pub fn managed() -> bool {
MANAGED.load(Ordering::Relaxed)
}
fn lanes() -> &'static Vec<Arc<Lane>> {
LANES.get_or_init(|| SPECS.iter().map(|spec| Arc::new(Lane::new(spec))).collect())
}
pub fn lane(id: &str) -> Option<&'static Arc<Lane>> {
lanes().iter().find(|lane| lane.id == id)
}
pub fn unavailable_here(id: &str) -> Option<String> {
let apple = cfg!(all(target_os = "macos", target_arch = "aarch64"));
let x86_windows_or_linux = cfg!(all(
target_arch = "x86_64",
any(windows, target_os = "linux")
));
match id {
"ane" if !apple => Some("the Neural Engine lane needs a Mac with Apple silicon".into()),
"cuda" => crate::cuda::unavailable_here(),
"trt" | "openvino" if !x86_windows_or_linux => Some(format!(
"the {} lane is built for Windows and Linux on x86_64",
spec(id).map_or(id, |s| s.label)
)),
"llama" if !(apple || x86_windows_or_linux) => {
Some("no llama.cpp build is pinned for this platform".into())
}
_ => None,
}
}
pub fn for_run() -> (Vec<Arc<Lane>>, bool) {
if !MANAGED.load(Ordering::Relaxed) {
return (Vec::new(), true);
}
let on = enabled();
let mut chosen: Vec<Arc<Lane>> = lanes()
.iter()
.filter(|lane| on.lane(lane.id) && unavailable_here(lane.id).is_none())
.filter(|lane| lane.usable())
.cloned()
.collect();
let mut cpu = on.cpu;
let ane_running = chosen.iter().any(|lane| lane.id == "ane" && lane.ready());
if ane_running {
cpu = false;
if !on.gpu_beside_ane {
chosen.retain(|lane| lane.id != "gpu");
}
}
let any_ready = chosen.iter().any(|lane| lane.ready());
let any_coming = chosen.iter().any(|lane| lane.coming());
(chosen, cpu || (!any_ready && !any_coming))
}
pub fn spell_left(ms: u64) -> String {
let secs = ms.div_ceil(1000);
if secs < 60 {
format!("about {secs} s left")
} else {
format!("about {} min left", secs.div_ceil(60))
}
}
pub fn waiting_for() -> Option<String> {
if !MANAGED.load(Ordering::Relaxed) {
return None;
}
let (lanes, cpu) = for_run();
if cpu || lanes.iter().any(|lane| lane.ready()) {
return None;
}
let lane = lanes.iter().find(|lane| lane.coming())?;
let label = spec(lane.id).map_or(lane.id, |s| s.label);
let left = |eta: Option<u64>| {
eta.map(|ms| format!(", {}", spell_left(ms)))
.unwrap_or_default()
};
Some(match lane.status() {
Status::Compiling { percent, eta_ms } => format!(
"waiting for the {label} lane to compile its models: {percent} %{}",
left(eta_ms)
),
Status::Downloading { percent, eta_ms } => format!(
"waiting for the {label} lane to download: {percent} %{}",
left(eta_ms)
),
_ => format!("waiting for the {label} lane to start"),
})
}
pub fn cache_variants() -> Vec<&'static str> {
let mut out: Vec<&'static str> = Vec::new();
if MANAGED.load(Ordering::Relaxed) {
let on = enabled();
for spec in SPECS {
if spec.id == "worker" || !on.lane(spec.id) || unavailable_here(spec.id).is_some() {
continue;
}
let variants: &[&'static str] = match spec.id {
"gpu" => &["fp16-coreml-gpu", "fp16-webgpu"],
_ => std::slice::from_ref(&spec.variant),
};
for variant in variants {
if !out.contains(variant) {
out.push(variant);
}
}
}
}
out.push("int8-cpu");
out
}
pub fn cache_in_use() -> bool {
(MANAGED.load(Ordering::Relaxed) || std::env::var(crate::cache::CAP_ENV).is_ok())
&& crate::cache::cap_bytes() > 0
}
pub fn snapshot() -> serde_json::Value {
let on = enabled();
let cpu = cpu_lane();
let cache = crate::model_cache_dir().ok();
let mut rows = vec![serde_json::json!({
"lane": "cpu",
"label": "CPU",
"enabled": on.cpu,
"experimental": false,
"status": Status::Active,
"device": crate::system::cpu_name(),
"variant": cpu.variant(),
"rate": round(cpu.rate()),
})];
for lane in lanes() {
let enabled = on.lane(lane.id);
if lane.id == "worker" && !enabled {
continue;
}
let status = match unavailable_here(lane.id) {
Some(reason) => Status::Unavailable { reason },
None => lane.status(),
};
let installed = cache
.as_deref()
.and_then(|cache| pack_for(lane.id).map(|pack| crate::packs::installed(cache, &pack)));
rows.push(serde_json::json!({
"lane": lane.id,
"label": spec(lane.id).map_or(lane.id, |s| s.label),
"enabled": enabled,
"experimental": lane.experimental,
"status": status,
"device": lane.device.lock().unwrap_or_else(|e| e.into_inner()).clone(),
"variant": lane.variant(),
"rate": round(lane.rate()),
"installed": installed.map(|dir| dir.is_some()),
"download_bytes": pack_for(lane.id).map(|pack| pack.bytes()),
}));
}
let total: f64 = rows.iter().filter_map(|row| row["rate"].as_f64()).sum();
for row in &mut rows {
let rate = row["rate"].as_f64().unwrap_or(0.0);
row["share"] = serde_json::json!(if total > 0.0 {
(rate / total * 100.0).round()
} else {
0.0
});
}
let accel_ready = lanes()
.iter()
.any(|lane| lane.id != "worker" && on.lane(lane.id) && lane.ready());
let accel_coming = lanes().iter().any(|lane| {
lane.id != "worker"
&& on.lane(lane.id)
&& unavailable_here(lane.id).is_none()
&& lane.coming()
});
serde_json::json!({
"lanes": rows,
"source": on.source,
"gpu_beside_ane": on.gpu_beside_ane,
"cpu_fallback": !on.cpu && !accel_ready && !accel_coming,
})
}
fn round(value: f64) -> f64 {
(value * 10.0).round() / 10.0
}
pub const SWITCH_NAMES: &str = "cpu, gpu, ane, cuda, trt, openvino, llama, gpu-beside-ane";
pub fn set(lane_id: &str, on: bool) -> Result<String> {
set_with_progress(lane_id, on, &mut |_| {})
}
pub fn set_with_progress(lane_id: &str, on: bool, progress: &mut dyn FnMut(u8)) -> Result<String> {
if std::env::var(ACCEL_ENV).is_ok() {
bail!("{ACCEL_ENV} is set in this process's environment, so the switches cannot change it");
}
let mut settings = crate::home::Settings::load();
match (lane_id, on) {
("cpu", false) => {
let now = enabled();
let can = lanes().iter().any(|l| {
l.id != "worker" && now.lane(l.id) && l.usable() && unavailable_here(l.id).is_none()
}) && (now.ane || detect_gpu().is_ok());
if !can {
bail!(
"the CPU cannot be turned off: no accelerator lane is on and usable here ({}), \
so the CPU is what indexes",
detect_gpu()
.err()
.unwrap_or_else(|| "the GPU lane is off".to_string())
);
}
settings.accelerators.cpu = Some(false);
}
("cpu", true) => settings.accelerators.cpu = Some(true),
("gpu", _) => settings.accelerators.gpu = Some(on),
("gpu-beside-ane", _) => settings.accelerators.gpu_beside_ane = Some(on),
(id @ ("ane" | "cuda" | "trt" | "openvino" | "llama"), _) => {
if on && let Some(why) = unavailable_here(id) {
bail!("{why}");
}
if on && let Some(pack) = pack_for(id) {
let cache = crate::model_cache_dir()?;
crate::packs::fetch(&cache, &pack, progress)
.with_context(|| format!("fetching the {} pack", pack.name))?;
}
let slot = match id {
"ane" => &mut settings.accelerators.ane,
"cuda" => &mut settings.accelerators.cuda,
"trt" => &mut settings.accelerators.trt,
"openvino" => &mut settings.accelerators.openvino,
_ => &mut settings.accelerators.llama,
};
*slot = Some(on);
}
(other, _) => bail!("there is no lane called {other}; the switches are {SWITCH_NAMES}"),
}
settings.save()?;
if on && let Some(found) = lane(lane_id) {
found.reset();
}
let note = spec(lane_id)
.filter(|s| s.experimental && on)
.map(|s| {
format!(
" ({} is experimental: built and checked without its hardware, and not measured on it)",
s.label
)
})
.unwrap_or_default();
Ok(format!(
"{lane_id} {} — runs pick it up at their next batch{note}",
if on { "on" } else { "off" }
))
}
pub fn set_in_background(lane_id: &str) -> Result<String> {
let needs_fetch = pack_for(lane_id).is_some_and(|pack| {
crate::model_cache_dir()
.ok()
.is_some_and(|cache| crate::packs::installed(&cache, &pack).is_none())
});
if !needs_fetch {
return set(lane_id, true);
}
if std::env::var(ACCEL_ENV).is_ok() {
bail!("{ACCEL_ENV} is set in this process's environment, so the switches cannot change it");
}
if let Some(why) = unavailable_here(lane_id) {
bail!("{why}");
}
let Some(found) = lane(lane_id) else {
bail!("there is no lane called {lane_id}; the switches are {SWITCH_NAMES}");
};
found.set(Status::Downloading {
percent: 0,
eta_ms: None,
});
let id = lane_id.to_string();
let lane = Arc::clone(found);
std::thread::spawn(move || {
let fetched = set_with_progress(&id, true, &mut |percent| {
lane.set(Status::Downloading {
percent,
eta_ms: None,
})
});
match fetched {
Ok(_) => lane.set(Status::Idle),
Err(e) => lane.set(Status::Failed {
reason: format!("{e:#}"),
}),
}
});
Ok(format!(
"{lane_id}: fetching its components; it turns on when they have arrived"
))
}
pub fn pack_for(id: &str) -> Option<crate::packs::Pack> {
match id {
"ane" => Some(crate::packs::coreml()),
"trt" => Some(crate::trt::pack()),
"openvino" => Some(crate::openvino::pack()),
"llama" => Some(crate::llama::pack()),
_ => None,
}
}
pub fn remove(lane_id: &str) -> Result<u64> {
let cache = crate::model_cache_dir()?;
if let Some(pack) = pack_for(lane_id) {
let worker = if lane_id == "ane" {
remove_coreml_worker(&cache)
} else {
0
};
return Ok(crate::packs::remove(&cache, &pack)? + worker);
}
let dir = match lane_id {
"gpu" => component_dir(&cache, &format!("webgpu-{}", crate::gpu::WEBGPU_VERSION)),
"cuda" => crate::gpu::cuda_dir(&cache),
other => bail!(
"{other} has nothing downloaded to remove; the lanes with components are gpu, ane, \
cuda, trt, openvino and llama"
),
};
let bytes = dir_bytes(&dir);
if dir.exists() {
std::fs::remove_dir_all(&dir)
.with_context(|| format!("removing {}", crate::plain(&dir.display().to_string())))?;
}
Ok(bytes)
}
fn remove_coreml_worker(cache: &Path) -> u64 {
let dir = component_dir(cache, &format!("coreml-worker-v{COREML_WORKER}"));
let bytes = dir_bytes(&dir);
let _ = std::fs::remove_dir_all(&dir);
bytes
}
pub fn component_bytes() -> serde_json::Value {
let Ok(cache) = crate::model_cache_dir() else {
return serde_json::json!({});
};
let mut out = serde_json::json!({
"gpu": dir_bytes(&component_dir(&cache, &format!("webgpu-{}", crate::gpu::WEBGPU_VERSION))),
"cuda": dir_bytes(&crate::gpu::cuda_dir(&cache)),
"cuda_download": crate::gpu::cuda_pack_bytes(),
});
for id in ["ane", "trt", "openvino", "llama"] {
if let Some(pack) = pack_for(id) {
out[id] = serde_json::json!(dir_bytes(&pack.dir(&cache)));
out[format!("{id}_download")] = serde_json::json!(pack.bytes());
}
}
out
}
pub(crate) fn dir_bytes(dir: &Path) -> u64 {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.map(|entry| {
let path = entry.path();
if path.is_dir() {
dir_bytes(&path)
} else {
entry.metadata().map(|m| m.len()).unwrap_or(0)
}
})
.sum()
}
fn deadline() -> Duration {
std::env::var(DEADLINE_ENV)
.ok()
.and_then(|v| v.parse().ok())
.map(Duration::from_millis)
.unwrap_or(BATCH_DEADLINE)
}
struct Worker {
child: std::process::Child,
stdin: Option<std::process::ChildStdin>,
answers: mpsc::Receiver<std::result::Result<Vec<u8>, String>>,
}
impl Drop for Worker {
fn drop(&mut self) {
drop(self.stdin.take());
let deadline = Instant::now() + Duration::from_secs(2);
while Instant::now() < deadline {
if let Ok(Some(_)) = self.child.try_wait() {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
let _ = self.child.kill();
let _ = self.child.wait();
}
}
struct Sent {
rows: usize,
reply: mpsc::Sender<std::result::Result<Vec<Vec<f32>>, String>>,
}
fn dispatch(lane: Arc<Lane>, jobs: mpsc::Receiver<Job>) {
let mut worker: Option<Worker> = None;
let mut sent: std::collections::VecDeque<Sent> = std::collections::VecDeque::new();
let fail = |lane: &Lane,
worker: &mut Option<Worker>,
sent: &mut std::collections::VecDeque<Sent>,
reason: String| {
*worker = None;
lane.set(Status::Failed {
reason: reason.clone(),
});
for batch in sent.drain(..) {
let _ = batch.reply.send(Err(reason.clone()));
}
};
loop {
let job = if sent.is_empty() {
match jobs.recv_timeout(WORKER_IDLE) {
Ok(job) => Some(job),
Err(mpsc::RecvTimeoutError::Timeout) => {
if worker.is_some() && matches!(lane.id, "ane" | "gpu") && coreml_compiling() {
continue;
}
if worker.take().is_some() && lane.status() == Status::Active {
lane.set(Status::Idle);
}
continue;
}
Err(mpsc::RecvTimeoutError::Disconnected) => return,
}
} else if sent.len() < WORKER_DEPTH {
jobs.try_recv().ok()
} else {
None
};
if let Some(job) = job {
let refuse = |job: Job, why: String| {
if let Job::Batch { reply, .. } = job {
let _ = reply.send(Err(why));
}
};
if !lane.usable() {
refuse(job, reason_of(&lane.status()));
continue;
}
if worker.is_none() {
lane.set(Status::Starting);
match start(&lane) {
Ok((started, _)) => {
worker = Some(started);
lane.set(Status::Active);
}
Err(e) => {
let reason = format!("{e:#}");
if let Some(why) = reason.strip_prefix("unavailable — ") {
lane.set(Status::Unavailable {
reason: why.to_string(),
});
} else {
lane.set(Status::Failed {
reason: reason.clone(),
});
}
refuse(job, reason);
continue;
}
}
}
let Job::Batch { batch, reply } = job else {
continue;
};
let running = worker.as_mut().expect("started above");
let written = match running.stdin.as_mut() {
Some(stdin) => write_frame(stdin, &encode_ids(&batch)),
None => Err(std::io::Error::other("the worker is being let go")),
};
if let Err(e) = written {
let _ = reply.send(Err(format!("the worker stopped reading: {e}")));
fail(
&lane,
&mut worker,
&mut sent,
format!("the worker stopped reading: {e}"),
);
continue;
}
sent.push_back(Sent {
rows: batch.len(),
reply,
});
if sent.len() < WORKER_DEPTH {
continue;
}
}
let Some(oldest) = sent.front() else {
continue;
};
let running = worker.as_mut().expect("a batch in flight has a worker");
let answer = match running.answers.recv_timeout(deadline()) {
Ok(Ok(frame)) => decode_vectors(&frame, oldest.rows),
Ok(Err(e)) => Err(anyhow::anyhow!("the worker exited: {e}")),
Err(_) => Err(anyhow::anyhow!(
"the worker did not answer a batch within {} s",
deadline().as_secs_f32()
)),
};
match answer {
Ok(vectors) => {
let done = sent.pop_front().expect("checked above");
lane.count(vectors.len());
let _ = done.reply.send(Ok(vectors));
}
Err(e) => fail(&lane, &mut worker, &mut sent, format!("{e:#}")),
}
}
}
fn reason_of(status: &Status) -> String {
match status {
Status::Unavailable { reason } | Status::Failed { reason } => reason.clone(),
other => format!("{other:?}"),
}
}
pub fn check_all(say: impl Fn(&str)) -> Vec<serde_json::Value> {
let mut out = Vec::new();
say("checking the CPU lane");
let cpu = (|| -> Result<Check> {
let cache = crate::model_cache_dir()?;
let mut model =
crate::embed::Model::Granite.load(cache, crate::chunk::MAX_CHARS / 2, true)?;
known_answer("cpu", &crate::system::cpu_name(), "int8-cpu", |texts| {
let mut got = model
.embed(texts, Some(1))
.map_err(|e| anyhow::anyhow!("{e}"))?;
for vector in &mut got {
crate::normalize(vector);
}
Ok(got)
})
})();
out.push(match cpu {
Ok(check) => serde_json::to_value(check).unwrap_or_default(),
Err(e) => serde_json::json!({ "lane": "cpu", "passed": false, "reason": format!("{e:#}") }),
});
let on = enabled();
for spec in SPECS {
let id = spec.id;
let base = serde_json::json!({
"lane": id,
"label": spec.label,
"experimental": spec.experimental,
});
let row = |extra: serde_json::Value| {
let mut row = base.clone();
if let (Some(row), Some(extra)) = (row.as_object_mut(), extra.as_object()) {
row.extend(extra.clone());
}
row
};
if let Some(why) = unavailable_here(id) {
if id != "worker" {
out.push(row(serde_json::json!({
"reason": format!("unavailable — {why}"),
"fallback": "the CPU carries the run",
})));
}
continue;
}
if !on.lane(id) {
if id != "worker" {
out.push(row(serde_json::json!({
"reason": format!("off — semlith accel on {id} turns it on"),
})));
}
continue;
}
say(&format!("checking the {} lane", spec.label));
let lane = Arc::new(Lane::new(spec));
out.push(match start(&lane) {
Ok((_, hello)) => row(serde_json::json!({
"device": hello["device"],
"variant": hello["variant"],
"cosine": hello["cosine"],
"chunks_per_s": hello["chunks_per_s"],
"passed": true,
})),
Err(e) => {
let text = format!("{e:#}");
match text.strip_prefix("unavailable — ") {
Some(reason) => row(serde_json::json!({
"reason": format!("unavailable — {reason}"),
"fallback": "the CPU carries the run",
})),
None => row(serde_json::json!({
"passed": false,
"reason": text,
"fallback": "the lane is failed and the CPU carries the run",
})),
}
}
});
}
out
}
fn adapter_choice() -> Option<String> {
std::env::var(crate::gpu::ADAPTER_ENV)
.ok()
.or_else(|| crate::home::Settings::load().gpu_adapter)
.filter(|name| !name.trim().is_empty())
}
fn worker_args(lane: &Arc<Lane>) -> Result<Vec<String>> {
if let Some(why) = unavailable_here(lane.id) {
bail!("unavailable — {why}");
}
let cache = crate::model_cache_dir()?;
let progress = |percent: u8| {
lane.set(Status::Downloading {
percent,
eta_ms: None,
})
};
Ok(match lane.id {
"ane" => {
let Some(pack) = crate::packs::installed(&cache, &crate::packs::coreml()) else {
bail!(
"unavailable — the Neural Engine pack is not installed; `semlith accel on ane` \
or `semlith setup` fetches it"
);
};
vec!["ane".into(), pack.display().to_string()]
}
"gpu" => {
let device = detect_gpu().map_err(|why| anyhow::anyhow!("unavailable — {why}"))?;
*lane.device.lock().unwrap_or_else(|e| e.into_inner()) = Some(device);
if cfg!(target_os = "macos")
&& let Some(pack) = crate::packs::installed(&cache, &crate::packs::coreml())
{
vec!["gpu-coreml".into(), pack.display().to_string()]
} else {
let dir = crate::gpu::fetch_webgpu(&cache, &mut |p| progress(p))
.context("fetching the WebGPU components")?;
let mut args = vec!["gpu".into(), dir.display().to_string()];
if let Some(name) = adapter_choice() {
args.push(name);
}
args
}
}
"cuda" => {
if let Some(why) = crate::cuda::unavailable_here() {
bail!("unavailable — {why}");
}
crate::gpu::fetch_fp16(&cache, &mut |p| progress(p))
.context("fetching the fp16 model")?;
let pack = crate::cuda::fetch_pack(&cache, &mut |p| progress(p))
.context("fetching the CUDA pack")?;
if let Ok(device) = crate::cuda::detect() {
*lane.device.lock().unwrap_or_else(|e| e.into_inner()) = Some(device.name);
}
vec!["cuda".into(), pack.display().to_string()]
}
id @ ("trt" | "openvino" | "llama") => {
let pack = pack_for(id).expect("these lanes have packs");
if id != "llama" {
crate::gpu::fetch_fp16(&cache, &mut |p| progress(p))
.context("fetching the fp16 model")?;
}
let dir = crate::packs::fetch(&cache, &pack, &mut |p| progress(p))
.with_context(|| format!("fetching the {} pack", pack.name))?;
vec![id.into(), dir.display().to_string()]
}
_ => vec!["worker".into()],
})
}
pub const COREML_WORKER: u32 = 2;
pub const COREML_WORKER_ENV: &str = "SEMLITH_COREML_WORKER";
pub fn coreml_worker(cache: &Path) -> Result<PathBuf> {
let current = std::env::current_exe().context("locating this binary")?;
if std::env::var(COREML_WORKER_ENV).is_ok_and(|v| v == "current") {
return Ok(current);
}
let dir = component_dir(cache, &format!("coreml-worker-v{COREML_WORKER}"));
let exe = dir.join(if cfg!(windows) {
"semlith.exe"
} else {
"semlith"
});
if exe.is_file() {
return Ok(exe);
}
std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
let temp = dir.join(format!(".semlith.{}", std::process::id()));
let _ = std::fs::remove_file(&temp);
if std::fs::hard_link(¤t, &temp).is_err() {
std::fs::copy(¤t, &temp)
.with_context(|| format!("copying this binary to {}", temp.display()))?;
}
std::fs::rename(&temp, &exe).with_context(|| format!("placing {}", exe.display()))?;
if let Ok(entries) = std::fs::read_dir(cache.join("accel")) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if name.starts_with("coreml-worker-v") && entry.path() != dir {
let _ = std::fs::remove_dir_all(entry.path());
}
}
}
Ok(exe)
}
const E5_CACHE: &str = "Library/Caches/semlith/com.apple.e5rt.e5bundlecache";
fn sweep_abandoned_compiles(root: &Path) {
let within = |dir: &Path| {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.flatten()
.map(|entry| entry.path())
.collect::<Vec<_>>()
};
for build in within(root) {
for key in within(&build) {
for bundle in within(&key) {
let name = bundle.file_name().unwrap_or_default().to_string_lossy();
let pid = name
.split_once(".tmp.")
.and_then(|(_, rest)| rest.split('_').next())
.and_then(|pid| pid.parse::<u32>().ok());
if pid.is_some_and(|pid| !crate::daemon::alive(pid)) {
let _ = std::fs::remove_dir_all(&bundle);
}
}
}
}
}
pub fn coreml_compile_lock() -> Option<std::fs::File> {
let dir = crate::model_cache_dir().ok()?.join("accel");
std::fs::create_dir_all(&dir).ok()?;
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(dir.join("coreml-compile.lock"))
.ok()?;
file.lock().ok()?;
Some(file)
}
fn coreml_compiling() -> bool {
cfg!(target_os = "macos")
&& crate::model_cache_dir().is_ok_and(|cache| {
std::fs::OpenOptions::new()
.write(true)
.open(cache.join("accel").join("coreml-compile.lock"))
.is_ok_and(|file| matches!(file.try_lock(), Err(std::fs::TryLockError::WouldBlock)))
})
}
fn start(lane: &Arc<Lane>) -> Result<(Worker, serde_json::Value)> {
let mut args = vec!["__embed-worker".to_string()];
args.extend(worker_args(lane)?);
lane.set(Status::Starting);
let exe = if cfg!(target_os = "macos") && matches!(args[1].as_str(), "ane" | "gpu-coreml") {
if let Ok(home) = crate::home::user_home() {
sweep_abandoned_compiles(&home.join(E5_CACHE));
}
coreml_worker(&crate::model_cache_dir()?)?
} else {
std::env::current_exe().context("locating this binary")?
};
let mut child = std::process::Command::new(exe)
.args(&args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.context("starting the embed worker")?;
let stdin = child.stdin.take().context("the worker has no stdin")?;
let mut stdout = child.stdout.take().context("the worker has no stdout")?;
let (tx, answers) = mpsc::channel();
std::thread::spawn(move || {
loop {
let frame = read_frame(&mut stdout).map_err(|e| e.to_string());
let end = frame.is_err();
if tx.send(frame).is_err() || end {
return;
}
}
});
let worker = Worker {
child,
stdin: Some(stdin),
answers,
};
let mut started = Instant::now();
let began = Instant::now();
let timed = crate::model_cache_dir()
.ok()
.map(|cache| component_dir(&cache, &format!("{}.compile-ms", lane.id)));
*lane.expected.lock().unwrap_or_else(|e| e.into_inner()) = timed
.as_deref()
.and_then(|path| std::fs::read_to_string(path).ok())
.and_then(|ms| ms.trim().parse().ok())
.map(Duration::from_millis)
.or((lane.id == "ane").then_some(FIRST_COMPILE));
let mut compiled = None;
let hello = loop {
if began.elapsed() > START_CAP {
bail!(
"the worker was not ready within {} min",
START_CAP.as_secs() / 60
);
}
let left = HELLO_DEADLINE.saturating_sub(started.elapsed());
let frame = match worker.answers.recv_timeout(left) {
Ok(Ok(frame)) => frame,
Ok(Err(e)) => bail!("the worker exited before it was ready: {e}"),
Err(_) => bail!(
"the worker was not ready within {} s",
HELLO_DEADLINE.as_secs()
),
};
let value: serde_json::Value =
serde_json::from_slice(&frame).context("the worker's hello")?;
if value["waiting"].as_bool() == Some(true) {
started = Instant::now();
continue;
}
if let (Some(done), Some(of)) = (value["loaded"].as_u64(), value["of"].as_u64()) {
started = Instant::now();
compiled.get_or_insert(started);
lane.set(Status::Compiling {
percent: (done * 100 / of.max(1)).min(100) as u8,
eta_ms: None,
});
continue;
}
break value;
};
if let (Some(since), Some(path)) = (compiled, timed)
&& since.elapsed() >= Duration::from_secs(10)
{
let _ = std::fs::write(path, since.elapsed().as_millis().to_string());
}
if hello["ok"].as_bool() != Some(true) {
let reason = hello["reason"].as_str().unwrap_or("no reason given");
if hello["unavailable"].as_bool() == Some(true) {
bail!("unavailable — {reason}");
}
bail!("{reason}");
}
if let Some(device) = hello["device"].as_str() {
*lane.device.lock().unwrap_or_else(|e| e.into_inner()) = Some(device.to_string());
}
if let Some(variant) = hello["variant"].as_str().and_then(static_variant) {
*lane.variant.lock().unwrap_or_else(|e| e.into_inner()) = variant;
}
Ok((worker, hello))
}
fn static_variant(name: &str) -> Option<&'static str> {
[
"int8-cpu",
"fp16-webgpu",
"fp16-coreml-gpu",
"fp16-ane",
"fp16-cuda",
"fp16-trt",
"openvino",
"gguf-f16",
]
.into_iter()
.find(|v| *v == name)
}
pub fn write_frame(out: &mut impl Write, body: &[u8]) -> std::io::Result<()> {
out.write_all(&(body.len() as u32).to_le_bytes())?;
out.write_all(body)?;
out.flush()
}
pub fn read_frame(input: &mut impl Read) -> std::io::Result<Vec<u8>> {
let mut len = [0u8; 4];
input.read_exact(&mut len)?;
let len = u32::from_le_bytes(len) as usize;
if len > 64 << 20 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("a frame of {len} bytes"),
));
}
let mut body = vec![0u8; len];
input.read_exact(&mut body)?;
Ok(body)
}
pub fn encode_ids(batch: &[crate::session::Ids]) -> Vec<u8> {
let words = 1 + batch.iter().map(|ids| 1 + ids.len()).sum::<usize>();
let mut out = Vec::with_capacity(words * 4);
out.extend_from_slice(&(batch.len() as u32).to_le_bytes());
for ids in batch {
out.extend_from_slice(&(ids.len() as u32).to_le_bytes());
for id in ids {
out.extend_from_slice(&id.to_le_bytes());
}
}
out
}
pub fn decode_ids(frame: &[u8]) -> Result<Vec<crate::session::Ids>> {
let words: Vec<u32> = frame
.as_chunks::<4>()
.0
.iter()
.map(|b| u32::from_le_bytes(*b))
.collect();
if !frame.len().is_multiple_of(4) || words.is_empty() {
bail!("a request frame of {} bytes", frame.len());
}
let rows = words[0] as usize;
let mut at = 1;
let mut batch = Vec::with_capacity(rows.min(4096));
for _ in 0..rows {
let len = *words.get(at).context("a request frame cut short")? as usize;
let ids = words
.get(at + 1..at + 1 + len)
.context("a request frame cut short")?;
batch.push(ids.to_vec());
at += 1 + len;
}
if at != words.len() {
bail!("a request frame with {} words left over", words.len() - at);
}
Ok(batch)
}
fn encode_vectors(vectors: &[Vec<f32>]) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + vectors.len() * DIM * 4);
out.push(0);
for vector in vectors {
for value in vector {
out.extend_from_slice(&value.to_le_bytes());
}
}
out
}
fn encode_error(why: &str) -> Vec<u8> {
let mut out = vec![1];
out.extend_from_slice(why.as_bytes());
out
}
fn decode_vectors(frame: &[u8], expected: usize) -> Result<Vec<Vec<f32>>> {
match frame.first() {
Some(0) => {}
Some(1) => bail!("{}", String::from_utf8_lossy(&frame[1..])),
_ => bail!("the worker sent a frame it should not have"),
}
let body = &frame[1..];
if body.len() != expected * DIM * 4 {
bail!(
"the worker sent {} bytes for {expected} vectors",
body.len()
);
}
Ok(vectors_of(body))
}
fn vectors_of(bytes: &[u8]) -> Vec<Vec<f32>> {
let values: Vec<f32> = bytes
.as_chunks::<4>()
.0
.iter()
.map(|b| f32::from_le_bytes(*b))
.collect();
values.chunks(DIM).map(<[f32]>::to_vec).collect()
}
pub fn fixture() -> (Vec<String>, Vec<Vec<f32>>) {
let texts: Vec<String> = serde_json::from_str::<serde_json::Value>(FIXTURE_TEXTS)
.ok()
.and_then(|v| {
v["texts"].as_array().map(|list| {
list.iter()
.filter_map(|t| t.as_str().map(str::to_string))
.collect()
})
})
.unwrap_or_default();
let vectors = vectors_of(FIXTURE_VECTORS);
(texts, vectors)
}
#[derive(Debug, Clone, Serialize)]
pub struct Check {
pub lane: String,
pub device: String,
pub variant: String,
pub cosine: f32,
pub chunks_per_s: f64,
pub passed: bool,
}
pub fn known_answer(
lane: &str,
device: &str,
variant: &str,
mut embed: impl FnMut(&[String]) -> Result<Vec<Vec<f32>>>,
) -> Result<Check> {
let (texts, expected) = fixture();
let started = Instant::now();
let mut worst = 1.0f32;
for (text, want) in texts.iter().zip(&expected) {
let got = embed(std::slice::from_ref(text))?;
let got = got.first().context("no vector came back")?;
worst = worst.min(crate::index::cosine(got, want));
}
let elapsed = started.elapsed().as_secs_f64().max(1e-6);
let floor = if variant == "int8-cpu" {
MIN_COSINE_INT8
} else {
MIN_COSINE_FP16
};
Ok(Check {
lane: lane.to_string(),
device: device.to_string(),
variant: variant.to_string(),
cosine: worst,
chunks_per_s: (texts.len() as f64 / elapsed * 10.0).round() / 10.0,
passed: worst >= floor,
})
}
pub fn worker_main(lane: &str, dir: Option<&Path>, adapter: Option<&str>) -> i32 {
let mut out = std::io::stdout().lock();
let mut input = std::io::stdin().lock();
let say = |out: &mut std::io::StdoutLock, value: serde_json::Value| {
let _ = write_frame(out, &serde_json::to_vec(&value).unwrap_or_default());
};
crate::priority::worker();
let fault = Fault::read(lane);
if matches!(fault, Some(Fault::Load)) {
say(
&mut out,
serde_json::json!({ "ok": false, "reason": format!("{FAULT_ENV} failed the load") }),
);
return 1;
}
let loading = std::sync::Arc::new(AtomicBool::new(true));
{
let beat = std::sync::Arc::clone(&loading);
std::thread::spawn(move || {
while beat.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_secs(30));
if beat.load(Ordering::Relaxed) {
let frame = serde_json::to_vec(&serde_json::json!({ "waiting": true }))
.unwrap_or_default();
let _ = write_frame(&mut std::io::stdout().lock(), &frame);
}
}
});
}
let opened = {
let mut progress = |loaded: usize, of: usize| {
say(
&mut std::io::stdout().lock(),
serde_json::json!({ "loaded": loaded, "of": of }),
);
};
crate::gpu::Session::open(lane, dir, adapter, &mut progress)
};
loading.store(false, Ordering::Relaxed);
let mut session = match opened {
Ok(session) => session,
Err(e) => {
let text = format!("{e:#}");
let unavailable = text.starts_with("unavailable");
say(
&mut out,
serde_json::json!({
"ok": false,
"unavailable": unavailable,
"reason": text.trim_start_matches("unavailable — "),
}),
);
return 1;
}
};
let tokenizer = crate::model_cache_dir().and_then(|cache| crate::session::tokenizer(&cache));
let check = tokenizer.and_then(|tokenizer| {
known_answer(lane, &session.device(), session.variant(), |texts| {
let batch = texts
.iter()
.map(|text| crate::session::encode(&tokenizer, text))
.collect::<Result<Vec<_>>>()?;
let rows: Vec<&[u32]> = batch.iter().map(Vec::as_slice).collect();
session.embed(&rows)
})
});
match check {
Ok(check) if check.passed => say(
&mut out,
serde_json::json!({
"ok": true,
"device": check.device,
"variant": check.variant,
"cosine": check.cosine,
"chunks_per_s": check.chunks_per_s,
}),
),
Ok(check) => {
say(
&mut out,
serde_json::json!({
"ok": false,
"reason": format!(
"{} failed the known-answer check: cosine {:.4} against the fp32 fixture",
check.device, check.cosine
),
}),
);
return 1;
}
Err(e) => {
say(
&mut out,
serde_json::json!({ "ok": false, "reason": format!("the known-answer check failed: {e:#}") }),
);
return 1;
}
}
let mut batches = 0u64;
loop {
let Ok(frame) = read_frame(&mut input) else {
return 0;
};
batches += 1;
match fault {
Some(Fault::Batch(n)) if batches == n => {
let _ = write_frame(
&mut out,
&encode_error(&format!("{FAULT_ENV} failed batch {n}")),
);
return 1;
}
Some(Fault::Hang(ms)) => std::thread::sleep(Duration::from_millis(ms)),
_ => {}
}
let answer = match decode_ids(&frame).and_then(|batch| {
let rows: Vec<&[u32]> = batch.iter().map(Vec::as_slice).collect();
session.embed(&rows)
}) {
Ok(mut vectors) => {
if let (Some(Fault::Poison(value)), Some(first)) = (fault, vectors.first_mut()) {
first.iter_mut().for_each(|v| *v = value);
}
encode_vectors(&vectors)
}
Err(e) => encode_error(&format!("{e:#}")),
};
if write_frame(&mut out, &answer).is_err() {
return 0;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Fault {
Load,
Batch(u64),
Hang(u64),
Poison(f32),
}
impl Fault {
fn read(lane: &str) -> Option<Self> {
let raw = std::env::var(FAULT_ENV).ok()?;
let mut parts = raw.split(':');
if parts.next()? != lane {
return None;
}
match (parts.next()?, parts.next()) {
("load", _) => Some(Fault::Load),
("batch", Some(n)) => n.parse().ok().map(Fault::Batch),
("hang", Some(ms)) => ms.parse().ok().map(Fault::Hang),
("poison", Some("zero")) => Some(Fault::Poison(0.0)),
("poison", Some("nan")) => Some(Fault::Poison(f32::NAN)),
("poison", Some("inf")) => Some(Fault::Poison(f32::INFINITY)),
_ => None,
}
}
}
fn detect_gpu() -> std::result::Result<String, String> {
crate::gpu::detect()
}
pub fn component_dir(cache: &Path, lane: &str) -> PathBuf {
cache.join("accel").join(lane)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_core_ml_worker_is_made_once_and_kept() {
let cache = tempfile::tempdir().unwrap();
let old = component_dir(cache.path(), "coreml-worker-v0");
std::fs::create_dir_all(&old).unwrap();
let made = coreml_worker(cache.path()).unwrap();
assert!(made.is_file());
assert!(made.starts_with(cache.path()));
assert_eq!(
made.file_name()
.unwrap()
.to_string_lossy()
.trim_end_matches(".exe"),
"semlith"
);
assert!(
!old.exists(),
"the earlier protocol's worker is still there"
);
let modified = std::fs::metadata(&made).unwrap().modified().unwrap();
let again = coreml_worker(cache.path()).unwrap();
assert_eq!(again, made);
assert_eq!(
std::fs::metadata(&again).unwrap().modified().unwrap(),
modified
);
}
#[test]
fn a_compiling_lane_is_waited_for_and_its_time_left_counts_down() {
let lane = Lane::new(spec("ane").unwrap());
lane.set(Status::Compiling {
percent: 0,
eta_ms: None,
});
let began = std::time::Instant::now();
assert!(lane.coming() && !lane.ready());
*lane.expected.lock().unwrap() = Some(Duration::from_secs(40));
let eta = |lane: &Lane| match lane.status() {
Status::Compiling { eta_ms, .. } => eta_ms.unwrap(),
other => panic!("{other:?}"),
};
assert!((39_000..=40_000).contains(&eta(&lane)));
std::thread::sleep(Duration::from_millis(1100));
assert!(eta(&lane) <= 39_000);
let taken = began.elapsed().as_millis() as u64;
lane.set(Status::Compiling {
percent: 50,
eta_ms: None,
});
let first = eta(&lane);
let slack = began.elapsed().as_millis() as u64 - taken + 50;
assert!(
(taken.saturating_sub(slack).max(1_000)..=taken + slack).contains(&first),
"{first} after {taken} ms"
);
std::thread::sleep(Duration::from_millis(300));
assert!(eta(&lane) <= first);
lane.set(Status::Failed { reason: "x".into() });
assert!(!lane.coming());
lane.set(Status::Active);
assert!(lane.ready() && !lane.coming());
}
#[cfg(unix)]
#[test]
fn abandoned_compiles_are_swept_and_nothing_else() {
let root = tempfile::tempdir().unwrap();
let key = root.path().join("26A428").join("3BADE4");
let bundle = |name: &str| {
let path = key.join(name);
std::fs::create_dir_all(path.join("H13G.bundle")).unwrap();
path
};
let dead = bundle("E3A0.tmp.999999999_1.bundle");
let live = bundle(&format!("E3A0.tmp.{}_1.bundle", std::process::id()));
let done = bundle("E3A0.bundle");
sweep_abandoned_compiles(root.path());
assert!(!dead.exists());
assert!(live.exists() && done.exists());
}
#[test]
fn frames_round_trip_and_refuse_a_wrong_length() {
let mut buffer = Vec::new();
write_frame(&mut buffer, b"hello").unwrap();
assert_eq!(read_frame(&mut buffer.as_slice()).unwrap(), b"hello");
let vectors = vec![vec![0.5f32; DIM], vec![-1.0f32; DIM]];
let frame = encode_vectors(&vectors);
assert_eq!(decode_vectors(&frame, 2).unwrap(), vectors);
assert!(
decode_vectors(&frame, 3).is_err(),
"a short answer is refused"
);
let error = encode_error("the driver went away");
assert_eq!(
decode_vectors(&error, 1).unwrap_err().to_string(),
"the driver went away"
);
}
#[test]
fn id_frames_round_trip_and_refuse_a_bad_length() {
let batch = vec![vec![1u32, 2, 3], vec![], vec![70_000]];
assert_eq!(decode_ids(&encode_ids(&batch)).unwrap(), batch);
let mut cut = encode_ids(&batch);
cut.truncate(cut.len() - 4);
assert!(decode_ids(&cut).is_err());
let mut long = encode_ids(&batch);
long.extend_from_slice(&9u32.to_le_bytes());
assert!(decode_ids(&long).is_err());
}
#[test]
fn the_fixture_is_32_normalised_vectors() {
let (texts, vectors) = fixture();
assert_eq!(texts.len(), 32);
assert_eq!(vectors.len(), 32);
for vector in &vectors {
assert_eq!(vector.len(), DIM);
let norm: f32 = vector.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!((norm - 1.0).abs() < 1e-3, "norm {norm}");
}
}
#[test]
fn faults_are_read_for_their_own_lane_only() {
unsafe { std::env::set_var(FAULT_ENV, "worker:batch:3") };
assert_eq!(Fault::read("worker"), Some(Fault::Batch(3)));
assert_eq!(Fault::read("gpu"), None);
unsafe { std::env::set_var(FAULT_ENV, "gpu:hang:60000") };
assert_eq!(Fault::read("gpu"), Some(Fault::Hang(60000)));
unsafe { std::env::set_var(FAULT_ENV, "ane:poison:zero") };
assert_eq!(Fault::read("ane"), Some(Fault::Poison(0.0)));
unsafe { std::env::remove_var(FAULT_ENV) };
}
}