1use crate::time_compat::Instant;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct LearningSignal {
12 pub query_embedding: Vec<f32>,
14 pub gradient_estimate: Vec<f32>,
16 pub quality_score: f32,
18 #[serde(skip)]
20 pub timestamp: Option<Instant>,
21 pub metadata: SignalMetadata,
23}
24
25#[derive(Clone, Debug, Default, Serialize, Deserialize)]
27pub struct SignalMetadata {
28 pub trajectory_id: u64,
30 pub step_count: usize,
32 pub model_route: Option<String>,
34 pub tags: HashMap<String, String>,
36}
37
38impl LearningSignal {
39 pub fn from_trajectory(trajectory: &QueryTrajectory) -> Self {
41 let gradient = Self::estimate_gradient(trajectory);
42
43 Self {
44 query_embedding: trajectory.query_embedding.clone(),
45 gradient_estimate: gradient,
46 quality_score: trajectory.final_quality,
47 timestamp: Some(Instant::now()),
48 metadata: SignalMetadata {
49 trajectory_id: trajectory.id,
50 step_count: trajectory.steps.len(),
51 model_route: trajectory.model_route.clone(),
52 tags: HashMap::new(),
53 },
54 }
55 }
56
57 pub fn with_gradient(embedding: Vec<f32>, gradient: Vec<f32>, quality: f32) -> Self {
59 Self {
60 query_embedding: embedding,
61 gradient_estimate: gradient,
62 quality_score: quality,
63 timestamp: Some(Instant::now()),
64 metadata: SignalMetadata::default(),
65 }
66 }
67
68 fn estimate_gradient(trajectory: &QueryTrajectory) -> Vec<f32> {
70 if trajectory.steps.is_empty() {
71 return trajectory.query_embedding.clone();
72 }
73
74 let dim = trajectory.query_embedding.len();
75 let mut gradient = vec![0.0f32; dim];
76
77 let baseline =
79 trajectory.steps.iter().map(|s| s.reward).sum::<f32>() / trajectory.steps.len() as f32;
80
81 for step in &trajectory.steps {
83 let advantage = step.reward - baseline;
84 let activation_len = step.activations.len().min(dim);
85 for (grad, &act) in gradient
86 .iter_mut()
87 .zip(step.activations.iter())
88 .take(activation_len)
89 {
90 *grad += advantage * act;
91 }
92 }
93
94 let norm: f32 = gradient.iter().map(|x| x * x).sum::<f32>().sqrt();
96 if norm > 1e-8 {
97 gradient.iter_mut().for_each(|x| *x /= norm);
98 return gradient;
99 }
100
101 let mut fallback = vec![0.0f32; dim];
110 for step in &trajectory.steps {
111 let activation_len = step.activations.len().min(dim);
112 for (grad, &act) in fallback
113 .iter_mut()
114 .zip(step.activations.iter())
115 .take(activation_len)
116 {
117 *grad += step.reward * act;
118 }
119 }
120
121 let fallback_norm: f32 = fallback.iter().map(|x| x * x).sum::<f32>().sqrt();
122 if fallback_norm > 1e-8 {
123 fallback.iter_mut().for_each(|x| *x /= fallback_norm);
124 return fallback;
125 }
126
127 gradient
128 }
129
130 pub fn scaled_gradient(&self) -> Vec<f32> {
132 self.gradient_estimate
133 .iter()
134 .map(|&g| g * self.quality_score)
135 .collect()
136 }
137}
138
139#[derive(Clone, Debug, Serialize, Deserialize)]
141pub struct QueryTrajectory {
142 pub id: u64,
144 pub query_embedding: Vec<f32>,
146 pub steps: Vec<TrajectoryStep>,
148 pub final_quality: f32,
150 pub latency_us: u64,
152 pub model_route: Option<String>,
154 pub context_ids: Vec<String>,
156}
157
158impl QueryTrajectory {
159 pub fn new(id: u64, query_embedding: Vec<f32>) -> Self {
161 Self {
162 id,
163 query_embedding,
164 steps: Vec::with_capacity(16),
165 final_quality: 0.0,
166 latency_us: 0,
167 model_route: None,
168 context_ids: Vec::new(),
169 }
170 }
171
172 pub fn add_step(&mut self, step: TrajectoryStep) {
174 self.steps.push(step);
175 }
176
177 pub fn finalize(&mut self, quality: f32, latency_us: u64) {
179 self.final_quality = quality;
180 self.latency_us = latency_us;
181 }
182
183 pub fn total_reward(&self) -> f32 {
185 self.steps.iter().map(|s| s.reward).sum()
186 }
187
188 pub fn avg_reward(&self) -> f32 {
190 if self.steps.is_empty() {
191 0.0
192 } else {
193 self.total_reward() / self.steps.len() as f32
194 }
195 }
196}
197
198#[derive(Clone, Debug, Serialize, Deserialize)]
200pub struct TrajectoryStep {
201 pub activations: Vec<f32>,
203 pub attention_weights: Vec<f32>,
205 pub reward: f32,
207 pub step_idx: usize,
209 pub layer_name: Option<String>,
211}
212
213impl TrajectoryStep {
214 pub fn new(
216 activations: Vec<f32>,
217 attention_weights: Vec<f32>,
218 reward: f32,
219 step_idx: usize,
220 ) -> Self {
221 Self {
222 activations,
223 attention_weights,
224 reward,
225 step_idx,
226 layer_name: None,
227 }
228 }
229
230 pub fn with_layer(mut self, name: &str) -> Self {
232 self.layer_name = Some(name.to_string());
233 self
234 }
235}
236
237#[derive(Clone, Debug, Serialize, Deserialize)]
239pub struct LearnedPattern {
240 pub id: u64,
242 pub centroid: Vec<f32>,
244 pub cluster_size: usize,
246 pub total_weight: f32,
248 pub avg_quality: f32,
250 pub created_at: u64,
252 pub last_accessed: u64,
254 pub access_count: u32,
256 pub pattern_type: PatternType,
258}
259
260#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
262pub enum PatternType {
263 #[default]
264 General,
265 Reasoning,
266 Factual,
267 Creative,
268 CodeGen,
269 Conversational,
270}
271
272impl std::fmt::Display for PatternType {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 match self {
275 PatternType::General => write!(f, "general"),
276 PatternType::Reasoning => write!(f, "reasoning"),
277 PatternType::Factual => write!(f, "factual"),
278 PatternType::Creative => write!(f, "creative"),
279 PatternType::CodeGen => write!(f, "codegen"),
280 PatternType::Conversational => write!(f, "conversational"),
281 }
282 }
283}
284
285impl LearnedPattern {
286 pub fn new(id: u64, centroid: Vec<f32>) -> Self {
288 use crate::time_compat::SystemTime;
289 let now = SystemTime::now().duration_since_epoch().as_secs();
290
291 Self {
292 id,
293 centroid,
294 cluster_size: 1,
295 total_weight: 1.0,
296 avg_quality: 0.0,
297 created_at: now,
298 last_accessed: now,
299 access_count: 0,
300 pattern_type: PatternType::default(),
301 }
302 }
303
304 pub fn merge(&self, other: &Self) -> Self {
306 let total_size = self.cluster_size + other.cluster_size;
307 let w1 = self.cluster_size as f32 / total_size as f32;
308 let w2 = other.cluster_size as f32 / total_size as f32;
309
310 let centroid: Vec<f32> = self
311 .centroid
312 .iter()
313 .zip(&other.centroid)
314 .map(|(&a, &b)| a * w1 + b * w2)
315 .collect();
316
317 Self {
318 id: self.id,
319 centroid,
320 cluster_size: total_size,
321 total_weight: self.total_weight + other.total_weight,
322 avg_quality: self.avg_quality * w1 + other.avg_quality * w2,
323 created_at: self.created_at.min(other.created_at),
324 last_accessed: self.last_accessed.max(other.last_accessed),
325 access_count: self.access_count + other.access_count,
326 pattern_type: self.pattern_type.clone(),
327 }
328 }
329
330 pub fn decay(&mut self, factor: f32) {
332 self.total_weight *= factor;
333 }
334
335 pub fn touch(&mut self) {
337 use crate::time_compat::SystemTime;
338 self.access_count += 1;
339 self.last_accessed = SystemTime::now().duration_since_epoch().as_secs();
340 }
341
342 pub fn should_prune(&self, min_quality: f32, min_accesses: u32, max_age_secs: u64) -> bool {
344 use crate::time_compat::SystemTime;
345 let now = SystemTime::now().duration_since_epoch().as_secs();
346 let age = now.saturating_sub(self.last_accessed);
347
348 self.avg_quality < min_quality && self.access_count < min_accesses && age > max_age_secs
349 }
350
351 pub fn similarity(&self, query: &[f32]) -> f32 {
353 if self.centroid.len() != query.len() {
354 return 0.0;
355 }
356
357 let dot: f32 = self.centroid.iter().zip(query).map(|(a, b)| a * b).sum();
358 let norm_a: f32 = self.centroid.iter().map(|x| x * x).sum::<f32>().sqrt();
359 let norm_b: f32 = query.iter().map(|x| x * x).sum::<f32>().sqrt();
360
361 if norm_a > 1e-8 && norm_b > 1e-8 {
362 dot / (norm_a * norm_b)
363 } else {
364 0.0
365 }
366 }
367}
368
369#[derive(Clone, Debug, Serialize, Deserialize)]
371pub struct SonaConfig {
372 pub hidden_dim: usize,
374 pub embedding_dim: usize,
376 pub micro_lora_rank: usize,
378 pub base_lora_rank: usize,
380 pub micro_lora_lr: f32,
382 pub base_lora_lr: f32,
384 pub ewc_lambda: f32,
386 pub pattern_clusters: usize,
388 pub trajectory_capacity: usize,
390 pub background_interval_ms: u64,
392 pub quality_threshold: f32,
394 pub enable_simd: bool,
396}
397
398impl Default for SonaConfig {
399 fn default() -> Self {
400 Self {
407 hidden_dim: 256,
408 embedding_dim: 256,
409 micro_lora_rank: 2, base_lora_rank: 8, micro_lora_lr: 0.002, base_lora_lr: 0.0001,
413 ewc_lambda: 2000.0, pattern_clusters: 100, trajectory_capacity: 10000,
416 background_interval_ms: 3600000, quality_threshold: 0.15, enable_simd: true,
419 }
420 }
421}
422
423impl SonaConfig {
424 pub fn max_throughput() -> Self {
426 Self {
427 hidden_dim: 256,
428 embedding_dim: 256,
429 micro_lora_rank: 2, base_lora_rank: 4, micro_lora_lr: 0.0005, base_lora_lr: 0.0001,
433 ewc_lambda: 2000.0,
434 pattern_clusters: 100,
435 trajectory_capacity: 5000,
436 background_interval_ms: 7200000, quality_threshold: 0.4,
438 enable_simd: true,
439 }
440 }
441
442 pub fn max_quality() -> Self {
444 Self {
445 hidden_dim: 256,
446 embedding_dim: 256,
447 micro_lora_rank: 2,
448 base_lora_rank: 16, micro_lora_lr: 0.002, base_lora_lr: 0.001, ewc_lambda: 2000.0,
452 pattern_clusters: 100,
453 trajectory_capacity: 20000,
454 background_interval_ms: 1800000, quality_threshold: 0.2, enable_simd: true,
457 }
458 }
459
460 pub fn edge_deployment() -> Self {
462 Self {
463 hidden_dim: 256,
464 embedding_dim: 256,
465 micro_lora_rank: 1, base_lora_rank: 4,
467 micro_lora_lr: 0.001,
468 base_lora_lr: 0.0001,
469 ewc_lambda: 1000.0,
470 pattern_clusters: 50,
471 trajectory_capacity: 200, background_interval_ms: 3600000,
473 quality_threshold: 0.5,
474 enable_simd: true,
475 }
476 }
477
478 pub fn batch_processing() -> Self {
480 Self {
481 hidden_dim: 256,
482 embedding_dim: 256,
483 micro_lora_rank: 2,
484 base_lora_rank: 8,
485 micro_lora_lr: 0.001,
486 base_lora_lr: 0.0001,
487 ewc_lambda: 2000.0,
488 pattern_clusters: 100,
489 trajectory_capacity: 10000,
490 background_interval_ms: 3600000,
491 quality_threshold: 0.3,
492 enable_simd: true,
493 }
494 }
495
496 pub fn for_ephemeral() -> Self {
501 Self {
502 hidden_dim: 256,
503 embedding_dim: 256,
504 micro_lora_rank: 2,
505 base_lora_rank: 4, micro_lora_lr: 0.002,
507 base_lora_lr: 0.0001,
508 ewc_lambda: 1000.0,
509 pattern_clusters: 50, trajectory_capacity: 500, background_interval_ms: 60000, quality_threshold: 0.3,
513 enable_simd: true,
514 }
515 }
516
517 pub fn for_coordinator() -> Self {
522 Self {
523 hidden_dim: 256,
524 embedding_dim: 256,
525 micro_lora_rank: 2,
526 base_lora_rank: 16, micro_lora_lr: 0.001, base_lora_lr: 0.0005, ewc_lambda: 2000.0, pattern_clusters: 200, trajectory_capacity: 50000, background_interval_ms: 300000, quality_threshold: 0.4, enable_simd: true,
535 }
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 #[test]
544 fn test_learning_signal_from_trajectory() {
545 let mut trajectory = QueryTrajectory::new(1, vec![0.1, 0.2, 0.3]);
546 trajectory.add_step(TrajectoryStep::new(
547 vec![0.5, 0.3, 0.2],
548 vec![0.4, 0.4, 0.2],
549 0.8,
550 0,
551 ));
552 trajectory.finalize(0.8, 1000);
553
554 let signal = LearningSignal::from_trajectory(&trajectory);
555 assert_eq!(signal.quality_score, 0.8);
556 assert_eq!(signal.gradient_estimate.len(), 3);
557 assert_eq!(signal.metadata.trajectory_id, 1);
558 }
559
560 #[test]
561 fn test_gradient_nonzero_for_single_step_trajectory() {
562 let mut trajectory = QueryTrajectory::new(1, vec![0.1; 8]);
566 trajectory.add_step(TrajectoryStep::new(vec![0.5; 8], vec![], 0.9, 0));
567 trajectory.finalize(0.9, 1000);
568
569 let signal = LearningSignal::from_trajectory(&trajectory);
570 let norm: f32 = signal
571 .gradient_estimate
572 .iter()
573 .map(|x| x * x)
574 .sum::<f32>()
575 .sqrt();
576 assert!(
577 norm > 1e-6,
578 "Expected non-zero gradient for single-step trajectory, norm={}",
579 norm
580 );
581
582 let mut neg = QueryTrajectory::new(2, vec![0.1; 8]);
584 neg.add_step(TrajectoryStep::new(vec![0.5; 8], vec![], -0.9, 0));
585 neg.finalize(0.9, 1000);
586 let neg_signal = LearningSignal::from_trajectory(&neg);
587 let dot: f32 = signal
588 .gradient_estimate
589 .iter()
590 .zip(neg_signal.gradient_estimate.iter())
591 .map(|(a, b)| a * b)
592 .sum();
593 assert!(
594 dot < 0.0,
595 "Negative reward should flip gradient, dot={}",
596 dot
597 );
598 }
599
600 #[test]
601 fn test_gradient_unchanged_for_varying_reward_trajectory() {
602 let mut trajectory = QueryTrajectory::new(1, vec![0.1; 4]);
605 trajectory.add_step(TrajectoryStep::new(
606 vec![1.0, 0.0, 0.0, 0.0],
607 vec![],
608 0.2,
609 0,
610 ));
611 trajectory.add_step(TrajectoryStep::new(
612 vec![0.0, 1.0, 0.0, 0.0],
613 vec![],
614 0.8,
615 1,
616 ));
617 trajectory.finalize(0.8, 1000);
618
619 let signal = LearningSignal::from_trajectory(&trajectory);
620 assert!(signal.gradient_estimate[0] < 0.0);
622 assert!(signal.gradient_estimate[1] > 0.0);
623 let norm: f32 = signal
624 .gradient_estimate
625 .iter()
626 .map(|x| x * x)
627 .sum::<f32>()
628 .sqrt();
629 assert!((norm - 1.0).abs() < 1e-4);
630 }
631
632 #[test]
633 fn test_pattern_merge() {
634 let p1 = LearnedPattern {
635 id: 1,
636 centroid: vec![1.0, 0.0],
637 cluster_size: 10,
638 total_weight: 5.0,
639 avg_quality: 0.8,
640 created_at: 100,
641 last_accessed: 200,
642 access_count: 5,
643 pattern_type: PatternType::General,
644 };
645
646 let p2 = LearnedPattern {
647 id: 2,
648 centroid: vec![0.0, 1.0],
649 cluster_size: 10,
650 total_weight: 5.0,
651 avg_quality: 0.9,
652 created_at: 150,
653 last_accessed: 250,
654 access_count: 3,
655 pattern_type: PatternType::General,
656 };
657
658 let merged = p1.merge(&p2);
659 assert_eq!(merged.cluster_size, 20);
660 assert!((merged.centroid[0] - 0.5).abs() < 1e-6);
661 assert!((merged.centroid[1] - 0.5).abs() < 1e-6);
662 assert!((merged.avg_quality - 0.85).abs() < 1e-6);
663 }
664
665 #[test]
666 fn test_pattern_similarity() {
667 let pattern = LearnedPattern::new(1, vec![1.0, 0.0, 0.0]);
668
669 assert!((pattern.similarity(&[1.0, 0.0, 0.0]) - 1.0).abs() < 1e-6);
670 assert!(pattern.similarity(&[0.0, 1.0, 0.0]).abs() < 1e-6);
671 }
672
673 #[test]
674 fn test_trajectory_rewards() {
675 let mut trajectory = QueryTrajectory::new(1, vec![0.1]);
676 trajectory.add_step(TrajectoryStep::new(vec![], vec![], 0.5, 0));
677 trajectory.add_step(TrajectoryStep::new(vec![], vec![], 0.7, 1));
678 trajectory.add_step(TrajectoryStep::new(vec![], vec![], 0.9, 2));
679
680 assert!((trajectory.total_reward() - 2.1).abs() < 1e-6);
681 assert!((trajectory.avg_reward() - 0.7).abs() < 1e-6);
682 }
683}