1use cortiq_core::quant::f16_to_f32;
18use cortiq_core::SelectionDescriptor;
19use base64::Engine as _;
20
21pub struct RoutableSkill {
23 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 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
87pub struct DynRouter {
89 pub skills: Vec<RoutableSkill>,
90 pub e_on: f32,
92 pub e_off: f32,
94 pub margin: f32,
96 pub period: usize,
98 active: Option<usize>,
100 tick: usize,
101 pub switches: Vec<(usize, Option<String>, Option<String>)>,
103 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 pub fn phi_layer(&self) -> Option<usize> {
130 self.skills.first().map(|s| s.phi_layer)
131 }
132
133 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 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; 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 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 pub fn last_best_e(&self) -> f32 {
186 self.last_best_e
187 }
188
189 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#[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 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 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 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 assert_eq!(decide(None, f32::INFINITY, Some(0), 0.70, e_on, e_off, m), None);
249 assert_eq!(decide(None, f32::INFINITY, Some(0), 0.55, e_on, e_off, m), Some(0));
251
252 assert_eq!(decide(Some(0), 0.70, Some(1), 0.68, e_on, e_off, m), Some(0));
255 assert_eq!(decide(Some(0), 0.80, Some(1), 0.55, e_on, e_off, m), Some(1));
257 assert_eq!(decide(Some(0), 0.80, Some(1), 0.70, e_on, e_off, m), None);
259 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}