Skip to main content

ferrum_types/
runtime_config.rs

1//! Runtime configuration snapshot and small env parsing helpers.
2//!
3//! This is intentionally a narrow data surface first: it makes effective
4//! `FERRUM_*` overrides visible in health and bench artifacts while the
5//! hot-path env reads are migrated to typed config structs.
6
7use serde::{Deserialize, Serialize};
8use std::ffi::OsString;
9use std::sync::RwLock;
10use std::{collections::BTreeMap, path::PathBuf};
11
12/// Process-wide runtime snapshot, installed once at the composition root.
13///
14/// This is the single env-bridge seam the test-architecture goal asks for:
15/// the CLI (`serve`/`run`/`bench`) captures `FERRUM_*` via
16/// [`RuntimeConfigSnapshot::capture_current`] and installs it here when it
17/// applies the snapshot to the engine config; model code downstream reads
18/// [`active_runtime_snapshot`] instead of `std::env`, so no model/engine
19/// module freezes its own env config. Re-installable (RwLock, not OnceLock)
20/// so per-construction test paths can vary it after `std::env::set_var`.
21static ACTIVE_SNAPSHOT: RwLock<Option<RuntimeConfigSnapshot>> = RwLock::new(None);
22
23/// Install the process-wide runtime snapshot resolved at the composition root.
24pub fn install_runtime_snapshot(snapshot: RuntimeConfigSnapshot) {
25    *ACTIVE_SNAPSHOT
26        .write()
27        .expect("runtime snapshot lock poisoned") = Some(snapshot);
28}
29
30/// The installed runtime snapshot, or an empty snapshot when none was
31/// installed (unit tests that do not exercise runtime knobs see defaults).
32pub fn active_runtime_snapshot() -> RuntimeConfigSnapshot {
33    ACTIVE_SNAPSHOT
34        .read()
35        .expect("runtime snapshot lock poisoned")
36        .clone()
37        .unwrap_or_default()
38}
39
40/// Stable snapshot of non-default runtime configuration visible to the process.
41#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
42pub struct RuntimeConfigSnapshot {
43    /// Sorted by key for stable JSON and machine-readable diffs.
44    pub entries: Vec<RuntimeConfigEntry>,
45}
46
47impl RuntimeConfigSnapshot {
48    /// Capture all currently set `FERRUM_*` env overrides.
49    pub fn capture_current() -> Self {
50        Self::from_os_env_vars(std::env::vars_os())
51    }
52
53    fn from_os_env_vars(vars: impl IntoIterator<Item = (OsString, OsString)>) -> Self {
54        Self::from_env_vars(vars.into_iter().filter_map(|(key, value)| {
55            let key = key.into_string().ok()?;
56            if !key.starts_with("FERRUM_") {
57                return None;
58            }
59            // Unrelated OS values need not be Unicode. Ferrum overrides retain
60            // their original text so the typed parser can reject invalid values.
61            let value = value
62                .into_string()
63                .unwrap_or_else(|_| panic!("environment variable {key} must contain Unicode"));
64            Some((key, value))
65        }))
66    }
67
68    /// Build a snapshot from a supplied environment map or iterator.
69    pub fn from_env_vars<I, K, V>(vars: I) -> Self
70    where
71        I: IntoIterator<Item = (K, V)>,
72        K: Into<String>,
73        V: Into<String>,
74    {
75        let mut sorted = BTreeMap::new();
76        for (key, value) in vars {
77            let key = key.into();
78            if key.starts_with("FERRUM_") {
79                sorted.insert(key, value.into());
80            }
81        }
82
83        Self {
84            entries: sorted
85                .into_iter()
86                .map(|(key, effective_value)| RuntimeConfigEntry {
87                    affects: infer_effects(&key),
88                    key,
89                    effective_value,
90                    source: RuntimeConfigSource::Env,
91                })
92                .collect(),
93        }
94    }
95
96    /// Build a stable snapshot from explicit entries. Later entries for the
97    /// same key replace earlier entries.
98    pub fn from_entries<I>(entries: I) -> Self
99    where
100        I: IntoIterator<Item = RuntimeConfigEntry>,
101    {
102        let mut sorted = BTreeMap::new();
103        for entry in entries {
104            sorted.insert(entry.key.clone(), entry);
105        }
106        Self {
107            entries: sorted.into_values().collect(),
108        }
109    }
110
111    /// Insert or replace one effective value, preserving stable key order.
112    pub fn upsert(
113        &mut self,
114        key: impl Into<String>,
115        effective_value: impl Into<String>,
116        source: RuntimeConfigSource,
117    ) {
118        self.upsert_entry(RuntimeConfigEntry::new(key, effective_value, source));
119    }
120
121    /// Insert or replace one explicit entry, preserving stable key order.
122    pub fn upsert_entry(&mut self, entry: RuntimeConfigEntry) {
123        let mut entries = std::mem::take(&mut self.entries);
124        entries.retain(|existing| existing.key != entry.key);
125        entries.push(entry);
126        *self = Self::from_entries(entries);
127    }
128
129    /// Return a snapshot with one additional effective value.
130    pub fn with_entry(
131        mut self,
132        key: impl Into<String>,
133        effective_value: impl Into<String>,
134        source: RuntimeConfigSource,
135    ) -> Self {
136        self.upsert(key, effective_value, source);
137        self
138    }
139}
140
141/// One effective config value in a runtime snapshot.
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct RuntimeConfigEntry {
144    pub key: String,
145    pub effective_value: String,
146    pub source: RuntimeConfigSource,
147    pub affects: Vec<RuntimeConfigEffect>,
148}
149
150impl RuntimeConfigEntry {
151    pub fn new(
152        key: impl Into<String>,
153        effective_value: impl Into<String>,
154        source: RuntimeConfigSource,
155    ) -> Self {
156        let key = key.into();
157        Self {
158            affects: infer_effects(&key),
159            key,
160            effective_value: effective_value.into(),
161            source,
162        }
163    }
164}
165
166/// Source of an effective config value.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "snake_case")]
169pub enum RuntimeConfigSource {
170    Default,
171    ConfigFile,
172    Cli,
173    Env,
174    ScriptCase,
175    MemoryProfile,
176}
177
178/// Impact classes used by config snapshots and artifact diffs.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum RuntimeConfigEffect {
182    Correctness,
183    Performance,
184    Memory,
185    Diagnostics,
186}
187
188/// Tri-state env override used by paths that distinguish unset from forced off.
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "snake_case")]
191pub enum EnvTriState {
192    Default,
193    ForcedOff,
194    ForcedOn,
195}
196
197pub fn parse_bool_env_value(raw: &str) -> Result<bool, String> {
198    match raw.trim().to_ascii_lowercase().as_str() {
199        "1" | "true" | "yes" | "on" => Ok(true),
200        "0" | "false" | "no" | "off" => Ok(false),
201        other => Err(format!("invalid boolean env value: {other:?}")),
202    }
203}
204
205pub fn parse_usize_env_value(raw: &str) -> Result<usize, String> {
206    raw.trim()
207        .parse::<usize>()
208        .map_err(|_| format!("invalid integer env value: {raw:?}"))
209}
210
211pub fn parse_path_env_value(raw: &str) -> Result<PathBuf, String> {
212    let trimmed = raw.trim();
213    if trimmed.is_empty() {
214        return Err("path env value must not be empty".to_string());
215    }
216    Ok(PathBuf::from(trimmed))
217}
218
219pub fn parse_tri_state_env_value(raw: Option<&str>) -> Result<EnvTriState, String> {
220    let Some(raw) = raw else {
221        return Ok(EnvTriState::Default);
222    };
223    if raw.trim().is_empty() {
224        return Ok(EnvTriState::Default);
225    }
226    Ok(if parse_bool_env_value(raw)? {
227        EnvTriState::ForcedOn
228    } else {
229        EnvTriState::ForcedOff
230    })
231}
232
233fn infer_effects(key: &str) -> Vec<RuntimeConfigEffect> {
234    let mut effects = Vec::new();
235
236    if key.contains("DIAG")
237        || key.contains("PROF")
238        || key.contains("TRACE")
239        || key.contains("DUMP")
240        || key.contains("LOG_CONFIG")
241        || key.contains("CAPTURE")
242        || key.contains("DEBUG")
243    {
244        effects.push(RuntimeConfigEffect::Diagnostics);
245    }
246
247    if key.contains("KV")
248        || key.contains("BATCHED_TOKENS")
249        || key.contains("PAGED_MAX_SEQS")
250        || key.contains("MODEL_LEN")
251        || key.contains("FIT_POLICY")
252        || key.contains("STATE_MAX_SLOTS")
253        || key.contains("REUSABLE_EXECUTION")
254        || key.contains("MEMORY")
255    {
256        effects.push(RuntimeConfigEffect::Memory);
257    }
258
259    if key.contains("PREFIX_CACHE")
260        || key.contains("MODEL_PATH")
261        || key.contains("MODEL_LEN")
262        || key.contains("FIT_POLICY")
263        || key.contains("RUNTIME_MEMORY_BUDGET")
264        || key.contains("NATIVE")
265        || key.contains("ARTIFACT")
266        || key.contains("SPEC_")
267        || key.contains("REF_")
268        || key.contains("DTYPE")
269        || key.contains("ATTENTION_POLICY")
270        || key.contains("REUSABLE_EXECUTION")
271    {
272        effects.push(RuntimeConfigEffect::Correctness);
273    }
274
275    if effects.is_empty()
276        || key.contains("MOE")
277        || key.contains("VLLM")
278        || key.contains("MARLIN")
279        || key.contains("PAGED")
280        || key.contains("GRAPH")
281        || key.contains("SCHED")
282        || key.contains("BATCH")
283        || key.contains("ATTN")
284        || key.contains("ATTENTION_POLICY")
285        || key.contains("FLASH")
286        || key.contains("CUDA")
287        || key.contains("TRITON")
288        || key.contains("GREEDY")
289        || key.contains("REUSABLE_EXECUTION")
290        || key.contains("FA")
291    {
292        effects.push(RuntimeConfigEffect::Performance);
293    }
294
295    effects.sort();
296    effects.dedup();
297    effects
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn parses_boolean_values() {
306        assert_eq!(parse_bool_env_value("1").unwrap(), true);
307        assert_eq!(parse_bool_env_value("off").unwrap(), false);
308        assert!(parse_bool_env_value("maybe").is_err());
309    }
310
311    #[test]
312    fn parses_integer_values() {
313        assert_eq!(parse_usize_env_value("4096").unwrap(), 4096);
314        assert!(parse_usize_env_value("-1").is_err());
315        assert!(parse_usize_env_value("many").is_err());
316    }
317
318    #[test]
319    fn parses_path_values() {
320        assert_eq!(
321            parse_path_env_value("/tmp/model").unwrap(),
322            PathBuf::from("/tmp/model")
323        );
324        assert!(parse_path_env_value("   ").is_err());
325    }
326
327    #[test]
328    fn parses_tri_state_values() {
329        assert_eq!(
330            parse_tri_state_env_value(None).unwrap(),
331            EnvTriState::Default
332        );
333        assert_eq!(
334            parse_tri_state_env_value(Some("0")).unwrap(),
335            EnvTriState::ForcedOff
336        );
337        assert_eq!(
338            parse_tri_state_env_value(Some("on")).unwrap(),
339            EnvTriState::ForcedOn
340        );
341        assert!(parse_tri_state_env_value(Some("auto")).is_err());
342    }
343
344    #[test]
345    fn attention_policy_is_a_correctness_and_performance_input() {
346        let snapshot =
347            RuntimeConfigSnapshot::from_env_vars([("FERRUM_ATTENTION_POLICY", "native-adaptive")]);
348        let entry = snapshot.entries.first().expect("attention policy entry");
349        assert!(entry.affects.contains(&RuntimeConfigEffect::Correctness));
350        assert!(entry.affects.contains(&RuntimeConfigEffect::Performance));
351    }
352
353    #[cfg(unix)]
354    #[test]
355    fn snapshot_ignores_unrelated_non_unicode_environment() {
356        use std::os::unix::ffi::OsStringExt;
357
358        let snapshot = RuntimeConfigSnapshot::from_os_env_vars([
359            (
360                OsString::from("PATH"),
361                OsString::from_vec(b"/bin:\xff".to_vec()),
362            ),
363            (
364                OsString::from_vec(b"OTHER_\xff".to_vec()),
365                OsString::from("ignored"),
366            ),
367            (
368                OsString::from("FERRUM_KV_MAX_BLOCKS"),
369                OsString::from(" 4096 "),
370            ),
371            (
372                OsString::from("FERRUM_ATTENTION_POLICY"),
373                OsString::from("native-adaptive"),
374            ),
375        ]);
376
377        assert_eq!(
378            snapshot,
379            RuntimeConfigSnapshot::from_env_vars([
380                ("FERRUM_ATTENTION_POLICY", "native-adaptive"),
381                ("FERRUM_KV_MAX_BLOCKS", " 4096 "),
382            ])
383        );
384    }
385
386    #[test]
387    fn os_environment_preserves_invalid_override_for_typed_validation() {
388        let snapshot = RuntimeConfigSnapshot::from_os_env_vars([(
389            OsString::from("FERRUM_KV_MAX_BLOCKS"),
390            OsString::from("many"),
391        )]);
392        assert_eq!(snapshot.entries[0].effective_value, "many");
393        let error = crate::EngineConfig::default()
394            .apply_runtime_config_snapshot(&snapshot)
395            .unwrap_err();
396        assert!(error.contains("FERRUM_KV_MAX_BLOCKS"));
397    }
398
399    #[cfg(unix)]
400    #[test]
401    #[should_panic(expected = "environment variable FERRUM_KV_MAX_BLOCKS must contain Unicode")]
402    fn snapshot_rejects_non_unicode_ferrum_override() {
403        use std::os::unix::ffi::OsStringExt;
404
405        RuntimeConfigSnapshot::from_os_env_vars([(
406            OsString::from("FERRUM_KV_MAX_BLOCKS"),
407            OsString::from_vec(vec![0xff]),
408        )]);
409    }
410
411    #[test]
412    fn snapshot_is_sorted_and_classified() {
413        let snapshot = RuntimeConfigSnapshot::from_env_vars([
414            ("OTHER_ENV", "ignored"),
415            ("FERRUM_FA2_NATIVE_ARTIFACT", "/tmp/libferrum_native_fa2.a"),
416            ("FERRUM_PREFIX_CACHE", "1"),
417            ("FERRUM_MOE_GRAPH", "1"),
418            ("FERRUM_REUSABLE_EXECUTION", "1"),
419        ]);
420        let keys: Vec<_> = snapshot
421            .entries
422            .iter()
423            .map(|entry| entry.key.as_str())
424            .collect();
425        assert_eq!(
426            keys,
427            vec![
428                "FERRUM_FA2_NATIVE_ARTIFACT",
429                "FERRUM_MOE_GRAPH",
430                "FERRUM_PREFIX_CACHE",
431                "FERRUM_REUSABLE_EXECUTION"
432            ]
433        );
434        assert_eq!(snapshot.entries[0].source, RuntimeConfigSource::Env);
435        assert!(snapshot.entries[0]
436            .affects
437            .contains(&RuntimeConfigEffect::Correctness));
438        assert!(snapshot.entries[0]
439            .affects
440            .contains(&RuntimeConfigEffect::Performance));
441        assert!(snapshot.entries[1]
442            .affects
443            .contains(&RuntimeConfigEffect::Performance));
444        assert!(snapshot.entries[2]
445            .affects
446            .contains(&RuntimeConfigEffect::Correctness));
447        for effect in [
448            RuntimeConfigEffect::Correctness,
449            RuntimeConfigEffect::Performance,
450            RuntimeConfigEffect::Memory,
451        ] {
452            assert!(snapshot.entries[3].affects.contains(&effect));
453        }
454    }
455
456    #[test]
457    fn upsert_preserves_non_env_source_and_stable_order() {
458        let mut snapshot = RuntimeConfigSnapshot::from_env_vars([
459            ("FERRUM_KV_DTYPE", "fp16"),
460            ("FERRUM_MOE_GRAPH", "1"),
461        ]);
462        snapshot.upsert("FERRUM_KV_DTYPE", "int8", RuntimeConfigSource::Cli);
463        snapshot.upsert(
464            "FERRUM_PROFILE_JSONL",
465            "/tmp/profile.jsonl",
466            RuntimeConfigSource::Cli,
467        );
468
469        let keys: Vec<_> = snapshot
470            .entries
471            .iter()
472            .map(|entry| entry.key.as_str())
473            .collect();
474        assert_eq!(
475            keys,
476            [
477                "FERRUM_KV_DTYPE",
478                "FERRUM_MOE_GRAPH",
479                "FERRUM_PROFILE_JSONL"
480            ]
481        );
482        let kv = snapshot
483            .entries
484            .iter()
485            .find(|entry| entry.key == "FERRUM_KV_DTYPE")
486            .unwrap();
487        assert_eq!(kv.effective_value, "int8");
488        assert_eq!(kv.source, RuntimeConfigSource::Cli);
489        assert!(kv.affects.contains(&RuntimeConfigEffect::Correctness));
490
491        let profile = snapshot
492            .entries
493            .iter()
494            .find(|entry| entry.key == "FERRUM_PROFILE_JSONL")
495            .unwrap();
496        assert_eq!(profile.source, RuntimeConfigSource::Cli);
497        assert!(profile.affects.contains(&RuntimeConfigEffect::Diagnostics));
498    }
499}