Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
//! Loop B - Background Learning
//!
//! Hourly pattern extraction and base LoRA updates.
use crate::ewc::EwcPlusPlus;
use crate::lora::BaseLoRA;
use crate::reasoning_bank::ReasoningBank;
use crate::time_compat::Instant;
use crate::types::{LearnedPattern, QueryTrajectory, SonaConfig};
use parking_lot::RwLock;
use std::sync::Arc;
use std::time::Duration;
/// Background loop configuration
#[derive(Clone, Debug)]
pub struct BackgroundLoopConfig {
/// Minimum trajectories to process
pub min_trajectories: usize,
/// Base LoRA learning rate
pub base_lora_lr: f32,
/// EWC lambda
pub ewc_lambda: f32,
/// Pattern extraction interval
pub extraction_interval: Duration,
}
impl Default for BackgroundLoopConfig {
fn default() -> Self {
Self {
min_trajectories: 100,
base_lora_lr: 0.0001,
ewc_lambda: 1000.0,
extraction_interval: Duration::from_secs(3600),
}
}
}
impl From<&SonaConfig> for BackgroundLoopConfig {
fn from(config: &SonaConfig) -> Self {
Self {
min_trajectories: 100,
base_lora_lr: config.base_lora_lr,
ewc_lambda: config.ewc_lambda,
extraction_interval: Duration::from_millis(config.background_interval_ms),
}
}
}
/// Background cycle result
#[derive(Debug)]
pub struct BackgroundResult {
pub trajectories_processed: usize,
pub patterns_extracted: usize,
pub ewc_updated: bool,
pub elapsed: Duration,
pub status: String,
}
impl BackgroundResult {
fn skipped(reason: &str) -> Self {
Self {
trajectories_processed: 0,
patterns_extracted: 0,
ewc_updated: false,
elapsed: Duration::ZERO,
status: format!("skipped: {}", reason),
}
}
}
/// Background learning loop (Loop B)
pub struct BackgroundLoop {
/// Configuration
config: BackgroundLoopConfig,
/// ReasoningBank for pattern storage
reasoning_bank: Arc<RwLock<ReasoningBank>>,
/// EWC++ for forgetting prevention
ewc: Arc<RwLock<EwcPlusPlus>>,
/// Base LoRA
base_lora: Arc<RwLock<BaseLoRA>>,
/// Last extraction time
last_extraction: RwLock<Instant>,
}
impl BackgroundLoop {
/// Create new background loop
pub fn new(
config: BackgroundLoopConfig,
reasoning_bank: Arc<RwLock<ReasoningBank>>,
ewc: Arc<RwLock<EwcPlusPlus>>,
base_lora: Arc<RwLock<BaseLoRA>>,
) -> Self {
Self {
config,
reasoning_bank,
ewc,
base_lora,
last_extraction: RwLock::new(Instant::now()),
}
}
/// Check if it's time for background cycle
pub fn should_run(&self) -> bool {
self.last_extraction.read().elapsed() >= self.config.extraction_interval
}
/// Run background learning cycle
pub fn run_cycle(&self, trajectories: Vec<QueryTrajectory>) -> BackgroundResult {
if trajectories.len() < self.config.min_trajectories {
return BackgroundResult::skipped("insufficient trajectories");
}
let start = Instant::now();
// 1. Add trajectories to reasoning bank
{
let mut bank = self.reasoning_bank.write();
for trajectory in &trajectories {
bank.add_trajectory(trajectory);
}
}
// 2. Extract patterns
let patterns = {
let mut bank = self.reasoning_bank.write();
bank.extract_patterns()
};
// 3. Compute gradients from patterns
let gradients = self.compute_pattern_gradients(&patterns);
// 4. Apply EWC++ constraints
let constrained_gradients = {
let ewc = self.ewc.read();
ewc.apply_constraints(&gradients)
};
// 5. Check for task boundary
let task_boundary = {
let ewc = self.ewc.read();
ewc.detect_task_boundary(&gradients)
};
if task_boundary {
let mut ewc = self.ewc.write();
ewc.start_new_task();
}
// 6. Update EWC++ Fisher
{
let mut ewc = self.ewc.write();
ewc.update_fisher(&constrained_gradients);
}
// 7. Update base LoRA
self.update_base_lora(&constrained_gradients);
// Update last extraction time
*self.last_extraction.write() = Instant::now();
BackgroundResult {
trajectories_processed: trajectories.len(),
patterns_extracted: patterns.len(),
ewc_updated: true,
elapsed: start.elapsed(),
status: "completed".to_string(),
}
}
fn compute_pattern_gradients(&self, patterns: &[LearnedPattern]) -> Vec<f32> {
if patterns.is_empty() {
return Vec::new();
}
let dim = patterns[0].centroid.len();
let mut gradient = vec![0.0f32; dim];
let mut total_weight = 0.0f32;
for pattern in patterns {
let weight = pattern.avg_quality * pattern.cluster_size as f32;
for (i, &v) in pattern.centroid.iter().enumerate() {
if i < dim {
gradient[i] += v * weight;
}
}
total_weight += weight;
}
if total_weight > 0.0 {
for g in &mut gradient {
*g /= total_weight;
}
}
gradient
}
fn update_base_lora(&self, gradients: &[f32]) {
let mut lora = self.base_lora.write();
let num_layers = lora.num_layers();
if num_layers == 0 || gradients.is_empty() {
return;
}
let per_layer = gradients.len() / num_layers;
for (layer_idx, layer) in lora.layers.iter_mut().enumerate() {
let start = layer_idx * per_layer;
let end = (start + per_layer).min(gradients.len());
for (i, &grad) in gradients[start..end].iter().enumerate() {
if i < layer.up_proj.len() {
layer.up_proj[i] += grad * self.config.base_lora_lr;
}
}
}
}
/// Get reasoning bank reference
pub fn reasoning_bank(&self) -> &Arc<RwLock<ReasoningBank>> {
&self.reasoning_bank
}
/// Get EWC reference
pub fn ewc(&self) -> &Arc<RwLock<EwcPlusPlus>> {
&self.ewc
}
/// Get base LoRA reference
pub fn base_lora(&self) -> &Arc<RwLock<BaseLoRA>> {
&self.base_lora
}
}
+225
View File
@@ -0,0 +1,225 @@
//! Loop Coordinator - Orchestrates all learning loops
use crate::ewc::{EwcConfig, EwcPlusPlus};
use crate::loops::background::{BackgroundLoop, BackgroundLoopConfig, BackgroundResult};
use crate::loops::instant::InstantLoop;
use crate::lora::{BaseLoRA, MicroLoRA};
use crate::reasoning_bank::{PatternConfig, ReasoningBank};
use crate::types::{QueryTrajectory, SonaConfig};
use parking_lot::RwLock;
use std::sync::Arc;
/// Loop coordinator managing all learning loops
pub struct LoopCoordinator {
/// Configuration
_config: SonaConfig,
/// Instant loop (Loop A)
instant: InstantLoop,
/// Background loop (Loop B)
background: BackgroundLoop,
/// Shared components
reasoning_bank: Arc<RwLock<ReasoningBank>>,
ewc: Arc<RwLock<EwcPlusPlus>>,
base_lora: Arc<RwLock<BaseLoRA>>,
/// Enabled flags
instant_enabled: bool,
background_enabled: bool,
}
impl LoopCoordinator {
/// Create new coordinator with default config
pub fn new(hidden_dim: usize) -> Self {
Self::with_config(SonaConfig {
hidden_dim,
embedding_dim: hidden_dim,
..Default::default()
})
}
/// Create with custom config
pub fn with_config(config: SonaConfig) -> Self {
let reasoning_bank = Arc::new(RwLock::new(ReasoningBank::new(PatternConfig {
embedding_dim: config.embedding_dim,
k_clusters: config.pattern_clusters,
..Default::default()
})));
let ewc = Arc::new(RwLock::new(EwcPlusPlus::new(EwcConfig {
param_count: config.hidden_dim * config.base_lora_rank * 2,
initial_lambda: config.ewc_lambda,
..Default::default()
})));
let base_lora = Arc::new(RwLock::new(BaseLoRA::new(
config.hidden_dim,
config.base_lora_rank,
12, // Default number of layers
)));
let instant = InstantLoop::from_sona_config(&config);
let background = BackgroundLoop::new(
BackgroundLoopConfig::from(&config),
reasoning_bank.clone(),
ewc.clone(),
base_lora.clone(),
);
Self {
_config: config,
instant,
background,
reasoning_bank,
ewc,
base_lora,
instant_enabled: true,
background_enabled: true,
}
}
/// Process inference trajectory (Loop A)
pub fn on_inference(&self, trajectory: QueryTrajectory) {
if self.instant_enabled {
self.instant.on_trajectory(trajectory);
}
}
/// Generate next trajectory ID
pub fn next_trajectory_id(&self) -> u64 {
self.instant.next_id()
}
/// Run background cycle if needed (Loop B)
pub fn maybe_run_background(&self) -> Option<BackgroundResult> {
if !self.background_enabled {
return None;
}
if self.background.should_run() {
let trajectories = self.instant.drain_trajectories();
if !trajectories.is_empty() {
return Some(self.background.run_cycle(trajectories));
}
}
None
}
/// Force background cycle
pub fn force_background(&self) -> BackgroundResult {
let trajectories = self.instant.drain_trajectories();
self.background.run_cycle(trajectories)
}
/// Flush instant loop updates
pub fn flush_instant(&self) {
self.instant.flush();
}
/// Get micro-LoRA for inference
pub fn micro_lora(&self) -> &Arc<RwLock<MicroLoRA>> {
self.instant.micro_lora()
}
/// Get base-LoRA for inference
pub fn base_lora(&self) -> &Arc<RwLock<BaseLoRA>> {
&self.base_lora
}
/// Get reasoning bank
pub fn reasoning_bank(&self) -> &Arc<RwLock<ReasoningBank>> {
&self.reasoning_bank
}
/// Get EWC++
pub fn ewc(&self) -> &Arc<RwLock<EwcPlusPlus>> {
&self.ewc
}
/// Enable/disable instant loop
pub fn set_instant_enabled(&mut self, enabled: bool) {
self.instant_enabled = enabled;
}
/// Enable/disable background loop
pub fn set_background_enabled(&mut self, enabled: bool) {
self.background_enabled = enabled;
}
/// Get statistics
pub fn stats(&self) -> CoordinatorStats {
let (buffer_len, dropped, success_rate) = self.instant.buffer_stats();
CoordinatorStats {
trajectories_buffered: buffer_len,
trajectories_dropped: dropped,
buffer_success_rate: success_rate,
patterns_stored: self.reasoning_bank.read().pattern_count(),
ewc_tasks: self.ewc.read().task_count(),
instant_enabled: self.instant_enabled,
background_enabled: self.background_enabled,
}
}
}
/// Coordinator statistics
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "serde-support",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct CoordinatorStats {
pub trajectories_buffered: usize,
pub trajectories_dropped: u64,
pub buffer_success_rate: f64,
pub patterns_stored: usize,
pub ewc_tasks: usize,
pub instant_enabled: bool,
pub background_enabled: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::TrajectoryStep;
fn make_trajectory(id: u64) -> QueryTrajectory {
let mut t = QueryTrajectory::new(id, vec![0.1; 256]);
t.add_step(TrajectoryStep::new(vec![0.5; 256], vec![], 0.8, 0));
t.finalize(0.8, 1000);
t
}
#[test]
fn test_coordinator_creation() {
let coord = LoopCoordinator::new(256);
let stats = coord.stats();
assert_eq!(stats.trajectories_buffered, 0);
}
#[test]
fn test_inference_processing() {
let coord = LoopCoordinator::new(256);
for i in 0..10 {
let t = make_trajectory(coord.next_trajectory_id());
coord.on_inference(t);
}
let stats = coord.stats();
assert_eq!(stats.trajectories_buffered, 10);
}
#[test]
fn test_force_background() {
let coord = LoopCoordinator::new(256);
for i in 0..150 {
let t = make_trajectory(coord.next_trajectory_id());
coord.on_inference(t);
}
let result = coord.force_background();
assert_eq!(result.trajectories_processed, 150);
assert!(result.patterns_extracted > 0);
}
}
+247
View File
@@ -0,0 +1,247 @@
//! Loop A - Instant Learning
//!
//! Per-request adaptation with <1ms overhead.
use crate::lora::MicroLoRA;
use crate::trajectory::{TrajectoryBuffer, TrajectoryIdGen};
use crate::types::{LearningSignal, QueryTrajectory, SonaConfig};
use parking_lot::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
/// Configuration for instant loop
#[derive(Clone, Debug)]
pub struct InstantLoopConfig {
/// Micro-LoRA rank
pub micro_lora_rank: usize,
/// Micro-LoRA learning rate
pub micro_lora_lr: f32,
/// Buffer capacity
pub buffer_capacity: usize,
/// Flush threshold (apply updates every N signals)
pub flush_threshold: usize,
}
impl Default for InstantLoopConfig {
fn default() -> Self {
Self {
micro_lora_rank: 1,
micro_lora_lr: 0.001,
buffer_capacity: 10000,
flush_threshold: 100,
}
}
}
impl From<&SonaConfig> for InstantLoopConfig {
fn from(config: &SonaConfig) -> Self {
Self {
micro_lora_rank: config.micro_lora_rank,
micro_lora_lr: config.micro_lora_lr,
buffer_capacity: config.trajectory_capacity,
flush_threshold: 100,
}
}
}
/// Instant loop metrics
#[derive(Debug, Default)]
pub struct InstantLoopMetrics {
/// Total trajectories processed
pub trajectories_processed: AtomicU64,
/// Total signals accumulated
pub signals_accumulated: AtomicU64,
/// Total flushes performed
pub flushes_performed: AtomicU64,
/// Total updates applied
pub updates_applied: AtomicU64,
}
/// Instant learning loop (Loop A)
pub struct InstantLoop {
/// Configuration
config: InstantLoopConfig,
/// Trajectory buffer
trajectory_buffer: Arc<TrajectoryBuffer>,
/// Micro-LoRA adapter
micro_lora: Arc<RwLock<MicroLoRA>>,
/// ID generator
id_gen: TrajectoryIdGen,
/// Pending signal count
pending_signals: AtomicU64,
/// Metrics
pub metrics: InstantLoopMetrics,
}
impl InstantLoop {
/// Create new instant loop
pub fn new(hidden_dim: usize, config: InstantLoopConfig) -> Self {
Self {
trajectory_buffer: Arc::new(TrajectoryBuffer::new(config.buffer_capacity)),
micro_lora: Arc::new(RwLock::new(MicroLoRA::new(
hidden_dim,
config.micro_lora_rank,
))),
id_gen: TrajectoryIdGen::new(),
pending_signals: AtomicU64::new(0),
config,
metrics: InstantLoopMetrics::default(),
}
}
/// Create from SONA config
pub fn from_sona_config(config: &SonaConfig) -> Self {
Self::new(config.hidden_dim, InstantLoopConfig::from(config))
}
/// Generate next trajectory ID
pub fn next_id(&self) -> u64 {
self.id_gen.next()
}
/// Process completed trajectory
pub fn on_trajectory(&self, trajectory: QueryTrajectory) {
// Record to buffer
self.trajectory_buffer.record(trajectory.clone());
self.metrics
.trajectories_processed
.fetch_add(1, Ordering::Relaxed);
// Generate learning signal
let signal = LearningSignal::from_trajectory(&trajectory);
// Accumulate gradient (non-blocking)
if let Some(mut lora) = self.micro_lora.try_write() {
lora.accumulate_gradient(&signal);
self.metrics
.signals_accumulated
.fetch_add(1, Ordering::Relaxed);
let pending = self.pending_signals.fetch_add(1, Ordering::Relaxed) + 1;
// Auto-flush if threshold reached
if pending >= self.config.flush_threshold as u64 {
self.flush_internal(&mut lora);
}
}
}
/// Manually flush accumulated updates
pub fn flush(&self) {
if let Some(mut lora) = self.micro_lora.try_write() {
self.flush_internal(&mut lora);
}
}
fn flush_internal(&self, lora: &mut MicroLoRA) {
let pending = lora.pending_updates();
if pending > 0 {
lora.apply_accumulated(self.config.micro_lora_lr);
self.pending_signals.store(0, Ordering::Relaxed);
self.metrics
.flushes_performed
.fetch_add(1, Ordering::Relaxed);
self.metrics
.updates_applied
.fetch_add(pending as u64, Ordering::Relaxed);
}
}
/// Drain trajectories for background processing
pub fn drain_trajectories(&self) -> Vec<QueryTrajectory> {
self.trajectory_buffer.drain()
}
/// Drain up to N trajectories
pub fn drain_trajectories_n(&self, n: usize) -> Vec<QueryTrajectory> {
self.trajectory_buffer.drain_n(n)
}
/// Get micro-LoRA reference for inference
pub fn micro_lora(&self) -> &Arc<RwLock<MicroLoRA>> {
&self.micro_lora
}
/// Get trajectory buffer reference
pub fn buffer(&self) -> &Arc<TrajectoryBuffer> {
&self.trajectory_buffer
}
/// Get pending trajectory count
pub fn pending_count(&self) -> usize {
self.trajectory_buffer.len()
}
/// Get buffer stats
pub fn buffer_stats(&self) -> (usize, u64, f64) {
(
self.trajectory_buffer.len(),
self.trajectory_buffer.dropped_count(),
self.trajectory_buffer.success_rate(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::TrajectoryStep;
fn make_trajectory(id: u64) -> QueryTrajectory {
let mut t = QueryTrajectory::new(id, vec![0.1; 64]);
t.add_step(TrajectoryStep::new(vec![0.5; 64], vec![], 0.8, 0));
t.finalize(0.8, 1000);
t
}
#[test]
fn test_instant_loop_creation() {
let loop_a = InstantLoop::new(64, InstantLoopConfig::default());
assert_eq!(loop_a.pending_count(), 0);
}
#[test]
fn test_trajectory_processing() {
let loop_a = InstantLoop::new(64, InstantLoopConfig::default());
let t = make_trajectory(loop_a.next_id());
loop_a.on_trajectory(t);
assert_eq!(loop_a.pending_count(), 1);
assert_eq!(
loop_a
.metrics
.trajectories_processed
.load(Ordering::Relaxed),
1
);
}
#[test]
fn test_auto_flush() {
let config = InstantLoopConfig {
flush_threshold: 3,
..Default::default()
};
let loop_a = InstantLoop::new(64, config);
for i in 0..5 {
loop_a.on_trajectory(make_trajectory(i));
}
assert!(loop_a.metrics.flushes_performed.load(Ordering::Relaxed) >= 1);
}
#[test]
fn test_drain() {
let loop_a = InstantLoop::new(64, InstantLoopConfig::default());
for i in 0..10 {
loop_a.on_trajectory(make_trajectory(i));
}
let drained = loop_a.drain_trajectories();
assert_eq!(drained.len(), 10);
assert_eq!(loop_a.pending_count(), 0);
}
}
+14
View File
@@ -0,0 +1,14 @@
//! SONA Learning Loops
//!
//! Three-tier temporal learning architecture:
//! - Loop A (Instant): Per-request trajectory recording and micro-LoRA updates
//! - Loop B (Background): Hourly pattern extraction and base LoRA updates
//! - Loop C (Deep): Weekly dream consolidation and full EWC++ update
pub mod background;
pub mod coordinator;
pub mod instant;
pub use background::BackgroundLoop;
pub use coordinator::LoopCoordinator;
pub use instant::InstantLoop;