1use base64::Engine as _;
16use cortiq_core::SelectionDescriptor;
17use cortiq_core::quant::f16_to_f32;
18
19pub struct RoutableSkill {
21 pub idx: usize,
23 pub id: String,
24 pub phi_layer: usize,
25 mean: Vec<f32>,
26 basis: Vec<f32>,
27 rank: usize,
28}
29
30fn decode_f16(b64: &str) -> Option<Vec<f32>> {
31 let bytes = base64::engine::general_purpose::STANDARD.decode(b64).ok()?;
32 Some(
33 bytes
34 .chunks_exact(2)
35 .map(|c| f16_to_f32(u16::from_le_bytes([c[0], c[1]])))
36 .collect(),
37 )
38}
39
40impl RoutableSkill {
41 pub fn from_descriptor(
42 idx: usize,
43 id: String,
44 sel: &SelectionDescriptor,
45 hidden: usize,
46 ) -> Option<Self> {
47 if sel.metric != "mse" {
48 return None;
49 }
50 let mean = decode_f16(&sel.mean)?;
51 let basis = decode_f16(&sel.basis)?;
52 if mean.len() != hidden || basis.len() != sel.rank * hidden {
53 return None;
54 }
55 Some(Self {
56 idx,
57 id,
58 phi_layer: sel.phi_layer,
59 mean,
60 basis,
61 rank: sel.rank,
62 })
63 }
64
65 pub fn error(&self, phi: &[f32]) -> f32 {
68 let hidden = self.mean.len();
69 if phi.len() != hidden {
70 return f32::INFINITY;
71 }
72 let r: Vec<f32> = phi.iter().zip(&self.mean).map(|(p, m)| p - m).collect();
73 let rr: f32 = r.iter().map(|v| v * v).sum();
74 let pp: f32 = phi.iter().map(|v| v * v).sum();
75 let mut proj = 0f32;
76 for k in 0..self.rank {
77 let row = &self.basis[k * hidden..(k + 1) * hidden];
78 let c: f32 = row.iter().zip(&r).map(|(b, v)| b * v).sum();
79 proj += c * c;
80 }
81 (rr - proj).max(0.0) / pp.max(1e-12)
82 }
83}
84
85pub struct DynRouter {
87 pub skills: Vec<RoutableSkill>,
88 pub e_on: f32,
90 pub e_off: f32,
92 pub margin: f32,
94 pub period: usize,
96 active: Option<usize>,
98 tick: usize,
99 pub switches: Vec<(usize, Option<String>, Option<String>)>,
101 last_best_e: f32,
104}
105
106impl DynRouter {
107 pub fn new(skills: Vec<RoutableSkill>) -> Self {
108 let e_on = std::env::var("CMF_ROUTE_EON")
109 .ok()
110 .and_then(|v| v.parse().ok())
111 .unwrap_or(0.62);
112 let e_off = std::env::var("CMF_ROUTE_EOFF")
113 .ok()
114 .and_then(|v| v.parse().ok())
115 .unwrap_or(0.74);
116 let margin = std::env::var("CMF_ROUTE_MARGIN")
117 .ok()
118 .and_then(|v| v.parse().ok())
119 .unwrap_or(0.03);
120 let period = std::env::var("CMF_ROUTE_PERIOD")
121 .ok()
122 .and_then(|v| v.parse().ok())
123 .unwrap_or(8usize)
124 .max(1);
125 Self {
126 skills,
127 e_on,
128 e_off,
129 margin,
130 period,
131 active: None,
132 tick: 0,
133 switches: Vec::new(),
134 last_best_e: f32::INFINITY,
135 }
136 }
137
138 pub fn phi_layer(&self) -> Option<usize> {
141 self.skills.first().map(|s| s.phi_layer)
142 }
143
144 pub fn step(&mut self, phi: &[f32], token_no: usize) -> Option<Option<usize>> {
149 self.tick += 1;
150 if self.tick % self.period != 0 || phi.is_empty() || self.skills.is_empty() {
151 return None;
152 }
153 let mut best_idx = None;
155 let mut best_e = f32::INFINITY;
156 let mut active_e = f32::INFINITY;
157 for s in &self.skills {
158 let e = s.error(phi);
159 if Some(s.idx) == self.active {
160 active_e = e;
161 }
162 if e < best_e {
163 best_e = e;
164 best_idx = Some(s.idx);
165 }
166 }
167
168 self.last_best_e = best_e; let next = decide(
171 self.active,
172 active_e,
173 best_idx,
174 best_e,
175 self.e_on,
176 self.e_off,
177 self.margin,
178 );
179
180 if next != self.active {
181 let from = self
182 .active
183 .and_then(|i| self.skills.iter().find(|s| s.idx == i))
184 .map(|s| s.id.clone());
185 let to = next
186 .and_then(|i| self.skills.iter().find(|s| s.idx == i))
187 .map(|s| s.id.clone());
188 self.switches.push((token_no, from, to));
189 self.active = next;
190 return Some(next);
191 }
192 None
193 }
194
195 pub fn active(&self) -> Option<usize> {
196 self.active
197 }
198
199 pub fn active_id(&self) -> Option<String> {
201 self.active
202 .and_then(|i| self.skills.iter().find(|s| s.idx == i))
203 .map(|s| s.id.clone())
204 }
205
206 pub fn last_best_e(&self) -> f32 {
208 self.last_best_e
209 }
210
211 pub fn reset(&mut self) {
214 self.active = None;
215 self.tick = 0;
216 self.switches.clear();
217 self.last_best_e = f32::INFINITY;
218 }
219}
220
221#[allow(clippy::too_many_arguments)]
225pub fn decide(
226 active: Option<usize>,
227 active_e: f32,
228 best_idx: Option<usize>,
229 best_e: f32,
230 e_on: f32,
231 e_off: f32,
232 margin: f32,
233) -> Option<usize> {
234 match active {
235 None => {
237 if best_e < e_on {
238 best_idx
239 } else {
240 None
241 }
242 }
243 Some(cur) => {
244 if active_e > e_off {
245 if best_e < e_on { best_idx } else { None }
247 } else if best_idx != Some(cur) && best_e + margin < active_e {
248 best_idx
250 } else {
251 Some(cur)
252 }
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::decide;
260
261 #[test]
262 fn hysteresis_barrier_suppresses_thrashing() {
263 let (e_on, e_off, m) = (0.60, 0.75, 0.03);
264
265 assert_eq!(
267 decide(None, f32::INFINITY, Some(0), 0.70, e_on, e_off, m),
268 None
269 );
270 assert_eq!(
272 decide(None, f32::INFINITY, Some(0), 0.55, e_on, e_off, m),
273 Some(0)
274 );
275
276 assert_eq!(
279 decide(Some(0), 0.70, Some(1), 0.68, e_on, e_off, m),
280 Some(0)
281 );
282 assert_eq!(
284 decide(Some(0), 0.80, Some(1), 0.55, e_on, e_off, m),
285 Some(1)
286 );
287 assert_eq!(decide(Some(0), 0.80, Some(1), 0.70, e_on, e_off, m), None);
289 assert_eq!(
291 decide(Some(0), 0.70, Some(1), 0.69, e_on, e_off, m),
292 Some(0)
293 );
294 assert_eq!(
295 decide(Some(0), 0.70, Some(1), 0.66, e_on, e_off, m),
296 Some(1)
297 );
298 }
299}