Skip to main content

superfluid_daemon/
runtime_options.rs

1//! `--<runtime>-<name> <value>` on `superfluid serve`: an option of one runtime, checked against what its adapter
2//! declares and forwarded to its worker as `--<name> <value>`; set once per process, as `BASERT_LIB` is.
3
4use std::path::Path;
5use std::sync::OnceLock;
6
7use serde_json::{json, Value};
8use superfluid_adapter_kit::{check_value, placeholder, OptionDecl, WorkerArgs};
9
10use crate::runtime_pick::RuntimeId;
11use crate::runtimes::{Catalog, Runtime, Source, Worker};
12
13/// One `--<runtime>-<name> <value>` as given, resolved to the runtime it names.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Given {
16    pub flag: String,
17    pub runtime: RuntimeId,
18    pub name: String,
19    pub value: String,
20}
21
22/// Whether `flag` is shaped as a runtime option, `--<id>-<name>`, spelled as a runtime id and an option name can be
23/// (an id may hold '.' and '_', a name never does); only then does `serve` keep it for [`resolve`].
24pub fn looks_like(flag: &str) -> bool {
25    let Some(body) = flag.strip_prefix("--") else { return false };
26    let id_char = |c: char| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '-' | '.' | '_');
27    let name_char = |c: char| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-';
28    body.match_indices('-').any(|(i, _)| {
29        let (id, name) = (&body[..i], &body[i + 1..]);
30        !id.is_empty() && id.chars().all(id_char) && !name.is_empty() && name.chars().all(name_char)
31    })
32}
33
34/// The runtime among `ids` that `flag` names, the longest id winning (`--mlx-x-y` is mlx's `x-y`
35/// unless a runtime `mlx-x` is here), or `None` when no runtime here is named.
36pub fn resolve(flag: &str, value: &str, ids: &[RuntimeId]) -> Option<Given> {
37    let body = flag.strip_prefix("--")?;
38    let mut best: Option<(RuntimeId, &str)> = None;
39    for id in ids.iter().map(|id| id.base()) {
40        let Some(name) = body.strip_prefix(id.name()).and_then(|rest| rest.strip_prefix('-')) else { continue };
41        if !name.is_empty() && best.is_none_or(|(b, _)| id.name().len() > b.name().len()) {
42            best = Some((id, name));
43        }
44    }
45    best.map(|(runtime, name)| Given { flag: flag.to_string(), runtime, name: name.to_string(), value: value.to_string() })
46}
47
48/// Refuses an option given twice: the worker reads the last value and this process the first (`daemon_environment`),
49/// so a repeat would split one knob between two values; serve refuses it as a usage error before anything starts.
50pub fn refuse_repeats(given: &[Given]) -> Result<(), String> {
51    for (i, g) in given.iter().enumerate() {
52        if let Some(first) = given[..i].iter().find(|p| p.runtime == g.runtime && p.name == g.name) {
53            return Err(format!("{}: given twice ({:?}, then {:?}); give it once", g.flag, first.value, g.value));
54        }
55    }
56    Ok(())
57}
58
59/// The options among `given` for `runtime`, checked against its declarations, as the pairs its worker takes.
60pub fn check(given: &[Given], runtime: &Runtime) -> Result<Vec<(String, String)>, String> {
61    let id = runtime.id.base();
62    let mut out = Vec::new();
63    for g in given.iter().filter(|g| g.runtime == id) {
64        let Some(decl) = runtime.options.iter().find(|o| o.name == g.name) else {
65            let takes: Vec<String> = runtime.options.iter().map(|o| format!("--{}-{}", id.name(), o.name)).collect();
66            return Err(if takes.is_empty() {
67                format!("{}: the {} runtime takes no option of its own", g.flag, id.name())
68            } else {
69                format!("{}: the {} runtime takes no such option (it takes: {})", g.flag, id.name(), takes.join(", "))
70            });
71        };
72        check_value(&decl.kind, &decl.choices, decl.least, decl.most, &g.value).map_err(|e| format!("{}: {e}", g.flag))?;
73        out.push((format!("--{}", g.name), g.value.clone()));
74    }
75    Ok(out)
76}
77
78/// The options per runtime, as the `--<name> <value>` pairs its worker takes.
79pub type Settled = Vec<(RuntimeId, Vec<(String, String)>)>;
80
81/// The options as given, grouped by runtime, for the first probe of a runtime: an option that makes it usable
82/// here (its library, its environment) counts from the start, and the worker refuses one it does not take.
83pub fn given_pairs(given: &[Given]) -> Settled {
84    let mut out: Settled = Vec::new();
85    for g in given {
86        let pair = (format!("--{}", g.name), g.value.clone());
87        match out.iter_mut().find(|(id, _)| *id == g.runtime) {
88            Some((_, pairs)) => pairs.push(pair),
89            None => out.push((g.runtime, vec![pair])),
90        }
91    }
92    out
93}
94
95/// When a probe failed with options given: the option at fault, named as the daemon names it, when the worker
96/// answers without them; `None` when the runtime fails without them too.
97pub fn blamed(catalog: &Catalog, id: RuntimeId, given: &[Given]) -> Option<String> {
98    if !given.iter().any(|g| g.runtime == id.base()) {
99        return None;
100    }
101    let plain = catalog.locate_plain(id)?;
102    let rt = crate::runtimes::check_worker(id, &plain).ok()?;
103    check(given, &rt).err()
104}
105
106/// The arguments an in-process worker gets, parsed as a worker process parses them, so `--venv` lands in its field.
107pub fn worker_args(id: RuntimeId, model: &Path, ctx: i32, max_batch: u32, kv_bits: i32) -> Result<WorkerArgs, String> {
108    worker_args_from(&for_runtime(id), model, ctx, max_batch, kv_bits)
109}
110
111/// The arguments an in-process pull gets: the runtime's options alone, parsed as a worker process parses them.
112pub fn pull_args(id: RuntimeId) -> Result<WorkerArgs, String> {
113    let options = for_runtime(id);
114    let argv: Vec<String> = options.iter().flat_map(|(f, v)| [f.clone(), v.clone()]).collect();
115    WorkerArgs::parse(&argv)
116}
117
118/// What this process must carry for the libraries it loads itself: the llama.cpp tokenizer finds libllama by
119/// `SUPERFLUID_LLAMA_LIB`, since the worker's `--lib` never reaches it.
120pub fn daemon_environment(settled: &Settled) -> Vec<(&'static str, String)> {
121    let mut out = Vec::new();
122    for (id, options) in settled {
123        if id.base().name() == "llamacpp" {
124            if let Some((_, lib)) = options.iter().find(|(flag, _)| flag == "--lib") {
125                out.push(("SUPERFLUID_LLAMA_LIB", lib.clone()));
126            }
127        }
128    }
129    out
130}
131
132/// Sets [`daemon_environment`] on this process, before any such library is loaded.
133pub fn export_to_daemon() {
134    if let Some(all) = SET.get() {
135        for (name, value) in daemon_environment(all) {
136            std::env::set_var(name, value);
137        }
138    }
139}
140
141pub fn worker_args_from(options: &[(String, String)], model: &Path, ctx: i32, max_batch: u32, kv_bits: i32) -> Result<WorkerArgs, String> {
142    let mut argv: Vec<String> = vec![
143        "--model".into(),
144        model.to_string_lossy().into_owned(),
145        "--max-context".into(),
146        ctx.to_string(),
147        "--max-batch".into(),
148        max_batch.to_string(),
149        "--kv-bits".into(),
150        kv_bits.to_string(),
151    ];
152    for (flag, value) in options {
153        argv.push(flag.clone());
154        argv.push(value.clone());
155    }
156    WorkerArgs::parse(&argv)
157}
158
159/// Every given option checked: against `primary` for its runtime, and against any other runtime named, which must be usable here.
160pub fn settle(given: &[Given], primary: &Runtime, catalog: &Catalog) -> Result<Settled, String> {
161    let mut ids: Vec<RuntimeId> = given.iter().map(|g| g.runtime).collect();
162    ids.sort();
163    ids.dedup();
164    let mut out = Vec::new();
165    for id in ids {
166        let checked = if id == primary.id.base() {
167            check(given, primary)?
168        } else {
169            let rt = catalog.require(id).map_err(|e| {
170                let flag = given.iter().find(|g| g.runtime == id).map(|g| g.flag.as_str()).unwrap_or_default();
171                format!("{flag}: the {} runtime is not usable here, so its options cannot be checked: {e}", id.name())
172            })?;
173            check(given, &rt)?
174        };
175        out.push((id, checked));
176    }
177    Ok(out)
178}
179
180static SET: OnceLock<Settled> = OnceLock::new();
181
182/// Keeps the settled options for every worker this process starts; the first call wins.
183pub fn set(settled: Settled) {
184    let _ = SET.set(settled);
185}
186
187/// The `--<name> <value>` pairs a worker of `id` gets.
188pub fn for_runtime(id: RuntimeId) -> Vec<(String, String)> {
189    SET.get().and_then(|all| all.iter().find(|(r, _)| *r == id.base()).map(|(_, o)| o.clone())).unwrap_or_default()
190}
191
192/// What `superfluid runtime options <id>` lists: a usable runtime's declaration, else the one its adapter makes before
193/// an install, asked as `runtime versions` asks it (the worker named for it, the linked adapter, the worker that would
194/// install it): the options, `--llamacpp-lib` among them, are what one reads before the runtime runs here.
195pub fn declared_by(catalog: &Catalog, id: RuntimeId) -> Result<Vec<OptionDecl>, String> {
196    let id = id.base();
197    let not_usable = match catalog.require(id) {
198        Ok(runtime) => return Ok(runtime.options),
199        Err(why) => why,
200    };
201    let named = std::env::var_os(crate::runtimes::worker_env(id)).is_some();
202    if let Some(rt) = crate::linked::get(id).filter(|_| !named) {
203        return Ok(superfluid_adapter_kit::declared(rt));
204    }
205    // A worker's report declares its options whether or not the runtime runs here.
206    let bin = crate::installs::installer(catalog, id).map_err(|_| not_usable.clone())?;
207    let report = crate::runtimes::run_check_model(&Worker { bin, prefix: Vec::new(), source: Source::Shipped }, None).map_err(|_| not_usable)?;
208    Ok(superfluid_worker::check::options_from(&report["options"]))
209}
210
211/// `superfluid runtime options <id>`, for a person.
212pub fn text(id: RuntimeId, options: &[OptionDecl]) -> String {
213    let id = id.base().name();
214    if options.is_empty() {
215        return format!("the {id} runtime takes no option of its own; `superfluid serve --help` lists the flags every runtime takes\n");
216    }
217    let rows: Vec<(String, String, String)> = options
218        .iter()
219        .map(|o| {
220            let default = if o.default.is_empty() { "-".to_string() } else { o.default.clone() };
221            (format!("--{id}-{} {}", o.name, placeholder(&o.kind, &o.choices)), default, o.about.clone())
222        })
223        .collect();
224    let w1 = rows.iter().map(|r| r.0.chars().count()).max().unwrap_or(0);
225    let w2 = rows.iter().map(|r| r.1.chars().count()).max().unwrap_or(0).max("DEFAULT".len());
226    let mut out = format!("Options of the {id} runtime on `superfluid serve`:\n\n  {:<w1$}  {:<w2$}  \n", "OPTION", "DEFAULT");
227    for (flag, default, about) in rows {
228        out.push_str(&format!("  {flag:<w1$}  {default:<w2$}  {about}\n"));
229    }
230    out
231}
232
233/// `superfluid runtime options <id> --json`.
234pub fn json(id: RuntimeId, options: &[OptionDecl]) -> Value {
235    json!({"runtime": id.base().name(), "options": options.iter().map(|o| o.to_json()).collect::<Vec<_>>()})
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::runtimes::Info;
242
243    fn runtime(id: &str, options: Vec<OptionDecl>) -> Runtime {
244        Runtime {
245            id: RuntimeId::new(id),
246            worker: Worker { bin: std::path::PathBuf::from("/nowhere"), prefix: Vec::new(), source: Source::Env },
247            version: None,
248            abi: None,
249            capabilities: None,
250            info: Info::default(),
251            devices: Vec::new(),
252            tokenizer: None,
253            pull: None,
254            options,
255        }
256    }
257
258    fn decl(name: &str, kind: &str, choices: &[&str], default: &str) -> OptionDecl {
259        OptionDecl {
260            name: name.into(),
261            kind: kind.into(),
262            choices: choices.iter().map(|c| c.to_string()).collect(),
263            least: None,
264            most: None,
265            default: default.into(),
266            about: format!("about {name}"),
267        }
268    }
269
270    #[test]
271    fn the_options_as_given_are_grouped_by_runtime_for_the_first_probe() {
272        let ids = [RuntimeId::new("llamacpp"), RuntimeId::new("mlx")];
273        let given = [
274            resolve("--llamacpp-lib", "/custom/lib", &ids).unwrap(),
275            resolve("--mlx-venv", "/custom/env", &ids).unwrap(),
276            resolve("--llamacpp-n-gpu-layers", "20", &ids).unwrap(),
277        ];
278        let pairs = given_pairs(&given);
279        assert_eq!(pairs.len(), 2);
280        assert_eq!(pairs[0], (ids[0], vec![("--lib".to_string(), "/custom/lib".to_string()), ("--n-gpu-layers".to_string(), "20".to_string())]));
281        assert_eq!(pairs[1], (ids[1], vec![("--venv".to_string(), "/custom/env".to_string())]));
282        assert!(given_pairs(&[]).is_empty());
283    }
284
285    #[test]
286    fn an_option_given_twice_is_refused() {
287        let ids = [RuntimeId::new("llamacpp"), RuntimeId::new("mlx")];
288        let given = |flags: &[(&str, &str)]| flags.iter().map(|(f, v)| resolve(f, v, &ids).unwrap()).collect::<Vec<_>>();
289        let twice = given(&[("--llamacpp-lib", "/a"), ("--mlx-venv", "/e"), ("--llamacpp-lib", "/b")]);
290        assert_eq!(refuse_repeats(&twice).unwrap_err(), "--llamacpp-lib: given twice (\"/a\", then \"/b\"); give it once");
291        let same = given(&[("--llamacpp-threads", "8"), ("--llamacpp-threads", "8")]);
292        assert!(refuse_repeats(&same).is_err(), "the same value twice is still a repeat");
293        assert!(refuse_repeats(&given(&[("--llamacpp-lib", "/a"), ("--mlx-venv", "/a"), ("--llamacpp-threads", "8")])).is_ok());
294        assert!(refuse_repeats(&[]).is_ok());
295    }
296
297    #[test]
298    fn in_process_worker_arguments_are_parsed_as_a_worker_process_parses_them() {
299        let options = [("--venv".to_string(), "/custom/env".to_string()), ("--prefill-chunk".to_string(), "512".to_string())];
300        let args = worker_args_from(&options, Path::new("/m"), 4096, 8, 0).unwrap();
301        assert_eq!(args.venv.as_deref(), Some(Path::new("/custom/env")), "where the MLX adapter reads its environment");
302        assert_eq!(args.extra, [("--prefill-chunk".to_string(), "512".to_string())]);
303        assert_eq!((args.model.as_deref(), args.max_context, args.max_batch, args.kv_bits), (Some(Path::new("/m")), 4096, 8, 0));
304        let plain = worker_args_from(&[], Path::new("/m"), 2048, 4, 8).unwrap();
305        assert!(plain.venv.is_none() && plain.extra.is_empty() && plain.kv_bits == 8);
306        assert!(worker_args_from(&[("--lib".to_string(), String::new())], Path::new("/m"), 1, 1, 0).is_ok(), "a value is the parser's to take; the worker judges it");
307    }
308
309    #[test]
310    fn the_daemon_carries_the_llama_cpp_library_for_its_own_tokenizer() {
311        let ids = [RuntimeId::new("llamacpp"), RuntimeId::new("mlx")];
312        let given = [resolve("--llamacpp-lib", "/custom/lib", &ids).unwrap(), resolve("--mlx-venv", "/custom/env", &ids).unwrap()];
313        assert_eq!(daemon_environment(&given_pairs(&given)), [("SUPERFLUID_LLAMA_LIB", "/custom/lib".to_string())]);
314        let other = [resolve("--llamacpp-n-gpu-layers", "20", &ids).unwrap()];
315        assert!(daemon_environment(&given_pairs(&other)).is_empty(), "only the library concerns the daemon's own process");
316        let args = WorkerArgs::parse(&["--venv".to_string(), "/custom/env".to_string()]).unwrap();
317        assert_eq!(args.venv.as_deref(), Some(Path::new("/custom/env")), "what pull_args hands an in-process pull");
318    }
319
320    #[test]
321    fn a_flag_is_kept_for_resolution_only_when_shaped_as_an_option() {
322        assert!(looks_like("--llamacpp-n-gpu-layers"));
323        assert!(looks_like("--mlx-venv"));
324        assert!(looks_like("--max-context"), "a flag serve knows never reaches here; the shape alone is judged");
325        assert!(!looks_like("--port"), "no '-' after the runtime");
326        assert!(!looks_like("--llamacpp-"), "an empty name");
327        assert!(!looks_like("-ngl") && !looks_like("--Llamacpp-x") && !looks_like("--llamacpp-x=1"));
328        assert!(!looks_like("--llamacpp-cache_type_k") && !looks_like("--llamacpp-rope.freq"), "no option name holds '_' or '.'");
329        assert!(looks_like("--my_rt.2-threads"), "a runtime id may");
330        assert!(!looks_like("--my_rt.2-"));
331    }
332
333    #[test]
334    fn the_longest_runtime_id_names_the_option() {
335        let ids = [RuntimeId::new("mlx"), RuntimeId::new("mlx-pro"), RuntimeId::new("llamacpp@v1-metal")];
336        let g = resolve("--mlx-pro-chunk", "4", &ids).unwrap();
337        assert_eq!((g.runtime.name(), g.name.as_str(), g.value.as_str()), ("mlx-pro", "chunk", "4"));
338        let g = resolve("--mlx-prefill-chunk", "4", &ids).unwrap();
339        assert_eq!((g.runtime.name(), g.name.as_str()), ("mlx", "prefill-chunk"));
340        let g = resolve("--llamacpp-threads", "8", &ids).unwrap();
341        assert_eq!((g.runtime.name(), g.name.as_str()), ("llamacpp", "threads"), "an install's id names its base runtime");
342        assert!(resolve("--max-contxt", "8", &ids).is_none(), "no runtime max here");
343        assert!(resolve("--mlx-", "8", &ids).is_none());
344    }
345
346    #[test]
347    fn a_value_is_checked_against_the_declaration_and_the_worker_flag_is_built() {
348        let rt = runtime("llamacpp", vec![decl("n-gpu-layers", "int", &[], "-1"), decl("flash-attn", "choice", &["auto", "on", "off"], "auto")]);
349        let ids = [rt.id];
350        let good = [resolve("--llamacpp-n-gpu-layers", "20", &ids).unwrap(), resolve("--llamacpp-flash-attn", "off", &ids).unwrap()];
351        assert_eq!(check(&good, &rt).unwrap(), [("--n-gpu-layers".to_string(), "20".to_string()), ("--flash-attn".to_string(), "off".to_string())]);
352        let bad = [resolve("--llamacpp-flash-attn", "yes", &ids).unwrap()];
353        assert_eq!(check(&bad, &rt).unwrap_err(), "--llamacpp-flash-attn: one of auto, on, off expected, got \"yes\"");
354        let mut floored = runtime("llamacpp", vec![decl("batch-size", "int", &[], "2048")]);
355        floored.options[0].least = Some(1);
356        let zero = [resolve("--llamacpp-batch-size", "0", &[floored.id]).unwrap()];
357        assert_eq!(check(&zero, &floored).unwrap_err(), "--llamacpp-batch-size: at least 1 expected, got 0");
358        let mut bounded = runtime("llamacpp", vec![decl("threads", "int", &[], "0"), decl("rope-freq-base", "float", &[], "model's")]);
359        (bounded.options[0].least, bounded.options[0].most, bounded.options[1].least) = (Some(0), Some(512), Some(0));
360        let ids = [bounded.id];
361        let many = [resolve("--llamacpp-threads", "2147483647", &ids).unwrap()];
362        assert_eq!(check(&many, &bounded).unwrap_err(), "--llamacpp-threads: 0 to 512 expected, got 2147483647");
363        let below = [resolve("--llamacpp-rope-freq-base", "-1", &ids).unwrap()];
364        assert_eq!(check(&below, &bounded).unwrap_err(), "--llamacpp-rope-freq-base: at least 0 expected, got -1");
365        let fine = [resolve("--llamacpp-threads", "512", &ids).unwrap(), resolve("--llamacpp-rope-freq-base", "0", &ids).unwrap()];
366        assert_eq!(check(&fine, &bounded).unwrap().len(), 2);
367        let ids = [rt.id];
368        let unknown = [resolve("--llamacpp-ngl", "20", &ids).unwrap()];
369        assert_eq!(
370            check(&unknown, &rt).unwrap_err(),
371            "--llamacpp-ngl: the llamacpp runtime takes no such option (it takes: --llamacpp-n-gpu-layers, --llamacpp-flash-attn)"
372        );
373        let none = runtime("basert", Vec::new());
374        let given = [resolve("--basert-threads", "2", &[none.id]).unwrap()];
375        assert_eq!(check(&given, &none).unwrap_err(), "--basert-threads: the basert runtime takes no option of its own");
376        assert!(check(&good, &none).unwrap().is_empty(), "another runtime's options are not its concern");
377    }
378
379    #[test]
380    fn the_options_are_listed_before_the_runtime_runs_here() {
381        let root = std::env::temp_dir().join(format!("superfluid-runtime-options-declared-{}", std::process::id()));
382        let _ = std::fs::remove_dir_all(&root);
383        let (beside, libexec) = (root.join("bin"), root.join("libexec").join("superfluid"));
384        std::fs::create_dir_all(&beside).unwrap();
385        std::fs::create_dir_all(&libexec).unwrap();
386        // A shipped adapter whose library is not here: not usable, and still declaring what it takes.
387        let report = r#"{"runtime":{"id":"toyopt","version":"v1"},"available":"no libtoy here","options":[{"name":"lib","kind":"text","default":"the install's","about":"a library of your own"}]}"#;
388        crate::runtimes::tests::fake_worker(&libexec, "superfluid-worker-toyopt", report, 1);
389        let cat = Catalog::new(root.join("home"), Some(beside.clone()), beside.join("superfluid-workerd"));
390        let id = RuntimeId::new("toyopt");
391        assert!(cat.require(id).is_err(), "the runtime does not run here");
392        let options = declared_by(&cat, id).unwrap();
393        assert_eq!(options.iter().map(|o| o.name.as_str()).collect::<Vec<_>>(), ["lib"]);
394        assert!(text(id, &options).contains("--toyopt-lib <value>"));
395        let nowhere = RuntimeId::new("nort");
396        assert_eq!(declared_by(&cat, nowhere).unwrap_err(), cat.require(nowhere).unwrap_err(), "nothing declares it: the runtime's own error");
397        for id in crate::linked::ids().into_iter().filter(|id| std::env::var_os(crate::runtimes::worker_env(*id)).is_none()) {
398            let linked = superfluid_adapter_kit::declared(crate::linked::get(id).unwrap());
399            assert_eq!(declared_by(&cat, id).unwrap(), linked, "{}: a linked adapter answers in process", id.name());
400        }
401        let _ = std::fs::remove_dir_all(&root);
402    }
403
404    #[test]
405    fn the_listing_shows_each_option_with_its_value_shape_and_default() {
406        let rt = runtime("llamacpp", vec![decl("n-gpu-layers", "int", &[], "-1"), decl("flash-attn", "choice", &["auto", "on", "off"], ""), decl("lib", "text", &[], "the install's")]);
407        let t = text(rt.id, &rt.options);
408        assert!(t.contains("--llamacpp-n-gpu-layers <N>"), "{t}");
409        assert!(t.contains("--llamacpp-flash-attn <auto|on|off>"), "{t}");
410        assert!(t.contains("--llamacpp-lib <value>") && t.contains("the install's") && t.contains("about lib"), "{t}");
411        assert!(text(RuntimeId::new("basert"), &[]).starts_with("the basert runtime takes no option of its own"));
412        assert_eq!(json(rt.id, &rt.options)["options"][1]["choices"], json!(["auto", "on", "off"]));
413    }
414}