Skip to main content

cortiq_engine/
swarm.rs

1//! Dynamic per-token skill routing with hysteresis (spec §9 made
2//! runtime; VMF-2026 experiment №2).
3//!
4//! The recon-argmin error E(skill) is computed against the rolling φ
5//! (EMA of the router layer's hidden state — on-policy, fireball-style).
6//! Switching uses TWO thresholds — a first-order-transition analogue of
7//! the VMF condensation potential V(𝒲,T) = D(T²−T₀²)𝒲² − E·T·𝒲³ +
8//! (λ/4)𝒲⁴, whose cubic term opens a barrier between the "off" (φ far
9//! from any skill) and "on" (φ inside a skill's subspace) minima:
10//!   - activate a skill only when its E drops below `e_on` (nucleation);
11//!   - abandon the active skill only when its E rises above `e_off`
12//!     (> e_on), or a rival beats it by more than `margin`.
13//!
14//! The barrier `e_off − e_on` is exactly what suppresses thrashing at
15//! domain boundaries (the very effect a single threshold cannot give).
16
17use cortiq_core::quant::f16_to_f32;
18use cortiq_core::SelectionDescriptor;
19use base64::Engine as _;
20
21/// One routable skill's precomputed subspace (decoded once).
22pub struct RoutableSkill {
23    /// Index into model.header.skills (pipeline.set_active_skill).
24    pub idx: usize,
25    pub id: String,
26    pub phi_layer: usize,
27    mean: Vec<f32>,
28    basis: Vec<f32>,
29    rank: usize,
30}
31
32fn decode_f16(b64: &str) -> Option<Vec<f32>> {
33    let bytes = base64::engine::general_purpose::STANDARD.decode(b64).ok()?;
34    Some(
35        bytes
36            .chunks_exact(2)
37            .map(|c| f16_to_f32(u16::from_le_bytes([c[0], c[1]])))
38            .collect(),
39    )
40}
41
42impl RoutableSkill {
43    pub fn from_descriptor(
44        idx: usize,
45        id: String,
46        sel: &SelectionDescriptor,
47        hidden: usize,
48    ) -> Option<Self> {
49        if sel.metric != "mse" {
50            return None;
51        }
52        let mean = decode_f16(&sel.mean)?;
53        let basis = decode_f16(&sel.basis)?;
54        if mean.len() != hidden || basis.len() != sel.rank * hidden {
55            return None;
56        }
57        Some(Self {
58            idx,
59            id,
60            phi_layer: sel.phi_layer,
61            mean,
62            basis,
63            rank: sel.rank,
64        })
65    }
66
67    /// Normalized reconstruction error E = ‖r − BBᵀr‖²/‖φ‖²,
68    /// r = φ − mean (identical math to router::route).
69    pub fn error(&self, phi: &[f32]) -> f32 {
70        let hidden = self.mean.len();
71        if phi.len() != hidden {
72            return f32::INFINITY;
73        }
74        let r: Vec<f32> = phi.iter().zip(&self.mean).map(|(p, m)| p - m).collect();
75        let rr: f32 = r.iter().map(|v| v * v).sum();
76        let pp: f32 = phi.iter().map(|v| v * v).sum();
77        let mut proj = 0f32;
78        for k in 0..self.rank {
79            let row = &self.basis[k * hidden..(k + 1) * hidden];
80            let c: f32 = row.iter().zip(&r).map(|(b, v)| b * v).sum();
81            proj += c * c;
82        }
83        (rr - proj).max(0.0) / pp.max(1e-12)
84    }
85}
86
87/// Hysteresis controller for dynamic routing.
88pub struct DynRouter {
89    pub skills: Vec<RoutableSkill>,
90    /// Nucleation threshold: activate below this E.
91    pub e_on: f32,
92    /// Abandon threshold: drop the active skill above this E (> e_on).
93    pub e_off: f32,
94    /// A rival must beat the active skill by this margin to steal it.
95    pub margin: f32,
96    /// Re-route every `period` tokens (dispatch amortization; 1 = every).
97    pub period: usize,
98    /// Currently active skill index (model.header.skills), None = base.
99    active: Option<usize>,
100    tick: usize,
101    /// Switch log for demo/telemetry: (token#, from_id, to_id).
102    pub switches: Vec<(usize, Option<String>, Option<String>)>,
103    /// Min recon error E at the last evaluation tick (telemetry): low E =
104    /// high coherence with a skill subspace. INFINITY before any eval.
105    last_best_e: f32,
106}
107
108impl DynRouter {
109    pub fn new(skills: Vec<RoutableSkill>) -> Self {
110        let e_on = std::env::var("CMF_ROUTE_EON").ok().and_then(|v| v.parse().ok()).unwrap_or(0.62);
111        let e_off = std::env::var("CMF_ROUTE_EOFF").ok().and_then(|v| v.parse().ok()).unwrap_or(0.74);
112        let margin = std::env::var("CMF_ROUTE_MARGIN").ok().and_then(|v| v.parse().ok()).unwrap_or(0.03);
113        let period = std::env::var("CMF_ROUTE_PERIOD").ok().and_then(|v| v.parse().ok()).unwrap_or(8usize).max(1);
114        Self {
115            skills,
116            e_on,
117            e_off,
118            margin,
119            period,
120            active: None,
121            tick: 0,
122            switches: Vec::new(),
123            last_best_e: f32::INFINITY,
124        }
125    }
126
127    /// The single phi_layer to capture (skills share it in the swarm;
128    /// if they differ, the first is used and a warning is the caller's).
129    pub fn phi_layer(&self) -> Option<usize> {
130        self.skills.first().map(|s| s.phi_layer)
131    }
132
133    /// Decide the active skill for the next window given the current φ.
134    /// Returns Some(new_active) when a switch is warranted (caller calls
135    /// pipeline.set_active_skill), else None (unchanged). `token_no` is
136    /// only for the switch log.
137    pub fn step(&mut self, phi: &[f32], token_no: usize) -> Option<Option<usize>> {
138        self.tick += 1;
139        if self.tick % self.period != 0 || phi.is_empty() || self.skills.is_empty() {
140            return None;
141        }
142        // Score all skills.
143        let mut best_idx = None;
144        let mut best_e = f32::INFINITY;
145        let mut active_e = f32::INFINITY;
146        for s in &self.skills {
147            let e = s.error(phi);
148            if Some(s.idx) == self.active {
149                active_e = e;
150            }
151            if e < best_e {
152                best_e = e;
153                best_idx = Some(s.idx);
154            }
155        }
156
157        self.last_best_e = best_e; // telemetry: coherence at this eval
158
159        let next = decide(
160            self.active, active_e, best_idx, best_e, self.e_on, self.e_off, self.margin,
161        );
162
163        if next != self.active {
164            let from = self.active.and_then(|i| self.skills.iter().find(|s| s.idx == i)).map(|s| s.id.clone());
165            let to = next.and_then(|i| self.skills.iter().find(|s| s.idx == i)).map(|s| s.id.clone());
166            self.switches.push((token_no, from, to));
167            self.active = next;
168            return Some(next);
169        }
170        None
171    }
172
173    pub fn active(&self) -> Option<usize> {
174        self.active
175    }
176
177    /// Id of the currently active skill (telemetry), None = backbone.
178    pub fn active_id(&self) -> Option<String> {
179        self.active
180            .and_then(|i| self.skills.iter().find(|s| s.idx == i))
181            .map(|s| s.id.clone())
182    }
183
184    /// Min recon error E at the last evaluation (telemetry coherence).
185    pub fn last_best_e(&self) -> f32 {
186        self.last_best_e
187    }
188
189    /// Reset per-generation state (active=backbone, empty log, tick 0) so
190    /// the router matches a freshly-reset pipeline overlay.
191    pub fn reset(&mut self) {
192        self.active = None;
193        self.tick = 0;
194        self.switches.clear();
195        self.last_best_e = f32::INFINITY;
196    }
197}
198
199/// Pure hysteresis decision (first-order transition analogue): given the
200/// current active skill, its error, and the best rival, return the next
201/// active. Two thresholds e_on < e_off open the anti-thrash barrier.
202#[allow(clippy::too_many_arguments)]
203pub fn decide(
204    active: Option<usize>,
205    active_e: f32,
206    best_idx: Option<usize>,
207    best_e: f32,
208    e_on: f32,
209    e_off: f32,
210    margin: f32,
211) -> Option<usize> {
212    match active {
213        // Nucleation: activate the best only if it clears e_on.
214        None => {
215            if best_e < e_on {
216                best_idx
217            } else {
218                None
219            }
220        }
221        Some(cur) => {
222            if active_e > e_off {
223                // Melted: re-nucleate, else fall back to backbone.
224                if best_e < e_on {
225                    best_idx
226                } else {
227                    None
228                }
229            } else if best_idx != Some(cur) && best_e + margin < active_e {
230                // Rival decisively better while active still holds.
231                best_idx
232            } else {
233                Some(cur)
234            }
235        }
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::decide;
242
243    #[test]
244    fn hysteresis_barrier_suppresses_thrashing() {
245        let (e_on, e_off, m) = (0.60, 0.75, 0.03);
246
247        // From backbone: does NOT activate in the barrier band [e_on,e_off).
248        assert_eq!(decide(None, f32::INFINITY, Some(0), 0.70, e_on, e_off, m), None);
249        // From backbone: activates below e_on (nucleation).
250        assert_eq!(decide(None, f32::INFINITY, Some(0), 0.55, e_on, e_off, m), Some(0));
251
252        // Active skill 0 at E=0.70 (in the band) STAYS — this is the whole
253        // point: a single threshold at 0.62 would have flip-flopped here.
254        assert_eq!(decide(Some(0), 0.70, Some(1), 0.68, e_on, e_off, m), Some(0));
255        // Active melts above e_off → re-nucleate to the qualifying rival.
256        assert_eq!(decide(Some(0), 0.80, Some(1), 0.55, e_on, e_off, m), Some(1));
257        // Active melts but no rival clears e_on → back to backbone.
258        assert_eq!(decide(Some(0), 0.80, Some(1), 0.70, e_on, e_off, m), None);
259        // Rival must beat active by `margin`, not merely be lower.
260        assert_eq!(decide(Some(0), 0.70, Some(1), 0.69, e_on, e_off, m), Some(0));
261        assert_eq!(decide(Some(0), 0.70, Some(1), 0.66, e_on, e_off, m), Some(1));
262    }
263}