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);
static WARM_UNTIL: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub fn keep_warm(for_: Duration) {
let until = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
+ for_.as_secs();
WARM_UNTIL.fetch_max(until, std::sync::atomic::Ordering::Relaxed);
}
pub const WATCH_WARM: Duration = Duration::from_secs(600);
pub fn prewarm() {
if !MANAGED.load(Ordering::Relaxed) {
return;
}
keep_warm(WATCH_WARM);
let on = enabled();
for lane in lanes() {
if on.lane(lane.id) && unavailable_here(lane.id).is_none() && lane.usable() {
lane.wake();
}
}
}
fn kept_warm() -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
WARM_UNTIL.load(std::sync::atomic::Ordering::Relaxed) > now
}
const WORKER_DEPTH: usize = 2;
fn depth(lane: &Lane) -> usize {
if is_remote(lane.id) { 8 } else { WORKER_DEPTH }
}
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, 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>,
pub remote: Option<bool>,
#[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub remotes: std::collections::BTreeMap<String, 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 remotes: Vec<String>,
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 (id, spec) in lane_specs() {
if id != "worker" && self.lane(id) {
lanes.push(if spec.experimental {
format!("{id} (experimental)")
} else {
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,
id => is_remote(id) && self.remotes.iter().any(|on| on == id),
}
}
}
pub const CPU_ALWAYS_ON: &str = "the CPU lane is always on; use the CPU cap to limit it";
fn on_by_default(id: &str) -> bool {
matches!(id, "cpu" | "gpu" | "ane")
}
impl Switches {
fn get(&self, id: &str) -> Option<bool> {
match id {
"cpu" => self.cpu,
"gpu" => self.gpu,
"cuda" => self.cuda,
"ane" => self.ane,
"trt" => self.trt,
"openvino" => self.openvino,
"llama" => self.llama,
"gpu-beside-ane" => self.gpu_beside_ane,
"remote" => self.remote,
id => self.remotes.get(id).copied(),
}
}
fn remotes_on(&self) -> Vec<String> {
let named = self.remotes.iter().filter(|(_, on)| **on);
(self.remote == Some(true))
.then(|| "remote".to_string())
.into_iter()
.chain(named.map(|(id, _)| id.clone()))
.collect()
}
}
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"),
remotes: list
.split(',')
.map(str::trim)
.filter(|item| is_remote(item))
.map(String::from)
.collect(),
worker: has("worker"),
source: "set by the environment",
};
}
let saved = crate::home::Settings::load().accelerators;
Enabled {
cpu: 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),
remotes: saved.remotes_on(),
worker: false,
source: if saved == Switches::default() {
"default"
} else {
"saved"
},
}
}
pub trait Remote: Send + Sync {
fn open(&self) -> Result<RemoteChannel>;
fn missing(&self) -> Option<String>;
fn describe(&self) -> serde_json::Value;
}
pub struct RemoteChannel {
pub send: Box<dyn Write + Send>,
pub frames: mpsc::Receiver<std::result::Result<Vec<u8>, String>>,
pub device: String,
}
struct Registered {
remote: Arc<dyn Remote>,
lane: Arc<Lane>,
}
static REMOTES: Mutex<Vec<Registered>> = Mutex::new(Vec::new());
fn remotes() -> std::sync::MutexGuard<'static, Vec<Registered>> {
REMOTES.lock().unwrap_or_else(|e| e.into_inner())
}
pub fn is_remote(id: &str) -> bool {
id == "remote"
|| id.strip_prefix("remote-").is_some_and(|name| {
!name.is_empty()
&& name
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
})
}
fn intern(id: &str) -> &'static str {
static IDS: Mutex<Vec<&'static str>> = Mutex::new(Vec::new());
let mut ids = IDS.lock().unwrap_or_else(|e| e.into_inner());
if let Some(&kept) = ids.iter().find(|kept| **kept == id) {
return kept;
}
let kept: &'static str = Box::leak(id.to_string().into_boxed_str());
ids.push(kept);
kept
}
pub fn add_remote(id: &str, remote: Arc<dyn Remote>) -> Result<()> {
if !is_remote(id) {
bail!(
"a remote lane is called remote or remote-<name>, the name in lowercase letters, \
digits and hyphens; {id} is neither"
);
}
let mut all = remotes();
match all.iter_mut().find(|r| r.lane.id == id) {
Some(found) => found.remote = remote,
None => {
let lane = Arc::new(Lane::named(intern(id), remote_spec()));
all.push(Registered { remote, lane });
}
}
Ok(())
}
pub fn remove_remote(id: &str) -> bool {
let gone = {
let mut all = remotes();
all.iter()
.position(|r| r.lane.id == id)
.map(|at| all.remove(at))
};
match gone {
Some(gone) => {
gone.lane.retire();
true
}
None => false,
}
}
pub fn set_remote(remote: Option<Arc<dyn Remote>>) {
match remote {
Some(remote) => add_remote("remote", remote).expect("`remote` is a remote lane id"),
None => {
remove_remote("remote");
}
}
}
fn remote_for(id: &str) -> Option<Arc<dyn Remote>> {
remotes()
.iter()
.find(|r| r.lane.id == id)
.map(|r| Arc::clone(&r.remote))
}
fn remote_ids() -> Vec<&'static str> {
remotes().iter().map(|r| r.lane.id).collect()
}
fn extend(row: &mut serde_json::Value, extra: serde_json::Value) {
if let (Some(row), serde_json::Value::Object(extra)) = (row.as_object_mut(), extra) {
row.extend(extra);
}
}
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: "remote",
label: "Remote",
variant: "fp16-cuda",
experimental: true,
},
Spec {
id: "worker",
label: "CPU worker",
variant: "int8-cpu",
experimental: false,
},
];
pub fn spec(id: &str) -> Option<&'static Spec> {
let id = if is_remote(id) { "remote" } else { id };
SPECS.iter().find(|s| s.id == id)
}
fn remote_spec() -> &'static Spec {
spec("remote").expect("SPECS has the remote entry")
}
fn lane_specs() -> Vec<(&'static str, &'static Spec)> {
let remote = remote_ids();
SPECS
.iter()
.flat_map(|spec| {
if spec.id == "remote" {
remote.iter().map(|&id| (id, spec)).collect()
} else {
vec![(spec.id, spec)]
}
})
.collect()
}
fn label_of(id: &str) -> String {
match id.strip_prefix("remote-") {
Some(name) if is_remote(id) => format!("Remote {name}"),
_ => spec(id).map_or(id, |s| s.label).to_string(),
}
}
#[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>>,
failed_at: Mutex<Option<Instant>>,
retired: AtomicBool,
}
const LANE_RETRY: Duration = Duration::from_secs(600);
impl Lane {
fn new(spec: &Spec) -> Self {
Self::named(spec.id, spec)
}
fn named(id: &'static str, spec: &Spec) -> Self {
Self {
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),
failed_at: Mutex::new(None),
retired: AtomicBool::new(false),
}
}
fn retire(&self) {
let mut jobs = self.jobs.lock().unwrap_or_else(|e| e.into_inner());
self.retired.store(true, Ordering::Relaxed);
jobs.take();
drop(jobs);
self.set(Status::Unavailable {
reason: "its remote worker was removed".into(),
});
}
pub fn token_budget(&self) -> (usize, usize, usize) {
match self.id {
id if is_remote(id) => (2_048, 16_384, 65_536),
"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,
Status::Starting => 3,
_ => 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);
let mut failed_at = self.failed_at.lock().unwrap_or_else(|e| e.into_inner());
match &status {
Status::Failed { reason } if !matches!(*current, Status::Failed { .. }) => {
*failed_at = Some(Instant::now());
eprintln!(
"semlith: the {} lane failed: {reason}; it is tried again in {} minutes",
self.id,
LANE_RETRY.as_secs() / 60
);
}
Status::Failed { .. } => {}
_ => *failed_at = None,
}
drop(failed_at);
*current = status;
}
pub fn coming(&self) -> bool {
matches!(
self.status(),
Status::Idle | Status::Starting | Status::Compiling { .. } | Status::Downloading { .. }
)
}
pub fn usable(&self) -> bool {
let due = self
.failed_at
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_some_and(|at| at.elapsed() >= LANE_RETRY);
if due {
self.reset();
}
!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 self.retired.load(Ordering::Relaxed) {
if let Job::Batch { reply, .. } = job {
let _ = reply.send(Err("its remote worker was removed".into()));
}
return;
}
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() -> Vec<Arc<Lane>> {
let fixed = LANES.get_or_init(|| {
SPECS
.iter()
.filter(|spec| spec.id != "remote")
.map(|spec| Arc::new(Lane::new(spec)))
.collect()
});
let remote: Vec<Arc<Lane>> = remotes().iter().map(|r| Arc::clone(&r.lane)).collect();
let mut out = Vec::with_capacity(fixed.len() + remote.len());
for lane in fixed {
if lane.id == "worker" {
out.extend(remote.iter().cloned());
}
out.push(Arc::clone(lane));
}
out
}
pub fn lane(id: &str) -> Option<Arc<Lane>> {
lanes().into_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",
label_of(id)
)),
"llama" if !(apple || x86_windows_or_linux) => {
Some("no llama.cpp build is pinned for this platform".into())
}
id if is_remote(id) && remote_for(id).is_none() => {
Some("no remote worker is registered".into())
}
_ => None,
}
.or_else(|| missing_hardware(id))
}
fn missing_hardware(id: &str) -> Option<String> {
static GPU: OnceLock<std::result::Result<String, String>> = OnceLock::new();
static NVIDIA: OnceLock<std::result::Result<String, String>> = OnceLock::new();
let gpu = || GPU.get_or_init(detect_gpu).as_ref().err();
let nvidia = || {
NVIDIA
.get_or_init(|| crate::cuda::detect().map(|d| d.name))
.as_ref()
.err()
};
match id {
"gpu" => gpu().map(|why| format!("no GPU this lane can use: {why}")),
"llama" if std::env::var(crate::llama::DEVICE_ENV).is_ok_and(|d| d == "cpu") => None,
"llama" => {
gpu().map(|why| format!("llama.cpp runs on a GPU, and there is none it can use: {why}"))
}
"cuda" | "trt" => nvidia().map(|why| format!("needs an NVIDIA GPU and driver: {why}")),
"openvino" => {
let cpu = crate::system::cpu_name();
(!cpu.to_ascii_lowercase().contains("intel"))
.then(|| format!("OpenVINO offers Intel devices only, and this CPU is {cpu}"))
}
_ => 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()
.into_iter()
.filter(|lane| on.lane(lane.id) && unavailable_here(lane.id).is_none())
.filter(|lane| lane.usable())
.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 let Some(line) = crate::cpucap::paused_line() {
return Some(line);
}
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 = label_of(lane.id);
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)
),
_ => match *lane.began.lock().unwrap_or_else(|e| e.into_inner()) {
Some((start, _, _)) if start.elapsed() >= Duration::from_secs(5) => format!(
"waiting for the {label} lane to start: {} s so far",
start.elapsed().as_secs()
),
_ => 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 (id, spec) in lane_specs() {
if id == "worker" || !on.lane(id) || unavailable_here(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");
let others = SPECS
.iter()
.map(|spec| spec.variant)
.chain(["fp16-coreml-gpu"]);
for variant in others {
if !out.contains(&variant) {
out.push(variant);
}
}
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 from_env = std::env::var(ACCEL_ENV).is_ok();
let saved = crate::home::Settings::load().accelerators;
let choice = |id: &str| match saved.get(id) {
_ if from_env => (on.lane(id), "environment"),
Some(value) => (value, "saved"),
None => (on_by_default(id), "default"),
};
let cpu = cpu_lane();
let cache = crate::model_cache_dir().ok();
let cpu_rate = cpu.rate();
let mut rows = vec![serde_json::json!({
"lane": "cpu",
"label": "CPU",
"enabled": on.cpu,
"saved": true,
"source": if from_env { "environment" } else { "default" },
"locked": true,
"locked_reason": CPU_ALWAYS_ON,
"experimental": false,
"status": if cpu_rate > 0.0 { Status::Active } else { Status::Idle },
"device": crate::system::cpu_name(),
"variant": cpu.variant(),
"rate": round(cpu_rate),
})];
for lane in lanes() {
let enabled = on.lane(lane.id);
let (saved_on, source) = choice(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": label_of(lane.id),
"enabled": enabled,
"saved": saved_on,
"source": source,
"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()),
}));
if let (Some(remote), Some(row)) = (remote_for(lane.id), rows.last_mut()) {
extend(row, remote.describe());
}
}
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 all = lanes();
let accel_ready = all
.iter()
.any(|lane| lane.id != "worker" && on.lane(lane.id) && lane.ready());
let accel_coming = all.iter().any(|lane| {
lane.id != "worker"
&& on.lane(lane.id)
&& unavailable_here(lane.id).is_none()
&& lane.coming()
});
let (cap, cap_source) = crate::cpucap::in_force();
serde_json::json!({
"lanes": rows,
"source": on.source,
"cpu_cap": {
"percent": cap,
"source": cap_source,
"measured_percent": crate::cpucap::measured_percent(),
"paused": crate::cpucap::paused(),
},
"gpu_beside_ane": on.gpu_beside_ane,
"cpu_fallback": !on.cpu && !accel_ready && !accel_coming,
"rates": rates(),
})
}
fn round(value: f64) -> f64 {
(value * 10.0).round() / 10.0
}
fn check_cpu() -> Result<Check> {
let cache = crate::model_cache_dir()?;
let mut model = crate::embed::Model::Granite.load(cache, crate::chunk::MAX_CHARS / 2, true)?;
let check = 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)
})?;
if check.passed {
let (texts, _) = fixture();
let started = Instant::now();
model
.embed(&texts, Some(8))
.map_err(|e| anyhow::anyhow!("{e}"))?;
note_known_answer(
"cpu",
texts.len() as f64 / started.elapsed().as_secs_f64().max(1e-6),
);
}
Ok(check)
}
pub fn seed_cpu_rate() {
let cached = crate::model_cache_dir().is_ok_and(|cache| crate::embed::is_cached(&cache));
if cached && !rates().contains_key("cpu") {
let _ = check_cpu();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, serde::Deserialize)]
pub struct Rate {
pub per_s: f64,
pub source: RateSource,
#[serde(default)]
pub runs: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum RateSource {
Measured,
KnownAnswer,
}
pub fn rates() -> std::collections::BTreeMap<String, Rate> {
crate::home::lane_rates_path()
.ok()
.and_then(|path| std::fs::read(path).ok())
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
.unwrap_or_default()
}
fn save_rates(rates: &std::collections::BTreeMap<String, Rate>) {
if let (Ok(path), Ok(bytes)) = (
crate::home::lane_rates_path(),
serde_json::to_vec_pretty(rates),
) {
let _ = crate::home::write_private(&path, &bytes);
}
}
pub fn note_rate(lane: &str, per_s: f64) {
if !per_s.is_finite() || per_s <= 0.0 {
return;
}
let mut all = rates();
let next = match all.get(lane) {
Some(old) if old.source == RateSource::Measured => Rate {
per_s: (old.per_s + per_s) / 2.0,
source: RateSource::Measured,
runs: old.runs.saturating_add(1),
},
_ => Rate {
per_s,
source: RateSource::Measured,
runs: 1,
},
};
all.insert(lane.to_string(), next);
save_rates(&all);
}
pub fn note_known_answer(lane: &str, per_s: f64) {
if !per_s.is_finite() || per_s <= 0.0 {
return;
}
let mut all = rates();
if all
.get(lane)
.is_some_and(|r| r.source == RateSource::Measured)
{
return;
}
all.insert(
lane.to_string(),
Rate {
per_s,
source: RateSource::KnownAnswer,
runs: 0,
},
);
save_rates(&all);
}
pub fn expected_rate() -> Option<f64> {
let known = rates();
let sum: f64 = run_lanes()
.iter()
.filter_map(|lane| known.get(*lane).map(|r| r.per_s))
.sum();
(sum > 0.0).then_some(sum)
}
pub fn images_beside_text() -> bool {
run_lanes().iter().any(|lane| *lane != "cpu")
}
fn run_lanes() -> Vec<&'static str> {
let on = enabled();
let known = rates();
let usable = |lane: &&str| on.lane(lane) && unavailable_here(lane).is_none();
let ane = usable(&"ane") && known.contains_key("ane");
let lanes: Vec<&str> = ["cpu", "ane", "gpu", "cuda", "trt", "openvino", "llama"]
.into_iter()
.chain(remote_ids())
.filter(|lane| usable(lane))
.filter(|lane| !ane || (*lane != "cpu" && (*lane != "gpu" || on.gpu_beside_ane)))
.collect();
lanes
}
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");
}
if is_remote(lane_id) && remote_for(lane_id).is_none() {
bail!("there is no lane called {lane_id}; the switches are {SWITCH_NAMES}");
}
if on {
refuse_on(lane_id)?;
}
let mut settings = crate::home::Settings::load();
match (lane_id, on) {
("cpu", false) => bail!("{CPU_ALWAYS_ON}"),
("cpu", true) => settings.accelerators.cpu = None,
("gpu", _) => settings.accelerators.gpu = Some(on),
("gpu-beside-ane", _) => settings.accelerators.gpu_beside_ane = Some(on),
(id, _) if is_remote(id) => {
if on && let Some(why) = remote_for(id).and_then(|r| r.missing()) {
bail!("{why}");
}
if id == "remote" {
settings.accelerators.remote = Some(on);
} else {
settings.accelerators.remotes.insert(id.to_string(), 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)
&& matches!(found.status(), Status::Failed { .. })
{
found.set(Status::Idle);
}
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" }
))
}
fn refuse_on(id: &str) -> Result<()> {
if let Some(why) = unavailable_here(id) {
bail!("{why}");
}
let Some(found) = lane(id) else {
return Ok(());
};
found.usable();
match found.status() {
Status::Unavailable { reason } => {
bail!("the {} lane is unavailable here: {reason}", label_of(id))
}
Status::Failed { reason } => bail!(
"the {} lane failed: {reason}; turn it off to clear the failure, or wait {} minutes and it is tried again",
label_of(id),
LANE_RETRY.as_secs() / 60
),
_ => Ok(()),
}
}
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");
}
refuse_on(lane_id)?;
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 = 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: Option<std::process::Child>,
stdin: Option<Box<dyn Write + Send>>,
answers: mpsc::Receiver<std::result::Result<Vec<u8>, String>>,
}
impl Drop for Worker {
fn drop(&mut self) {
drop(self.stdin.take());
let Some(child) = self.child.as_mut() else {
return;
};
let deadline = Instant::now() + Duration::from_secs(2);
while Instant::now() < deadline {
if let Ok(Some(_)) = child.try_wait() {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
let _ = child.kill();
let _ = 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.is_some() && kept_warm() {
continue;
}
if worker.take().is_some() && lane.status() == Status::Active {
lane.set(Status::Idle);
}
continue;
}
Err(mpsc::RecvTimeoutError::Disconnected) => return,
}
} else if sent.len() < depth(&lane) {
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() < depth(&lane) {
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 = check_cpu();
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 (id, spec) in lane_specs() {
let mut base = serde_json::json!({
"lane": id,
"label": label_of(id),
"experimental": spec.experimental,
});
if let Some(remote) = remote_for(id) {
extend(&mut base, remote.describe());
}
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", label_of(id)));
let lane = Arc::new(Lane::named(id, spec));
out.push(match start(&lane) {
Ok((_, hello)) => {
let mut passed = row(serde_json::json!({
"device": hello["device"],
"variant": hello["variant"],
"cosine": hello["cosine"],
"chunks_per_s": hello["chunks_per_s"],
"passed": true,
}));
if let Some(remote) = remote_for(id) {
extend(&mut passed, remote.describe());
}
passed
}
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())
}
pub fn check_lane(id: &str) -> Result<serde_json::Value> {
let spec = spec(id).with_context(|| format!("there is no lane called {id}"))?;
let id = if is_remote(id) { intern(id) } else { spec.id };
let lane = Arc::new(Lane::named(id, spec));
let (_worker, hello) = start(&lane)?;
Ok(hello)
}
pub fn worker_command(id: &str) -> Result<Vec<String>> {
let spec = spec(id).with_context(|| format!("there is no lane called {id}"))?;
if is_remote(id) {
bail!("a worker serves a lane of its own machine, not a remote lane");
}
let mut args = vec!["__embed-worker".to_string()];
args.extend(worker_args(&Arc::new(Lane::new(spec)))?);
Ok(args)
}
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 = 4;
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();
let earlier = name
.strip_prefix("coreml-worker-v")
.and_then(|v| v.parse::<u32>().ok())
.is_some_and(|v| v < COREML_WORKER);
if earlier {
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()?;
#[cfg(unix)]
let parent = std::os::unix::process::parent_id();
loop {
match file.try_lock() {
Ok(()) => return Some(file),
Err(std::fs::TryLockError::WouldBlock) => {
#[cfg(unix)]
if std::os::unix::process::parent_id() != parent {
std::process::exit(0);
}
std::thread::sleep(Duration::from_millis(250));
}
Err(_) => return None,
}
}
}
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 worker = if is_remote(lane.id) {
lane.set(Status::Starting);
let channel = remote_for(lane.id)
.context("no remote worker is registered")?
.open()?;
*lane.device.lock().unwrap_or_else(|e| e.into_inner()) = Some(channel.device);
Worker {
child: None,
stdin: Some(channel.send),
answers: channel.frames,
}
} else {
spawn(lane)?
};
hello(lane, worker)
}
fn spawn(lane: &Arc<Lane>) -> Result<Worker> {
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;
}
}
});
Ok(Worker {
child: Some(child),
stdin: Some(Box::new(stdin)),
answers,
})
}
fn hello(lane: &Arc<Lane>, mut worker: Worker) -> Result<(Worker, serde_json::Value)> {
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}");
}
let measured = if is_remote(lane.id) {
time_fixture(&mut worker)?
} else {
hello["batch_per_s"].as_f64()
};
if let Some(per_s) = measured {
note_known_answer(lane.id, per_s);
}
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 time_fixture(worker: &mut Worker) -> Result<Option<f64>> {
let cache = crate::model_cache_dir()?;
let tokenizer = crate::session::tokenizer(&cache)?;
let (texts, _) = fixture();
let ids = texts
.iter()
.map(|text| crate::session::encode(&tokenizer, text))
.collect::<Result<Vec<_>>>()?;
let started = Instant::now();
let stdin = worker
.stdin
.as_mut()
.context("the worker is being let go")?;
write_frame(stdin, &encode_ids(&ids)).context("sending the fixture")?;
let frame = match worker.answers.recv_timeout(deadline()) {
Ok(Ok(frame)) => frame,
Ok(Err(e)) => bail!("the worker exited during the fixture: {e}"),
Err(_) => bail!(
"the worker did not answer the fixture within {} s",
deadline().as_secs()
),
};
decode_vectors(&frame, ids.len())?;
Ok(Some(
ids.len() as f64 / started.elapsed().as_secs_f64().max(1e-6),
))
}
const MIN_LANE_PER_S: f64 = 2.0;
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 low: Vec<(usize, f32)>,
pub chunks_per_s: f64,
pub passed: bool,
}
impl Check {
pub fn low_line(&self) -> String {
let each: Vec<String> = self
.low
.iter()
.map(|(at, cosine)| format!("#{at} {cosine:.4}"))
.collect();
format!(
"{} of {} chunks below the floor: {}",
self.low.len(),
fixture().0.len(),
each.join(", ")
)
}
}
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 floor = if variant == "int8-cpu" {
MIN_COSINE_INT8
} else {
MIN_COSINE_FP16
};
let started = Instant::now();
let mut worst = 1.0f32;
let mut low = Vec::new();
for (at, (text, want)) in texts.iter().zip(&expected).enumerate() {
let got = embed(std::slice::from_ref(text))?;
let got = got.first().context("no vector came back")?;
let cosine = crate::index::cosine(got, want);
worst = worst.min(cosine);
if cosine < floor {
low.push((at, cosine));
}
}
let elapsed = started.elapsed().as_secs_f64().max(1e-6);
Ok(Check {
lane: lane.to_string(),
device: device.to_string(),
variant: variant.to_string(),
cosine: worst,
low,
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 mut batched = None;
let check = tokenizer.and_then(|tokenizer| {
let check = 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)
})?;
if check.passed {
let (texts, _) = fixture();
let ids = texts
.iter()
.map(|text| crate::session::encode(&tokenizer, text))
.collect::<Result<Vec<_>>>()?;
let started = Instant::now();
for group in ids.chunks(8) {
let rows: Vec<&[u32]> = group.iter().map(Vec::as_slice).collect();
session.embed(&rows)?;
}
batched = Some(ids.len() as f64 / started.elapsed().as_secs_f64().max(1e-6));
}
Ok(check)
});
if let (Ok(check), Some(per_s)) = (&check, batched)
&& check.passed
&& check.variant != "int8-cpu"
&& per_s < MIN_LANE_PER_S
{
say(
&mut out,
serde_json::json!({
"ok": false,
"unavailable": true,
"reason": format!(
"{} embedded at {per_s:.1} chunks/s, slower than any GPU, so it is most likely a software renderer standing in for the card (on Linux: the GPU maker's Vulkan driver is missing, or the container has no graphics capability)",
check.device
),
}),
);
return 1;
}
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,
"batch_per_s": batched,
}),
),
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, check.low_line()
),
}),
);
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 a_run_looks_up_every_variant_after_its_own() {
let variants = cache_variants();
for spec in SPECS {
assert!(
variants.contains(&spec.variant),
"{} is never looked up",
spec.variant
);
}
for variant in ["fp16-coreml-gpu", "fp16-cuda"] {
assert!(variants.contains(&variant), "{variant} is never looked up");
}
let mut unique = variants.clone();
unique.sort();
unique.dedup();
assert_eq!(unique.len(), variants.len(), "a variant looked up twice");
if !MANAGED.load(Ordering::Relaxed) {
assert_eq!(variants[0], "int8-cpu", "this process's own variant first");
}
}
#[test]
fn a_failed_check_names_every_chunk_under_the_floor() {
let (texts, expected) = fixture();
let mut at = 0;
let check = known_answer("gpu", "test", "fp16-webgpu", |_| {
let mut v = expected[at].clone();
if at == 3 || at == 17 {
v[0] += 0.3;
v[1] -= 0.3;
}
at += 1;
Ok(vec![v])
})
.unwrap();
assert!(!check.passed);
assert_eq!(
check.low.iter().map(|l| l.0).collect::<Vec<_>>(),
vec![3, 17]
);
let line = check.low_line();
assert!(
line.starts_with(&format!("2 of {} chunks below the floor: #3 ", texts.len())),
"{line}"
);
}
#[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 later = component_dir(
cache.path(),
&format!("coreml-worker-v{}", COREML_WORKER + 1),
);
std::fs::create_dir_all(&later).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"
);
assert!(later.exists(), "a later protocol's worker was removed");
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());
let began = std::time::Instant::now();
lane.set(Status::Compiling {
percent: 0,
eta_ms: None,
});
let lag = began.elapsed().as_millis() as u64;
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 + lag).max(1_000)..=taken + slack).contains(&first),
"{first} after {taken} ms, the lane's clock {lag} ms behind"
);
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());
}
#[test]
fn a_failed_lane_is_tried_again_after_its_cool_down() {
let lane = Lane::new(spec("ane").unwrap());
lane.set(Status::Failed { reason: "x".into() });
assert!(!lane.usable());
let first = *lane.failed_at.lock().unwrap();
lane.set(Status::Failed { reason: "y".into() });
assert_eq!(*lane.failed_at.lock().unwrap(), first);
*lane.failed_at.lock().unwrap() = Instant::now().checked_sub(LANE_RETRY);
assert!(lane.usable());
assert_eq!(lane.status(), Status::Idle);
assert!(lane.failed_at.lock().unwrap().is_none());
}
#[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) };
}
}