Skip to main content

clawft_kernel/workload_runtime/infer/
config.rs

1//! Adapter configuration: adopted or managed, and what managed needs.
2
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5use std::time::Duration;
6
7use super::spec::InferFlavor;
8use crate::model_manifest::ModelRegistry;
9
10/// Backoff for restarting a managed server that died while it should run.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct RestartPolicy {
13    /// Restarts before giving up (the operator then sees `Exited`).
14    pub max_restarts: u32,
15    /// Delay before the first restart; doubles each time.
16    pub base: Duration,
17    /// Longest delay.
18    pub cap: Duration,
19    /// How long a process must have been up (and answering) before the
20    /// restart count is cleared. A server that answers one probe and then
21    /// dies is still crash-looping and must reach `GaveUp`.
22    pub stable_after: Duration,
23}
24
25impl Default for RestartPolicy {
26    fn default() -> Self {
27        Self {
28            max_restarts: 5,
29            base: Duration::from_secs(2),
30            cap: Duration::from_secs(60),
31            stable_after: Duration::from_secs(60),
32        }
33    }
34}
35
36impl RestartPolicy {
37    /// Delay before restart number `attempt` (0-based).
38    pub fn delay(&self, attempt: u32) -> Duration {
39        self.base
40            .saturating_mul(1u32 << attempt.min(16))
41            .min(self.cap)
42    }
43}
44
45/// What managed mode needs.
46#[derive(Clone)]
47pub struct ManagedConfig {
48    /// The launcher script (`serve-llamacpp`, `serve`). Not used by Ollama,
49    /// which manages its own process.
50    pub serve_program: Option<PathBuf>,
51    /// Optional capability probe command (`llama-server --version`); exit 0
52    /// is a pass and the first stdout line is recorded as the version.
53    pub version_probe: Option<(PathBuf, Vec<String>)>,
54    /// Where per-instance working directories are created.
55    pub data_root: PathBuf,
56    /// Adopted model manifests; the only source of weight paths.
57    pub models: Arc<ModelRegistry>,
58    /// Extra environment for the server (the parent's is cleared).
59    pub env: Vec<(String, String)>,
60    /// Names of parent environment variables to forward (`HOME`,
61    /// `HF_HOME`, `PATH`). The default forwards the daemon's `PATH` and
62    /// `HOME` to the launcher: the server runs with the daemon's search
63    /// path and home directory. A forwarded or explicit `env` `PATH` wins
64    /// over the supervisor's default one.
65    pub env_passthrough: Vec<String>,
66    /// `(uid, gid)` to drop to when the host runs as root.
67    pub run_as: Option<(u32, u32)>,
68    /// Restart backoff.
69    pub restart: RestartPolicy,
70    /// Ollama `keep_alive` sent when loading a model (`5m`, `30m`, `-1`).
71    pub keep_alive: String,
72    /// Unified-memory budget and co-residency, shared by every managed
73    /// adapter on the node. `None`: no check (a single adapter, or the
74    /// operator did not ask for one).
75    pub ledger: Option<Arc<super::residency::ResidencyLedger>>,
76}
77
78impl ManagedConfig {
79    /// Defaults: no launcher, `HOME` and `PATH` forwarded, default backoff.
80    pub fn new(models: Arc<ModelRegistry>, data_root: PathBuf) -> Self {
81        Self {
82            serve_program: None,
83            version_probe: None,
84            data_root,
85            models,
86            env: Vec::new(),
87            env_passthrough: vec!["HOME".into(), "PATH".into()],
88            run_as: None,
89            restart: RestartPolicy::default(),
90            keep_alive: "30m".into(),
91            ledger: None,
92        }
93    }
94
95    /// Share a residency ledger (budget and co-residency).
96    pub fn with_ledger(mut self, ledger: Arc<super::residency::ResidencyLedger>) -> Self {
97        self.ledger = Some(ledger);
98        self
99    }
100
101    /// Launcher script.
102    pub fn with_serve_program(mut self, p: impl Into<PathBuf>) -> Self {
103        self.serve_program = Some(p.into());
104        self
105    }
106}
107
108/// The model lab's launcher for `flavor` under `home` (`<home>/llm/bin/...`).
109/// Only builds the path; nothing is read or checked.
110pub fn lab_serve_program(home: &Path, flavor: InferFlavor) -> Option<PathBuf> {
111    let name = match flavor {
112        InferFlavor::LlamaCpp => "serve-llamacpp",
113        InferFlavor::MlxLm => "serve",
114        InferFlavor::Ollama => return None,
115    };
116    Some(home.join("llm").join("bin").join(name))
117}
118
119/// Observe-only, or launch and control.
120///
121/// Adopted mode is observe-only. Everything it does is a read against a
122/// loopback address taken from the spec, and that includes `admit`, which
123/// the host calls before the governance gate (an admission self-check is
124/// read-only by contract), so a spec's loopback port is probed with GETs
125/// before any permit is consulted. [`InferConfig::allowed_ports`] bounds
126/// which ports that can be.
127#[derive(Clone)]
128pub enum InferMode {
129    /// Register and health-check a server already running; never start,
130    /// stop or signal it.
131    Adopted,
132    /// Start, stop, restart and unload the server.
133    Managed(Box<ManagedConfig>),
134}
135
136/// One adapter's configuration.
137#[derive(Clone)]
138pub struct InferConfig {
139    /// Server software.
140    pub flavor: InferFlavor,
141    /// Adopted or managed.
142    pub mode: InferMode,
143    /// Per-request timeout of probes.
144    pub probe_timeout: Duration,
145    /// Port the capability probe asks (the flavor's conventional port when
146    /// absent). Instances carry their own ports.
147    pub probe_port: Option<u16>,
148    /// Inclusive port range instances may use. Checked before anything is
149    /// probed: a spec naming a port outside it never causes a connection.
150    pub allowed_ports: Option<(u16, u16)>,
151    /// Timeout of [`InferRuntime::deep_health`](super::InferRuntime::deep_health)
152    /// (a cold model can take minutes to answer its first token).
153    pub deep_timeout: Duration,
154}
155
156impl InferConfig {
157    /// Limit instances to ports in `lo..=hi`.
158    pub fn with_port_range(mut self, lo: u16, hi: u16) -> Self {
159        self.allowed_ports = Some((lo, hi));
160        self
161    }
162
163    /// Probe a different port for capabilities.
164    pub fn with_probe_port(mut self, port: u16) -> Self {
165        self.probe_port = Some(port);
166        self
167    }
168
169    /// An adopting adapter.
170    pub fn adopted(flavor: InferFlavor) -> Self {
171        Self {
172            flavor,
173            mode: InferMode::Adopted,
174            probe_timeout: Duration::from_secs(2),
175            probe_port: None,
176            allowed_ports: None,
177            deep_timeout: Duration::from_secs(300),
178        }
179    }
180
181    /// A managing adapter.
182    pub fn managed(flavor: InferFlavor, cfg: ManagedConfig) -> Self {
183        Self {
184            flavor,
185            mode: InferMode::Managed(Box::new(cfg)),
186            probe_timeout: Duration::from_secs(2),
187            probe_port: None,
188            allowed_ports: None,
189            deep_timeout: Duration::from_secs(300),
190        }
191    }
192}