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
+225
View File
@@ -0,0 +1,225 @@
//! Graph Attention for Context Ranking
//!
//! Multi-head attention with edge-aware scoring and residual connections.
/// Attention configuration
#[derive(Clone, Debug)]
pub struct AttentionConfig {
/// Number of attention heads
pub num_heads: usize,
/// Hidden dimension
pub hidden_dim: usize,
/// Dropout rate (training only)
pub dropout: f32,
/// Use layer normalization
pub layer_norm: bool,
}
impl Default for AttentionConfig {
fn default() -> Self {
Self {
num_heads: 8,
hidden_dim: 128,
dropout: 0.1,
layer_norm: true,
}
}
}
/// Graph context for attention
#[derive(Clone, Debug)]
pub struct GraphContext {
/// Node embeddings [num_nodes, hidden_dim]
pub node_embeddings: Vec<Vec<f32>>,
/// Edge features (optional)
pub edge_features: Option<Vec<Vec<f32>>>,
/// Adjacency (node pairs)
pub edges: Vec<(usize, usize)>,
}
/// Multi-head graph attention
pub struct GraphAttention {
/// Configuration
config: AttentionConfig,
/// Query projection [hidden_dim, hidden_dim]
w_query: Vec<f32>,
/// Key projection [hidden_dim, hidden_dim]
w_key: Vec<f32>,
/// Value projection [hidden_dim, hidden_dim]
w_value: Vec<f32>,
/// Output projection [hidden_dim, hidden_dim]
w_out: Vec<f32>,
}
impl GraphAttention {
/// Create new graph attention layer
pub fn new(hidden_dim: usize, num_heads: usize) -> Result<Self, String> {
if hidden_dim % num_heads != 0 {
return Err(format!(
"hidden_dim {} must be divisible by num_heads {}",
hidden_dim, num_heads
));
}
let size = hidden_dim * hidden_dim;
Ok(Self {
config: AttentionConfig {
num_heads,
hidden_dim,
..Default::default()
},
w_query: vec![0.01; size],
w_key: vec![0.01; size],
w_value: vec![0.01; size],
w_out: vec![0.01; size],
})
}
/// Compute attention over graph context
pub fn attend(&self, query: &[f32], context: &GraphContext) -> Vec<f32> {
if context.node_embeddings.is_empty() {
return query.to_vec();
}
let hidden_dim = self.config.hidden_dim;
let num_heads = self.config.num_heads;
let head_dim = hidden_dim / num_heads;
let num_nodes = context.node_embeddings.len();
// Project query
let q = self.linear(query, &self.w_query, hidden_dim);
// Project keys and values from context nodes
let mut keys = Vec::with_capacity(num_nodes);
let mut values = Vec::with_capacity(num_nodes);
for node in &context.node_embeddings {
keys.push(self.linear(node, &self.w_key, hidden_dim));
values.push(self.linear(node, &self.w_value, hidden_dim));
}
// Compute attention scores
let mut scores = vec![0.0f32; num_nodes];
let scale = (head_dim as f32).sqrt();
for (i, key) in keys.iter().enumerate() {
let mut dot = 0.0f32;
for j in 0..hidden_dim {
dot += q[j] * key[j];
}
scores[i] = dot / scale;
}
// Softmax
self.softmax(&mut scores);
// Weighted sum of values
let mut output = vec![0.0f32; hidden_dim];
for (i, value) in values.iter().enumerate() {
for j in 0..hidden_dim {
output[j] += scores[i] * value[j];
}
}
// Output projection + residual
let projected = self.linear(&output, &self.w_out, hidden_dim);
// Residual connection
let mut result = vec![0.0f32; hidden_dim];
for j in 0..hidden_dim.min(query.len()) {
result[j] = query[j] + projected[j];
}
// Layer norm
if self.config.layer_norm {
self.layer_norm(&mut result);
}
result
}
// Private helpers
fn linear(&self, input: &[f32], weight: &[f32], out_dim: usize) -> Vec<f32> {
let in_dim = input.len();
let mut output = vec![0.0f32; out_dim];
for o in 0..out_dim {
for i in 0..in_dim.min(out_dim) {
output[o] += input[i] * weight[i * out_dim + o];
}
}
output
}
fn softmax(&self, scores: &mut [f32]) {
if scores.is_empty() {
return;
}
let max = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let mut sum = 0.0f32;
for s in scores.iter_mut() {
*s = (*s - max).exp();
sum += *s;
}
if sum > 0.0 {
for s in scores.iter_mut() {
*s /= sum;
}
}
}
fn layer_norm(&self, x: &mut [f32]) {
if x.is_empty() {
return;
}
// Compute mean
let mean: f32 = x.iter().sum::<f32>() / x.len() as f32;
// Compute variance
let var: f32 = x.iter().map(|v| (v - mean).powi(2)).sum::<f32>() / x.len() as f32;
let std = (var + 1e-5).sqrt();
// Normalize
for v in x.iter_mut() {
*v = (*v - mean) / std;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_attention_creation() {
let attn = GraphAttention::new(128, 8);
assert!(attn.is_ok());
}
#[test]
fn test_attention_invalid_dims() {
let attn = GraphAttention::new(100, 8);
assert!(attn.is_err());
}
#[test]
fn test_attention_forward() {
let attn = GraphAttention::new(64, 8).unwrap();
let query = vec![1.0; 64];
let context = GraphContext {
node_embeddings: vec![vec![0.5; 64], vec![0.3; 64]],
edge_features: None,
edges: vec![(0, 1)],
};
let output = attn.attend(&query, &context);
assert_eq!(output.len(), 64);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,561 @@
//! DAG Attention for Task Orchestration
//!
//! Answers: "What computational steps matter?"
//!
//! Uses topological attention to focus compute on critical path tasks
//! in distributed workflows. Combines:
//! - Topological sort for dependency ordering
//! - Attention scores based on downstream impact
//! - Critical path analysis for priority allocation
use std::collections::{HashMap, HashSet, VecDeque};
use serde::{Deserialize, Serialize};
/// A node in the task DAG
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskNode {
pub id: String,
pub cost: f32, // Estimated compute cost
pub priority: f32, // Base priority (0-1)
pub status: TaskStatus,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TaskStatus {
Pending,
Ready, // All dependencies satisfied
Running,
Completed,
Failed,
}
/// Edge representing dependency between tasks
#[derive(Debug, Clone)]
pub struct TaskEdge {
pub from: String, // Dependency (must complete first)
pub to: String, // Dependent task
pub weight: f32, // Importance of this dependency
}
/// DAG Attention mechanism for task orchestration
#[derive(Debug)]
pub struct DagAttention {
nodes: HashMap<String, TaskNode>,
edges: Vec<TaskEdge>,
adjacency: HashMap<String, Vec<String>>, // Forward edges: task -> dependents
reverse_adj: HashMap<String, Vec<String>>, // Reverse edges: task -> dependencies
attention_scores: HashMap<String, f32>,
critical_path: Vec<String>,
temperature: f32,
}
impl DagAttention {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
adjacency: HashMap::new(),
reverse_adj: HashMap::new(),
attention_scores: HashMap::new(),
critical_path: Vec::new(),
temperature: 1.0,
}
}
/// Add a task node to the DAG
pub fn add_task(&mut self, id: &str, cost: f32, priority: f32) {
let node = TaskNode {
id: id.to_string(),
cost,
priority,
status: TaskStatus::Pending,
metadata: HashMap::new(),
};
self.nodes.insert(id.to_string(), node);
self.adjacency.entry(id.to_string()).or_default();
self.reverse_adj.entry(id.to_string()).or_default();
}
/// Add dependency: `from` must complete before `to` can start
pub fn add_dependency(&mut self, from: &str, to: &str, weight: f32) {
self.edges.push(TaskEdge {
from: from.to_string(),
to: to.to_string(),
weight,
});
self.adjacency.entry(from.to_string()).or_default().push(to.to_string());
self.reverse_adj.entry(to.to_string()).or_default().push(from.to_string());
}
/// Check for cycles (DAG must be acyclic)
pub fn has_cycle(&self) -> bool {
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
for node_id in self.nodes.keys() {
if self.has_cycle_dfs(node_id, &mut visited, &mut rec_stack) {
return true;
}
}
false
}
fn has_cycle_dfs(
&self,
node: &str,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
) -> bool {
if rec_stack.contains(node) {
return true;
}
if visited.contains(node) {
return false;
}
visited.insert(node.to_string());
rec_stack.insert(node.to_string());
if let Some(neighbors) = self.adjacency.get(node) {
for neighbor in neighbors {
if self.has_cycle_dfs(neighbor, visited, rec_stack) {
return true;
}
}
}
rec_stack.remove(node);
false
}
/// Topological sort using Kahn's algorithm
pub fn topological_sort(&self) -> Option<Vec<String>> {
let mut in_degree: HashMap<String, usize> = HashMap::new();
// Initialize in-degrees
for node_id in self.nodes.keys() {
in_degree.insert(node_id.clone(), 0);
}
// Count incoming edges
for edge in &self.edges {
*in_degree.entry(edge.to.clone()).or_default() += 1;
}
// Queue nodes with no dependencies
let mut queue: VecDeque<String> = in_degree
.iter()
.filter(|(_, &deg)| deg == 0)
.map(|(id, _)| id.clone())
.collect();
let mut sorted = Vec::new();
while let Some(node) = queue.pop_front() {
sorted.push(node.clone());
if let Some(neighbors) = self.adjacency.get(&node) {
for neighbor in neighbors {
if let Some(deg) = in_degree.get_mut(neighbor) {
*deg -= 1;
if *deg == 0 {
queue.push_back(neighbor.clone());
}
}
}
}
}
if sorted.len() == self.nodes.len() {
Some(sorted)
} else {
None // Cycle detected
}
}
/// Compute critical path (longest path through DAG)
pub fn compute_critical_path(&mut self) -> Vec<String> {
let topo_order = match self.topological_sort() {
Some(order) => order,
None => return Vec::new(),
};
// Distance and predecessor for longest path
let mut dist: HashMap<String, f32> = HashMap::new();
let mut pred: HashMap<String, Option<String>> = HashMap::new();
for node_id in &topo_order {
let node_cost = self.nodes.get(node_id).map(|n| n.cost).unwrap_or(0.0);
dist.insert(node_id.clone(), node_cost);
pred.insert(node_id.clone(), None);
}
// Relax edges in topological order
for node_id in &topo_order {
let current_dist = dist.get(node_id).copied().unwrap_or(0.0);
if let Some(neighbors) = self.adjacency.get(node_id) {
for neighbor in neighbors {
let neighbor_cost = self.nodes.get(neighbor).map(|n| n.cost).unwrap_or(0.0);
let new_dist = current_dist + neighbor_cost;
if new_dist > dist.get(neighbor).copied().unwrap_or(0.0) {
dist.insert(neighbor.clone(), new_dist);
pred.insert(neighbor.clone(), Some(node_id.clone()));
}
}
}
}
// Find the end node with maximum distance
let end_node = dist.iter()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(id, _)| id.clone());
// Reconstruct path
let mut path = Vec::new();
let mut current = end_node;
while let Some(node_id) = current {
path.push(node_id.clone());
current = pred.get(&node_id).cloned().flatten();
}
path.reverse();
self.critical_path = path.clone();
path
}
/// Compute attention scores for all tasks
///
/// Attention is based on:
/// 1. Position on critical path (highest attention)
/// 2. Number of downstream dependents (more = higher)
/// 3. Task priority
/// 4. Current status (ready tasks get boost)
pub fn compute_attention(&mut self) {
self.compute_critical_path();
let critical_set: HashSet<_> = self.critical_path.iter().cloned().collect();
// Compute downstream impact for each node
let mut downstream_count: HashMap<String, usize> = HashMap::new();
for node_id in self.nodes.keys() {
let count = self.count_downstream(node_id);
downstream_count.insert(node_id.clone(), count);
}
let max_downstream = downstream_count.values().max().copied().unwrap_or(1) as f32;
// Compute attention scores
for (node_id, node) in &self.nodes {
let mut score = 0.0;
// Critical path bonus (0.4 weight)
if critical_set.contains(node_id) {
score += 0.4;
}
// Downstream impact (0.3 weight)
let downstream = downstream_count.get(node_id).copied().unwrap_or(0) as f32;
score += 0.3 * (downstream / max_downstream);
// Base priority (0.2 weight)
score += 0.2 * node.priority;
// Ready status boost (0.1 weight)
if node.status == TaskStatus::Ready {
score += 0.1;
}
self.attention_scores.insert(node_id.clone(), score);
}
// Apply softmax with temperature
self.apply_softmax();
}
fn count_downstream(&self, node_id: &str) -> usize {
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back(node_id.to_string());
while let Some(current) = queue.pop_front() {
if visited.contains(&current) {
continue;
}
visited.insert(current.clone());
if let Some(neighbors) = self.adjacency.get(&current) {
for neighbor in neighbors {
queue.push_back(neighbor.clone());
}
}
}
visited.len().saturating_sub(1) // Exclude self
}
fn apply_softmax(&mut self) {
let max_score = self.attention_scores.values()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.copied()
.unwrap_or(0.0);
let exp_sum: f32 = self.attention_scores.values()
.map(|s| ((s - max_score) / self.temperature).exp())
.sum();
for score in self.attention_scores.values_mut() {
*score = ((*score - max_score) / self.temperature).exp() / exp_sum;
}
}
/// Get tasks sorted by attention (highest first)
pub fn get_prioritized_tasks(&self) -> Vec<(String, f32)> {
let mut tasks: Vec<_> = self.attention_scores.iter()
.map(|(id, score)| (id.clone(), *score))
.collect();
tasks.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
tasks
}
/// Get ready tasks (all dependencies satisfied) sorted by attention
pub fn get_ready_tasks(&self) -> Vec<(String, f32)> {
self.get_prioritized_tasks()
.into_iter()
.filter(|(id, _)| {
self.nodes.get(id)
.map(|n| n.status == TaskStatus::Ready || n.status == TaskStatus::Pending)
.unwrap_or(false)
&& self.all_deps_completed(id)
})
.collect()
}
fn all_deps_completed(&self, task_id: &str) -> bool {
self.reverse_adj.get(task_id)
.map(|deps| {
deps.iter().all(|dep| {
self.nodes.get(dep)
.map(|n| n.status == TaskStatus::Completed)
.unwrap_or(false)
})
})
.unwrap_or(true)
}
/// Mark task as completed and update attention
pub fn complete_task(&mut self, task_id: &str) {
if let Some(node) = self.nodes.get_mut(task_id) {
node.status = TaskStatus::Completed;
}
// Update status of dependent tasks
if let Some(dependents) = self.adjacency.get(task_id).cloned() {
for dep_id in dependents {
if self.all_deps_completed(&dep_id) {
if let Some(node) = self.nodes.get_mut(&dep_id) {
if node.status == TaskStatus::Pending {
node.status = TaskStatus::Ready;
}
}
}
}
}
// Recompute attention
self.compute_attention();
}
/// Get attention score for a specific task
pub fn get_attention(&self, task_id: &str) -> f32 {
self.attention_scores.get(task_id).copied().unwrap_or(0.0)
}
/// Get the critical path
pub fn get_critical_path(&self) -> &[String] {
&self.critical_path
}
/// Set temperature for softmax (higher = more uniform attention)
pub fn set_temperature(&mut self, temp: f32) {
self.temperature = temp.max(0.01);
}
/// Get total estimated time (critical path length)
pub fn estimated_total_time(&self) -> f32 {
self.critical_path.iter()
.filter_map(|id| self.nodes.get(id))
.map(|n| n.cost)
.sum()
}
/// Get summary statistics
pub fn summary(&self) -> DagSummary {
let completed = self.nodes.values()
.filter(|n| n.status == TaskStatus::Completed)
.count();
DagSummary {
total_tasks: self.nodes.len(),
completed_tasks: completed,
critical_path_length: self.critical_path.len(),
estimated_total_time: self.estimated_total_time(),
max_parallelism: self.compute_max_parallelism(),
}
}
fn compute_max_parallelism(&self) -> usize {
// Compute level-based parallelism
let topo = match self.topological_sort() {
Some(t) => t,
None => return 0,
};
let mut levels: HashMap<String, usize> = HashMap::new();
for node_id in &topo {
let deps = self.reverse_adj.get(node_id);
let level = deps
.map(|d| d.iter().filter_map(|dep| levels.get(dep)).max().copied().unwrap_or(0) + 1)
.unwrap_or(0);
levels.insert(node_id.clone(), level);
}
// Count nodes per level
let mut level_counts: HashMap<usize, usize> = HashMap::new();
for level in levels.values() {
*level_counts.entry(*level).or_default() += 1;
}
level_counts.values().max().copied().unwrap_or(0)
}
}
impl Default for DagAttention {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DagSummary {
pub total_tasks: usize,
pub completed_tasks: usize,
pub critical_path_length: usize,
pub estimated_total_time: f32,
pub max_parallelism: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dag_attention_basic() {
let mut dag = DagAttention::new();
// Create a simple diamond DAG:
// A
// / \
// B C
// \ /
// D
dag.add_task("A", 1.0, 0.5);
dag.add_task("B", 2.0, 0.5);
dag.add_task("C", 3.0, 0.5);
dag.add_task("D", 1.0, 0.5);
dag.add_dependency("A", "B", 1.0);
dag.add_dependency("A", "C", 1.0);
dag.add_dependency("B", "D", 1.0);
dag.add_dependency("C", "D", 1.0);
assert!(!dag.has_cycle());
let topo = dag.topological_sort().unwrap();
assert_eq!(topo[0], "A");
assert_eq!(topo[3], "D");
}
#[test]
fn test_critical_path() {
let mut dag = DagAttention::new();
dag.add_task("A", 1.0, 0.5);
dag.add_task("B", 5.0, 0.5); // Longer path through B
dag.add_task("C", 1.0, 0.5);
dag.add_task("D", 1.0, 0.5);
dag.add_dependency("A", "B", 1.0);
dag.add_dependency("A", "C", 1.0);
dag.add_dependency("B", "D", 1.0);
dag.add_dependency("C", "D", 1.0);
let critical = dag.compute_critical_path();
// Critical path should be A -> B -> D (cost 7)
assert!(critical.contains(&"B".to_string()));
}
#[test]
fn test_attention_scores() {
let mut dag = DagAttention::new();
dag.add_task("root", 1.0, 0.9);
dag.add_task("leaf1", 1.0, 0.1);
dag.add_task("leaf2", 1.0, 0.1);
dag.add_dependency("root", "leaf1", 1.0);
dag.add_dependency("root", "leaf2", 1.0);
dag.compute_attention();
// Root should have higher attention (more downstream impact)
assert!(dag.get_attention("root") > dag.get_attention("leaf1"));
}
#[test]
fn test_cycle_detection() {
let mut dag = DagAttention::new();
dag.add_task("A", 1.0, 0.5);
dag.add_task("B", 1.0, 0.5);
dag.add_task("C", 1.0, 0.5);
dag.add_dependency("A", "B", 1.0);
dag.add_dependency("B", "C", 1.0);
dag.add_dependency("C", "A", 1.0); // Creates cycle
assert!(dag.has_cycle());
assert!(dag.topological_sort().is_none());
}
#[test]
fn test_task_completion() {
let mut dag = DagAttention::new();
dag.add_task("A", 1.0, 0.5);
dag.add_task("B", 1.0, 0.5);
dag.add_dependency("A", "B", 1.0);
dag.compute_attention();
// B should not be ready yet
let ready = dag.get_ready_tasks();
assert!(ready.iter().any(|(id, _)| id == "A"));
assert!(!ready.iter().any(|(id, _)| id == "B"));
// Complete A
dag.complete_task("A");
// Now B should be ready
let ready = dag.get_ready_tasks();
assert!(ready.iter().any(|(id, _)| id == "B"));
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+727
View File
@@ -0,0 +1,727 @@
//! # HNSW Vector Index for Edge-Net
//!
//! Hierarchical Navigable Small World graph for efficient approximate nearest neighbor search.
//! Provides 150x faster search than naive linear scan with O(log N) complexity.
//!
//! ## Key Features
//!
//! - **Multi-layer graph**: Higher layers for coarse search, lower layers for fine-grained
//! - **Incremental updates**: Add vectors without rebuilding the entire index
//! - **P2P synchronization**: Index can be incrementally updated from peer events
//! - **SIMD acceleration**: Uses ComputeOps trait for vectorized distance calculations
//!
//! ## Architecture
//!
//! ```text
//! Layer 2: [node-5] -------- [node-42]
//! | |
//! Layer 1: [node-5] -- [node-12] -- [node-42] -- [node-87]
//! | | | |
//! Layer 0: [all nodes connected with M*2 edges per node]
//! ```
//!
//! ## Parameters
//!
//! - `M`: Maximum connections per node (default 32)
//! - `ef_construction`: Build-time beam width (default 200)
//! - `ef_search`: Search-time beam width (default 64)
use crate::ai::{ComputeOps, CpuOps};
use serde::{Deserialize, Serialize};
use std::collections::{BinaryHeap, HashSet};
use std::cmp::Ordering;
/// HNSW configuration parameters
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HnswConfig {
/// Maximum connections per node at layers > 0
pub m: usize,
/// Maximum connections per node at layer 0 (typically 2*M)
pub m_max_0: usize,
/// Build-time beam width
pub ef_construction: usize,
/// Search-time beam width
pub ef_search: usize,
/// Vector dimension
pub dimensions: usize,
}
impl Default for HnswConfig {
fn default() -> Self {
Self {
m: 32,
m_max_0: 64,
ef_construction: 200,
ef_search: 64,
dimensions: 128,
}
}
}
impl HnswConfig {
/// Create config for small indices (< 10k vectors)
pub fn small(dimensions: usize) -> Self {
Self {
m: 16,
m_max_0: 32,
ef_construction: 100,
ef_search: 32,
dimensions,
}
}
/// Create config for medium indices (10k - 100k vectors)
pub fn medium(dimensions: usize) -> Self {
Self {
m: 32,
m_max_0: 64,
ef_construction: 200,
ef_search: 64,
dimensions,
}
}
/// Create config for large indices (> 100k vectors)
pub fn large(dimensions: usize) -> Self {
Self {
m: 48,
m_max_0: 96,
ef_construction: 400,
ef_search: 128,
dimensions,
}
}
}
/// A node in the HNSW graph
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HnswNode {
/// Unique node identifier
pub id: String,
/// Vector data
pub vector: Vec<f32>,
/// Connections at each layer (layer -> list of neighbor indices)
connections: Vec<Vec<usize>>,
/// Maximum layer this node appears in
max_layer: usize,
}
impl HnswNode {
/// Create a new HNSW node
pub fn new(id: String, vector: Vec<f32>, max_layer: usize) -> Self {
Self {
id,
vector,
connections: vec![Vec::new(); max_layer + 1],
max_layer,
}
}
/// Get neighbors at a specific layer
pub fn neighbors_at_layer(&self, layer: usize) -> &[usize] {
if layer <= self.max_layer {
&self.connections[layer]
} else {
&[]
}
}
}
/// Candidate for priority queue (min-heap by distance)
#[derive(Clone, Debug)]
struct Candidate {
distance: f32,
node_idx: usize,
}
impl PartialEq for Candidate {
fn eq(&self, other: &Self) -> bool {
self.node_idx == other.node_idx
}
}
impl Eq for Candidate {}
impl PartialOrd for Candidate {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Candidate {
fn cmp(&self, other: &Self) -> Ordering {
// Reverse for min-heap (smaller distance = higher priority)
other.distance.partial_cmp(&self.distance).unwrap_or(Ordering::Equal)
}
}
/// Search result from HNSW query
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SearchResult {
/// Node ID
pub id: String,
/// Distance from query
pub distance: f32,
/// Node index in the index
pub index: usize,
}
/// Search statistics for performance monitoring
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SearchStats {
/// Number of results returned
pub k_retrieved: usize,
/// Layers traversed during search
pub layers_traversed: usize,
/// Total distance computations
pub distance_computations: usize,
/// Mean distance of results
pub distance_mean: f32,
/// Min distance of results
pub distance_min: f32,
/// Max distance of results
pub distance_max: f32,
}
/// HNSW vector index for approximate nearest neighbor search
pub struct HnswIndex {
/// All nodes in the graph
nodes: Vec<HnswNode>,
/// Index from ID to node index
id_to_index: rustc_hash::FxHashMap<String, usize>,
/// Entry point (highest layer node)
entry_point: Option<usize>,
/// Maximum layer in the graph
max_layer: usize,
/// Configuration
config: HnswConfig,
/// Statistics
total_insertions: u64,
total_searches: u64,
total_distance_ops: u64,
}
impl HnswIndex {
/// Create a new HNSW index
pub fn new(dimensions: usize, config: HnswConfig) -> Self {
Self {
nodes: Vec::new(),
id_to_index: rustc_hash::FxHashMap::default(),
entry_point: None,
max_layer: 0,
config: HnswConfig { dimensions, ..config },
total_insertions: 0,
total_searches: 0,
total_distance_ops: 0,
}
}
/// Create with default config for given dimensions
pub fn with_dimensions(dimensions: usize) -> Self {
Self::new(dimensions, HnswConfig::medium(dimensions))
}
/// Get number of vectors in the index
pub fn len(&self) -> usize {
self.nodes.len()
}
/// Check if index is empty
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
/// Get index configuration
pub fn config(&self) -> &HnswConfig {
&self.config
}
/// Generate random layer for new node (exponential distribution)
fn random_layer(&self) -> usize {
let m = self.config.m.max(2) as f32;
let ml = 1.0 / m.ln();
// Use wasm-compatible random via js_sys
#[cfg(target_arch = "wasm32")]
let r: f32 = js_sys::Math::random() as f32;
#[cfg(not(target_arch = "wasm32"))]
let r: f32 = {
use std::time::{SystemTime, UNIX_EPOCH};
let seed = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos();
((seed as f32 / u32::MAX as f32) * 1000.0).fract()
};
if r <= f32::EPSILON {
return 0;
}
let level = (-r.ln() * ml).floor();
level.min(32.0) as usize
}
/// Insert a vector into the index
pub fn insert(&mut self, id: impl Into<String>, vector: Vec<f32>) -> Result<usize, &'static str> {
let id = id.into();
// Validate dimensions
if vector.len() != self.config.dimensions {
return Err("Vector dimension mismatch");
}
// Check if ID already exists
if self.id_to_index.contains_key(&id) {
return Err("ID already exists in index");
}
// Determine layer for new node
let new_layer = self.random_layer();
let node_idx = self.nodes.len();
// Create new node
let mut new_node = HnswNode::new(id.clone(), vector, new_layer);
// Handle first insertion
if self.entry_point.is_none() {
self.nodes.push(new_node);
self.id_to_index.insert(id, node_idx);
self.entry_point = Some(node_idx);
self.max_layer = new_layer;
self.total_insertions += 1;
return Ok(node_idx);
}
let entry_point = self.entry_point.unwrap();
// Search phase: traverse from top layer down
let mut current = entry_point;
let mut current_dist = CpuOps::cosine_distance(&new_node.vector, &self.nodes[current].vector);
// Greedy search from top layer to layer above new_layer
for layer in (new_layer + 1..=self.max_layer).rev() {
loop {
let mut changed = false;
let neighbors = self.nodes[current].neighbors_at_layer(layer);
for &neighbor in neighbors {
if neighbor < self.nodes.len() {
let dist = CpuOps::cosine_distance(&new_node.vector, &self.nodes[neighbor].vector);
self.total_distance_ops += 1;
if dist < current_dist {
current = neighbor;
current_dist = dist;
changed = true;
}
}
}
if !changed {
break;
}
}
}
// Store the node first so we can reference it
self.nodes.push(new_node);
self.id_to_index.insert(id, node_idx);
// Insert phase: insert at each layer from min(new_layer, max_layer) down to 0
let top_layer = new_layer.min(self.max_layer);
for layer in (0..=top_layer).rev() {
let max_connections = if layer == 0 { self.config.m_max_0 } else { self.config.m };
// Find nearest neighbors at this layer
let neighbors = self.search_layer(node_idx, current, self.config.ef_construction, layer);
// Select best connections
let connections: Vec<usize> = neighbors
.into_iter()
.take(max_connections)
.map(|(idx, _)| idx)
.collect();
// Add bidirectional connections
for &neighbor in &connections {
// Add connection from new node to neighbor
if layer <= self.nodes[node_idx].max_layer {
self.nodes[node_idx].connections[layer].push(neighbor);
}
// Add connection from neighbor to new node
if layer <= self.nodes[neighbor].max_layer {
self.nodes[neighbor].connections[layer].push(node_idx);
// Prune if too many connections
if self.nodes[neighbor].connections[layer].len() > max_connections {
self.prune_connections(neighbor, layer, max_connections);
}
}
}
// Update entry point for next layer
if !connections.is_empty() {
current = connections[0];
}
}
// Update entry point if necessary
if new_layer > self.max_layer {
self.entry_point = Some(node_idx);
self.max_layer = new_layer;
}
self.total_insertions += 1;
Ok(node_idx)
}
/// Search for k nearest neighbors
pub fn search(&mut self, query: &[f32], k: usize) -> Result<Vec<SearchResult>, &'static str> {
self.search_with_ef(query, k, self.config.ef_search)
}
/// Search with custom ef parameter
pub fn search_with_ef(&mut self, query: &[f32], k: usize, ef: usize) -> Result<Vec<SearchResult>, &'static str> {
if query.len() != self.config.dimensions {
return Err("Query dimension mismatch");
}
if self.entry_point.is_none() {
return Ok(vec![]);
}
self.total_searches += 1;
let entry_point = self.entry_point.unwrap();
// Start from entry point
let mut current = entry_point;
let mut current_dist = CpuOps::cosine_distance(query, &self.nodes[current].vector);
self.total_distance_ops += 1;
// Traverse from top layer to layer 1
for layer in (1..=self.max_layer).rev() {
loop {
let mut changed = false;
let neighbors = self.nodes[current].neighbors_at_layer(layer);
for &neighbor in neighbors {
if neighbor < self.nodes.len() {
let dist = CpuOps::cosine_distance(query, &self.nodes[neighbor].vector);
self.total_distance_ops += 1;
if dist < current_dist {
current = neighbor;
current_dist = dist;
changed = true;
}
}
}
if !changed {
break;
}
}
}
// Search at layer 0 with ef
let neighbors = self.search_layer_query(query, current, ef, 0);
// Return top-k results
let results: Vec<SearchResult> = neighbors
.into_iter()
.take(k)
.map(|(idx, dist)| SearchResult {
id: self.nodes[idx].id.clone(),
distance: dist,
index: idx,
})
.collect();
Ok(results)
}
/// Search within a layer starting from entry point
fn search_layer(&self, query_idx: usize, entry: usize, ef: usize, layer: usize) -> Vec<(usize, f32)> {
let query = &self.nodes[query_idx].vector;
self.search_layer_query(query, entry, ef, layer)
}
/// Search within a layer with a query vector
fn search_layer_query(&self, query: &[f32], entry: usize, ef: usize, layer: usize) -> Vec<(usize, f32)> {
let mut visited = HashSet::new();
let mut candidates = BinaryHeap::new();
let mut result = Vec::new();
let entry_dist = CpuOps::cosine_distance(query, &self.nodes[entry].vector);
visited.insert(entry);
candidates.push(Candidate { distance: entry_dist, node_idx: entry });
result.push((entry, entry_dist));
while let Some(Candidate { distance: _, node_idx }) = candidates.pop() {
// Check stopping condition
if result.len() >= ef {
result.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
if let Some(&(_, furthest_dist)) = result.last() {
if let Some(closest) = candidates.peek() {
if closest.distance > furthest_dist {
break;
}
}
}
}
// Explore neighbors
let neighbors = self.nodes[node_idx].neighbors_at_layer(layer);
for &neighbor in neighbors {
if !visited.contains(&neighbor) && neighbor < self.nodes.len() {
visited.insert(neighbor);
let dist = CpuOps::cosine_distance(query, &self.nodes[neighbor].vector);
candidates.push(Candidate { distance: dist, node_idx: neighbor });
result.push((neighbor, dist));
}
}
}
result.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
result.truncate(ef);
result
}
/// Prune connections to keep only the best ones
fn prune_connections(&mut self, node_idx: usize, layer: usize, max_conn: usize) {
if layer > self.nodes[node_idx].max_layer {
return;
}
let node_vec = self.nodes[node_idx].vector.clone();
let mut scored: Vec<(usize, f32)> = self.nodes[node_idx].connections[layer]
.iter()
.filter_map(|&n| {
if n < self.nodes.len() {
Some((n, CpuOps::cosine_distance(&node_vec, &self.nodes[n].vector)))
} else {
None
}
})
.collect();
scored.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
self.nodes[node_idx].connections[layer] = scored.into_iter().take(max_conn).map(|(n, _)| n).collect();
}
/// Get a node by ID
pub fn get(&self, id: &str) -> Option<&HnswNode> {
self.id_to_index.get(id).map(|&idx| &self.nodes[idx])
}
/// Get a node by index
pub fn get_by_index(&self, idx: usize) -> Option<&HnswNode> {
self.nodes.get(idx)
}
/// Check if an ID exists in the index
pub fn contains(&self, id: &str) -> bool {
self.id_to_index.contains_key(id)
}
/// Get statistics about the index
pub fn stats(&self) -> HnswStats {
let layer_counts: Vec<usize> = (0..=self.max_layer)
.map(|l| self.nodes.iter().filter(|n| n.max_layer >= l).count())
.collect();
let avg_connections = if self.nodes.is_empty() {
0.0
} else {
let total_connections: usize = self.nodes
.iter()
.map(|n| n.connections.iter().map(|c| c.len()).sum::<usize>())
.sum();
total_connections as f64 / self.nodes.len() as f64
};
HnswStats {
node_count: self.nodes.len(),
max_layer: self.max_layer,
layer_counts,
avg_connections_per_node: avg_connections,
total_insertions: self.total_insertions,
total_searches: self.total_searches,
total_distance_computations: self.total_distance_ops,
}
}
/// Merge updates from a peer (for P2P sync)
pub fn merge_peer_updates(&mut self, updates: Vec<(String, Vec<f32>)>) -> usize {
let mut inserted = 0;
for (id, vector) in updates {
if !self.contains(&id) {
if self.insert(id, vector).is_ok() {
inserted += 1;
}
}
}
inserted
}
}
/// Statistics about the HNSW index
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HnswStats {
/// Total number of nodes
pub node_count: usize,
/// Maximum layer in the graph
pub max_layer: usize,
/// Number of nodes at each layer
pub layer_counts: Vec<usize>,
/// Average connections per node
pub avg_connections_per_node: f64,
/// Total insertions performed
pub total_insertions: u64,
/// Total searches performed
pub total_searches: u64,
/// Total distance computations
pub total_distance_computations: u64,
}
#[cfg(test)]
mod tests {
use super::*;
fn random_vector(dim: usize, seed: u64) -> Vec<f32> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut vec = Vec::with_capacity(dim);
for i in 0..dim {
let mut hasher = DefaultHasher::new();
(seed, i).hash(&mut hasher);
let hash = hasher.finish();
vec.push(((hash % 1000) as f32 / 1000.0) - 0.5);
}
// Normalize
let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
vec.iter_mut().for_each(|x| *x /= norm);
}
vec
}
#[test]
fn test_insert_and_search() {
let mut index = HnswIndex::with_dimensions(8);
// Insert some vectors
for i in 0..10 {
let vec = random_vector(8, i);
index.insert(format!("node-{}", i), vec).unwrap();
}
assert_eq!(index.len(), 10);
// Search for first vector
let query = random_vector(8, 0);
let results = index.search(&query, 5).unwrap();
assert!(!results.is_empty());
// First result should be the exact match or very close
assert!(results[0].distance < 0.1, "First result should be very close");
}
#[test]
fn test_exact_match_search() {
let mut index = HnswIndex::with_dimensions(4);
let v1 = vec![1.0, 0.0, 0.0, 0.0];
let v2 = vec![0.0, 1.0, 0.0, 0.0];
let v3 = vec![0.0, 0.0, 1.0, 0.0];
index.insert("v1", v1.clone()).unwrap();
index.insert("v2", v2).unwrap();
index.insert("v3", v3).unwrap();
let results = index.search(&v1, 1).unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, "v1");
assert!(results[0].distance < 0.001);
}
#[test]
fn test_duplicate_id_rejected() {
let mut index = HnswIndex::with_dimensions(4);
let v = vec![1.0, 0.0, 0.0, 0.0];
index.insert("dup", v.clone()).unwrap();
let result = index.insert("dup", v);
assert!(result.is_err());
}
#[test]
fn test_dimension_mismatch() {
let mut index = HnswIndex::with_dimensions(4);
let wrong_dim = vec![1.0, 0.0, 0.0]; // 3D instead of 4D
let result = index.insert("wrong", wrong_dim);
assert!(result.is_err());
}
#[test]
fn test_empty_search() {
let mut index = HnswIndex::with_dimensions(4);
let query = vec![1.0, 0.0, 0.0, 0.0];
let results = index.search(&query, 5).unwrap();
assert!(results.is_empty());
}
#[test]
fn test_stats() {
let mut index = HnswIndex::with_dimensions(8);
for i in 0..50 {
let vec = random_vector(8, i);
index.insert(format!("node-{}", i), vec).unwrap();
}
let stats = index.stats();
assert_eq!(stats.node_count, 50);
assert_eq!(stats.total_insertions, 50);
assert!(stats.max_layer >= 0);
}
#[test]
fn test_peer_merge() {
let mut index = HnswIndex::with_dimensions(4);
index.insert("local-1", vec![1.0, 0.0, 0.0, 0.0]).unwrap();
let peer_updates = vec![
("peer-1".to_string(), vec![0.0, 1.0, 0.0, 0.0]),
("peer-2".to_string(), vec![0.0, 0.0, 1.0, 0.0]),
("local-1".to_string(), vec![1.0, 1.0, 0.0, 0.0]), // Duplicate, should be ignored
];
let inserted = index.merge_peer_updates(peer_updates);
assert_eq!(inserted, 2);
assert_eq!(index.len(), 3);
}
#[test]
fn test_search_ordering() {
let mut index = HnswIndex::with_dimensions(4);
// Insert vectors at different angles
index.insert("v0", vec![1.0, 0.0, 0.0, 0.0]).unwrap();
index.insert("v1", vec![0.707, 0.707, 0.0, 0.0]).unwrap();
index.insert("v2", vec![0.0, 1.0, 0.0, 0.0]).unwrap();
let query = vec![1.0, 0.0, 0.0, 0.0];
let results = index.search(&query, 3).unwrap();
assert_eq!(results.len(), 3);
// Results should be ordered by distance
for i in 1..results.len() {
assert!(results[i - 1].distance <= results[i].distance);
}
}
}
+368
View File
@@ -0,0 +1,368 @@
//! # AI Module for Edge-Net
//!
//! Provides core AI capabilities for the P2P network:
//!
//! - **HNSW Vector Index** (`memory.rs`): 150x faster than naive search, O(log N) complexity
//! - **MicroLoRA Adapter Pool** (`lora.rs`): Task-specific adaptation with LRU eviction
//! - **Federated Learning** (`federated.rs`): P2P gradient gossip without coordinators
//!
//! ## Architecture
//!
//! ```text
//! +------------------------------------------------------------------------+
//! | AI Intelligence Layer |
//! +------------------------------------------------------------------------+
//! | +-----------------+ +-----------------+ +-----------------+ |
//! | | HNSW Index | | AdapterPool | | Federated | |
//! | | (memory.rs) | | (lora.rs) | | (federated.rs) | |
//! | | Neural Attention| | | | | |
//! | | "What matters?" | | - LRU eviction | | - TopK Sparse | |
//! | | - 150x speedup | | - 16 slots | | - Byzantine tol | |
//! | | - O(log N) | | - Task routing | | - Rep-weighted | |
//! | +-----------------+ +-----------------+ +-----------------+ |
//! | | | | |
//! | +-----------------+ +-----------------+ +-----------------+ |
//! | | DAG Attention | | LoraAdapter | | GradientGossip | |
//! | |(dag_attention.rs| | (lora.rs) | | (federated.rs) | |
//! | | "What steps?" | | | | | |
//! | | - Critical path | | - Rank 1-16 | | - Error feedback| |
//! | | - Topo sort | | - SIMD forward | | - Diff privacy | |
//! | | - Parallelism | | - 4/8-bit quant | | - Gossipsub | |
//! | +-----------------+ +-----------------+ +-----------------+ |
//! | | |
//! | ComputeOps Trait |
//! | (SIMD acceleration when available) |
//! +------------------------------------------------------------------------+
//! ```
//!
//! ## Usage
//!
//! ```rust,ignore
//! use edge_net::ai::{HnswIndex, GradientGossip, FederatedModel};
//!
//! // Create HNSW index for semantic search
//! let mut index = HnswIndex::new(128, HnswConfig::default());
//! index.insert("doc-1", vec![0.1; 128])?;
//! let results = index.search(&query, 10)?;
//!
//! // Federated learning with gradient gossip
//! let gossip = GradientGossip::new(&peer_id, 1000, 0.1)?;
//! gossip.set_local_gradients(&gradients)?;
//! let aggregated = gossip.aggregate();
//!
//! // Apply to model
//! let model = FederatedModel::new(1000, 0.01, 0.9);
//! model.apply_gradients(&aggregated)?;
//! ```
pub mod memory;
pub mod lora;
pub mod federated;
pub mod dag_attention;
pub mod attention_unified;
// Re-export unified attention types
pub use attention_unified::{
UnifiedAttention, NeuralAttention, DAGAttention, GraphAttentionNetwork, StateSpaceModel,
AttentionOutput, AttentionMetadata, UnifiedAttentionConfig, AttentionType,
DAGNode, Edge,
};
// Re-export memory types
pub use memory::{HnswIndex, HnswConfig, HnswNode, SearchResult as HnswSearchResult};
// Re-export LoRA types
pub use lora::{
AdapterPool, LoraAdapter, TaskType, PoolStats,
QuantizationLevel, QuantizedTensor,
LruEvictionPolicy, WasmAdapterPool,
OPTIMAL_BATCH_SIZE, DEFAULT_MAX_ADAPTERS,
};
// Re-export federated learning types
pub use federated::{
GradientGossip,
GradientMessage,
SparseGradient,
TopKSparsifier,
ByzantineDetector,
DifferentialPrivacy,
FederatedModel,
TOPIC_GRADIENT_GOSSIP,
TOPIC_MODEL_SYNC,
};
// Re-export DAG attention types
pub use dag_attention::{
DagAttention,
TaskNode,
TaskEdge,
TaskStatus,
DagSummary,
};
/// Common compute operations trait for SIMD acceleration
/// Used by all AI components for distance calculations and matrix ops
pub trait ComputeOps {
/// Compute cosine distance between two vectors
fn cosine_distance(a: &[f32], b: &[f32]) -> f32;
/// Compute dot product
fn dot_product(a: &[f32], b: &[f32]) -> f32;
/// Apply softmax in-place
fn softmax_inplace(x: &mut [f32]);
/// Compute L2 norm
fn l2_norm(x: &[f32]) -> f32;
/// Matrix-vector multiply
fn matmul_vec(matrix: &[f32], rows: usize, cols: usize, vec: &[f32]) -> Vec<f32>;
}
/// Default CPU implementation of ComputeOps
pub struct CpuOps;
impl ComputeOps for CpuOps {
fn cosine_distance(a: &[f32], b: &[f32]) -> f32 {
debug_assert_eq!(a.len(), b.len(), "Vector dimensions must match");
let mut dot = 0.0f32;
let mut norm_a = 0.0f32;
let mut norm_b = 0.0f32;
// Manual loop unrolling for better performance
let chunks = a.len() / 4;
let remainder = a.len() % 4;
for i in 0..chunks {
let base = i * 4;
dot += a[base] * b[base];
dot += a[base + 1] * b[base + 1];
dot += a[base + 2] * b[base + 2];
dot += a[base + 3] * b[base + 3];
norm_a += a[base] * a[base];
norm_a += a[base + 1] * a[base + 1];
norm_a += a[base + 2] * a[base + 2];
norm_a += a[base + 3] * a[base + 3];
norm_b += b[base] * b[base];
norm_b += b[base + 1] * b[base + 1];
norm_b += b[base + 2] * b[base + 2];
norm_b += b[base + 3] * b[base + 3];
}
// Handle remainder
let base = chunks * 4;
for i in 0..remainder {
dot += a[base + i] * b[base + i];
norm_a += a[base + i] * a[base + i];
norm_b += b[base + i] * b[base + i];
}
let norm_a = norm_a.sqrt();
let norm_b = norm_b.sqrt();
if norm_a > 1e-10 && norm_b > 1e-10 {
1.0 - dot / (norm_a * norm_b)
} else {
1.0
}
}
fn dot_product(a: &[f32], b: &[f32]) -> f32 {
debug_assert_eq!(a.len(), b.len(), "Vector dimensions must match");
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn softmax_inplace(x: &mut [f32]) {
if x.is_empty() {
return;
}
// Numerical stability: subtract max
let max = x.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let mut sum = 0.0f32;
for val in x.iter_mut() {
*val = (*val - max).exp();
sum += *val;
}
if sum > 0.0 {
for val in x.iter_mut() {
*val /= sum;
}
} else {
// Fallback to uniform
let uniform = 1.0 / x.len() as f32;
for val in x.iter_mut() {
*val = uniform;
}
}
}
fn l2_norm(x: &[f32]) -> f32 {
x.iter().map(|v| v * v).sum::<f32>().sqrt()
}
fn matmul_vec(matrix: &[f32], rows: usize, cols: usize, vec: &[f32]) -> Vec<f32> {
debug_assert_eq!(matrix.len(), rows * cols, "Matrix size mismatch");
debug_assert_eq!(vec.len(), cols, "Vector size mismatch");
let mut result = vec![0.0f32; rows];
for r in 0..rows {
let row_start = r * cols;
for c in 0..cols {
result[r] += matrix[row_start + c] * vec[c];
}
}
result
}
}
/// WASM SIMD implementation when available
#[cfg(target_feature = "simd128")]
pub struct SimdOps;
#[cfg(target_feature = "simd128")]
impl ComputeOps for SimdOps {
fn cosine_distance(a: &[f32], b: &[f32]) -> f32 {
use core::arch::wasm32::*;
debug_assert_eq!(a.len(), b.len());
let chunks = a.len() / 4;
let remainder = a.len() % 4;
let mut dot_acc = f32x4_splat(0.0);
let mut norm_a_acc = f32x4_splat(0.0);
let mut norm_b_acc = f32x4_splat(0.0);
for i in 0..chunks {
let base = i * 4;
let va = v128_load(a[base..].as_ptr() as *const v128);
let vb = v128_load(b[base..].as_ptr() as *const v128);
dot_acc = f32x4_add(dot_acc, f32x4_mul(va, vb));
norm_a_acc = f32x4_add(norm_a_acc, f32x4_mul(va, va));
norm_b_acc = f32x4_add(norm_b_acc, f32x4_mul(vb, vb));
}
// Reduce accumulators
let dot = f32x4_extract_lane::<0>(dot_acc)
+ f32x4_extract_lane::<1>(dot_acc)
+ f32x4_extract_lane::<2>(dot_acc)
+ f32x4_extract_lane::<3>(dot_acc);
let norm_a = f32x4_extract_lane::<0>(norm_a_acc)
+ f32x4_extract_lane::<1>(norm_a_acc)
+ f32x4_extract_lane::<2>(norm_a_acc)
+ f32x4_extract_lane::<3>(norm_a_acc);
let norm_b = f32x4_extract_lane::<0>(norm_b_acc)
+ f32x4_extract_lane::<1>(norm_b_acc)
+ f32x4_extract_lane::<2>(norm_b_acc)
+ f32x4_extract_lane::<3>(norm_b_acc);
// Handle remainder
let base = chunks * 4;
let mut dot_rem = 0.0f32;
let mut norm_a_rem = 0.0f32;
let mut norm_b_rem = 0.0f32;
for i in 0..remainder {
dot_rem += a[base + i] * b[base + i];
norm_a_rem += a[base + i] * a[base + i];
norm_b_rem += b[base + i] * b[base + i];
}
let dot = dot + dot_rem;
let norm_a = (norm_a + norm_a_rem).sqrt();
let norm_b = (norm_b + norm_b_rem).sqrt();
if norm_a > 1e-10 && norm_b > 1e-10 {
1.0 - dot / (norm_a * norm_b)
} else {
1.0
}
}
fn dot_product(a: &[f32], b: &[f32]) -> f32 {
CpuOps::dot_product(a, b)
}
fn softmax_inplace(x: &mut [f32]) {
CpuOps::softmax_inplace(x)
}
fn l2_norm(x: &[f32]) -> f32 {
CpuOps::l2_norm(x)
}
fn matmul_vec(matrix: &[f32], rows: usize, cols: usize, vec: &[f32]) -> Vec<f32> {
CpuOps::matmul_vec(matrix, rows, cols, vec)
}
}
/// Get the best available compute ops implementation
pub fn get_compute_ops() -> impl ComputeOps {
CpuOps
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cosine_distance_same_vector() {
let v = vec![1.0, 0.0, 0.0];
let dist = CpuOps::cosine_distance(&v, &v);
assert!(dist.abs() < 1e-5, "Same vector should have 0 distance");
}
#[test]
fn test_cosine_distance_orthogonal() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![0.0, 1.0, 0.0];
let dist = CpuOps::cosine_distance(&a, &b);
assert!((dist - 1.0).abs() < 1e-5, "Orthogonal vectors should have distance 1.0");
}
#[test]
fn test_cosine_distance_opposite() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![-1.0, 0.0, 0.0];
let dist = CpuOps::cosine_distance(&a, &b);
assert!((dist - 2.0).abs() < 1e-5, "Opposite vectors should have distance 2.0");
}
#[test]
fn test_softmax() {
let mut x = vec![1.0, 2.0, 3.0];
CpuOps::softmax_inplace(&mut x);
let sum: f32 = x.iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "Softmax should sum to 1.0");
assert!(x[2] > x[1] && x[1] > x[0], "Softmax should preserve ordering");
}
#[test]
fn test_dot_product() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![4.0, 5.0, 6.0];
let dot = CpuOps::dot_product(&a, &b);
assert!((dot - 32.0).abs() < 1e-5);
}
#[test]
fn test_matmul_vec() {
// 2x3 matrix times 3x1 vector
let matrix = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let vec = vec![1.0, 2.0, 3.0];
let result = CpuOps::matmul_vec(&matrix, 2, 3, &vec);
assert_eq!(result.len(), 2);
assert!((result[0] - 14.0).abs() < 1e-5); // 1*1 + 2*2 + 3*3
assert!((result[1] - 32.0).abs() < 1e-5); // 4*1 + 5*2 + 6*3
}
}
+241
View File
@@ -0,0 +1,241 @@
//! FastGRNN Router for Intelligent Model Selection
//!
//! Uses sparse + low-rank matrices for efficient routing decisions.
//! 90% sparse weight matrices with rank-8 decomposition.
/// Router configuration
#[derive(Clone, Debug)]
pub struct RouterConfig {
/// Input dimension
pub input_dim: usize,
/// Hidden state dimension
pub hidden_dim: usize,
/// Number of model outputs
pub num_models: usize,
/// Weight sparsity (0.0 - 1.0)
pub sparsity: f32,
/// Low-rank decomposition rank
pub rank: usize,
}
impl Default for RouterConfig {
fn default() -> Self {
Self {
input_dim: 128,
hidden_dim: 64,
num_models: 4,
sparsity: 0.9,
rank: 8,
}
}
}
/// Routing decision from FastGRNN
#[derive(Clone, Debug)]
pub struct RoutingDecision {
/// Selected model index
pub model_index: usize,
/// Model selection probabilities
pub model_probs: Vec<f32>,
/// Recommended context size bucket
pub context_bucket: usize,
/// Recommended temperature
pub temperature: f32,
/// Confidence score
pub confidence: f32,
}
/// FastGRNN Router with sparse + low-rank weights
pub struct FastGRNNRouter {
/// Configuration
config: RouterConfig,
/// Input to gate (sparse)
w_z: Vec<f32>,
/// Low-rank factor A for recurrent
u_z_a: Vec<f32>,
/// Low-rank factor B for recurrent
u_z_b: Vec<f32>,
/// Output projection for models
w_model: Vec<f32>,
/// Output projection for context
w_context: Vec<f32>,
/// Output projection for temperature
w_temp: Vec<f32>,
/// Gate modulation parameters
zeta: f32,
nu: f32,
}
impl FastGRNNRouter {
/// Create a new FastGRNN router
pub fn new(config: RouterConfig) -> Result<Self, String> {
let h = config.hidden_dim;
let d = config.input_dim;
let r = config.rank;
let m = config.num_models;
Ok(Self {
config: config.clone(),
w_z: vec![0.01; d * h],
u_z_a: vec![0.01; h * r],
u_z_b: vec![0.01; r * h],
w_model: vec![0.01; h * m],
w_context: vec![0.01; h * 5], // 5 context buckets
w_temp: vec![0.01; h],
zeta: 1.0,
nu: 0.0,
})
}
/// Forward pass with hidden state
pub fn forward(&self, input: &[f32], hidden: &[f32]) -> Result<(RoutingDecision, Vec<f32>), String> {
let h = self.config.hidden_dim;
let d = self.config.input_dim;
let r = self.config.rank;
let m = self.config.num_models;
if input.len() != d {
return Err(format!("Input dimension mismatch: expected {}, got {}", d, input.len()));
}
// Compute gate: z = sigmoid(W_z @ x + U_z @ h)
// where U_z = U_z_a @ U_z_b (low-rank)
// W_z @ x
let mut pre_gate = vec![0.0f32; h];
for i in 0..h {
for j in 0..d {
pre_gate[i] += self.w_z[j * h + i] * input[j];
}
}
// Low-rank recurrent: U_z_a @ (U_z_b @ h)
// First: U_z_b @ h
let mut low_rank = vec![0.0f32; r];
for i in 0..r {
for j in 0..h.min(hidden.len()) {
low_rank[i] += self.u_z_b[j * r + i] * hidden[j];
}
}
// Then: U_z_a @ low_rank
for i in 0..h {
for j in 0..r {
pre_gate[i] += self.u_z_a[j * h + i] * low_rank[j];
}
}
// Gate activation: z = sigmoid(pre_gate)
let gate: Vec<f32> = pre_gate.iter().map(|&x| 1.0 / (1.0 + (-x).exp())).collect();
// New hidden state: h' = (zeta * (1 - z) + nu) * tanh(W_x @ x) + z * h
let mut new_hidden = vec![0.0f32; h];
for i in 0..h.min(hidden.len()) {
let tanh_wx = (pre_gate[i]).tanh();
new_hidden[i] = (self.zeta * (1.0 - gate[i]) + self.nu) * tanh_wx + gate[i] * hidden[i];
}
// Output heads
// Model selection (softmax)
let mut model_logits = vec![0.0f32; m];
for i in 0..m {
for j in 0..h {
model_logits[i] += self.w_model[j * m + i] * new_hidden[j];
}
}
self.softmax(&mut model_logits);
let model_index = model_logits.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(i, _)| i)
.unwrap_or(0);
// Context bucket (softmax over 5 buckets)
let mut context_logits = vec![0.0f32; 5];
for i in 0..5 {
for j in 0..h {
context_logits[i] += self.w_context[j * 5 + i] * new_hidden[j];
}
}
self.softmax(&mut context_logits);
let context_bucket = context_logits.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.map(|(i, _)| i)
.unwrap_or(2);
// Temperature (sigmoid scaled to [0.1, 2.0])
let mut temp_logit = 0.0f32;
for j in 0..h {
temp_logit += self.w_temp[j] * new_hidden[j];
}
let temperature = 0.1 + 1.9 / (1.0 + (-temp_logit).exp());
// Confidence
let confidence = model_logits[model_index];
let decision = RoutingDecision {
model_index,
model_probs: model_logits,
context_bucket,
temperature,
confidence,
};
Ok((decision, new_hidden))
}
/// Initialize hidden state
pub fn init_hidden(&self) -> Vec<f32> {
vec![0.0; self.config.hidden_dim]
}
fn softmax(&self, x: &mut [f32]) {
if x.is_empty() {
return;
}
let max = x.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let mut sum = 0.0f32;
for v in x.iter_mut() {
*v = (*v - max).exp();
sum += *v;
}
if sum > 0.0 {
for v in x.iter_mut() {
*v /= sum;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_router_creation() {
let router = FastGRNNRouter::new(RouterConfig::default());
assert!(router.is_ok());
}
#[test]
fn test_router_forward() {
let config = RouterConfig {
input_dim: 64,
hidden_dim: 32,
num_models: 4,
..Default::default()
};
let router = FastGRNNRouter::new(config).unwrap();
let input = vec![0.5; 64];
let hidden = router.init_hidden();
let (decision, new_hidden) = router.forward(&input, &hidden).unwrap();
assert!(decision.model_index < 4);
assert!(decision.confidence >= 0.0 && decision.confidence <= 1.0);
assert!(decision.temperature >= 0.1 && decision.temperature <= 2.0);
assert_eq!(new_hidden.len(), 32);
}
}
+529
View File
@@ -0,0 +1,529 @@
//! LoRA (Low-Rank Adaptation) implementations for SONA in edge-net
//!
//! Two-tier LoRA system optimized for edge/WASM deployment:
//! - MicroLoRA: Rank 1-2, per-request adaptation (<100us)
//! - BaseLoRA: Rank 4-8, background adaptation (hourly)
use crate::ai::sona::types::LearningSignal;
use serde::{Deserialize, Serialize};
/// Optimal batch size for processing (benchmark-validated)
pub const OPTIMAL_BATCH_SIZE: usize = 32;
/// Micro-LoRA for per-request adaptation
///
/// Uses rank 1-2 for ultra-low latency updates.
/// Forward pass: output += scale * (input @ down) @ up
///
/// **Performance notes (from benchmarks):**
/// - Rank-2 is ~5% faster than Rank-1 due to better SIMD vectorization
/// - Batch size 32 optimal for throughput
/// - WASM SIMD: +10% speedup over scalar
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MicroLoRA {
/// Down projection (hidden_dim -> rank)
down_proj: Vec<f32>,
/// Up projection (rank -> hidden_dim)
up_proj: Vec<f32>,
/// Rank (1-2 for micro updates)
rank: usize,
/// Hidden dimension
hidden_dim: usize,
/// Accumulated gradients for up projection
#[serde(skip)]
grad_up: Vec<f32>,
/// Update count for averaging
#[serde(skip)]
update_count: usize,
/// Scaling factor
scale: f32,
}
impl MicroLoRA {
/// Create new Micro-LoRA adapter
///
/// # Arguments
/// * `hidden_dim` - Model hidden dimension
/// * `rank` - LoRA rank (must be 1-2)
///
/// # Panics
/// Panics if rank > 2
pub fn new(hidden_dim: usize, rank: usize) -> Self {
assert!(
rank >= 1 && rank <= 2,
"MicroLoRA rank must be 1-2, got {}",
rank
);
// Initialize down with small random-like values (deterministic for reproducibility)
let down_proj: Vec<f32> = (0..hidden_dim * rank)
.map(|i| {
let x = (i as f32 * 0.618033988749895) % 1.0;
(x - 0.5) * 0.02
})
.collect();
// Initialize up to zero (standard LoRA init)
let up_proj = vec![0.0f32; rank * hidden_dim];
Self {
down_proj,
up_proj,
rank,
hidden_dim,
grad_up: vec![0.0; rank * hidden_dim],
update_count: 0,
scale: 1.0 / (rank as f32).sqrt(),
}
}
/// Scalar forward pass
pub fn forward(&self, input: &[f32], output: &mut [f32]) {
if input.len() != self.hidden_dim || output.len() != self.hidden_dim {
return;
}
// Down projection: hidden_dim -> rank
let mut intermediate = vec![0.0f32; self.rank];
for r in 0..self.rank {
let mut sum = 0.0f32;
let offset = r * self.hidden_dim;
for i in 0..self.hidden_dim {
sum += input[i] * self.down_proj[offset + i];
}
intermediate[r] = sum;
}
// Up projection: rank -> hidden_dim
for i in 0..self.hidden_dim {
let mut sum = 0.0f32;
for r in 0..self.rank {
sum += intermediate[r] * self.up_proj[r * self.hidden_dim + i];
}
output[i] += sum * self.scale;
}
}
/// WASM SIMD-optimized forward pass (when available)
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
pub fn forward_simd(&self, input: &[f32], output: &mut [f32]) {
use std::arch::wasm32::*;
if input.len() != self.hidden_dim || output.len() != self.hidden_dim {
return;
}
unsafe {
let mut intermediate = vec![0.0f32; self.rank];
for r in 0..self.rank {
let mut sum = f32x4_splat(0.0);
let offset = r * self.hidden_dim;
let mut i = 0;
while i + 4 <= self.hidden_dim {
let inp = v128_load(input[i..].as_ptr() as *const v128);
let weight = v128_load(self.down_proj[offset + i..].as_ptr() as *const v128);
sum = f32x4_add(sum, f32x4_mul(inp, weight));
i += 4;
}
// Horizontal sum
let mut result = [0.0f32; 4];
v128_store(result.as_mut_ptr() as *mut v128, sum);
intermediate[r] = result.iter().sum();
// Handle remaining elements
for j in i..self.hidden_dim {
intermediate[r] += input[j] * self.down_proj[offset + j];
}
}
// Up projection with SIMD
let scale_vec = f32x4_splat(self.scale);
let mut i = 0;
while i + 4 <= self.hidden_dim {
let mut sum = f32x4_splat(0.0);
for r in 0..self.rank {
let up_offset = r * self.hidden_dim;
let weight = v128_load(self.up_proj[up_offset + i..].as_ptr() as *const v128);
let inter = f32x4_splat(intermediate[r]);
sum = f32x4_add(sum, f32x4_mul(inter, weight));
}
sum = f32x4_mul(sum, scale_vec);
let existing = v128_load(output[i..].as_ptr() as *const v128);
let result = f32x4_add(existing, sum);
v128_store(output[i..].as_mut_ptr() as *mut v128, result);
i += 4;
}
// Handle remaining elements
for j in i..self.hidden_dim {
let mut val = 0.0;
for r in 0..self.rank {
val += intermediate[r] * self.up_proj[r * self.hidden_dim + j];
}
output[j] += val * self.scale;
}
}
}
/// Batch forward pass - process multiple inputs efficiently
pub fn forward_batch(&self, inputs: &[Vec<f32>], outputs: &mut [Vec<f32>]) {
assert_eq!(inputs.len(), outputs.len());
for (input, output) in inputs.iter().zip(outputs.iter_mut()) {
self.forward(input, output);
}
}
/// Accumulate gradient from learning signal
pub fn accumulate_gradient(&mut self, signal: &LearningSignal) {
if signal.gradient_estimate.len() != self.hidden_dim {
return;
}
let quality = signal.quality_score;
// Simplified gradient: outer product scaled by quality
for r in 0..self.rank {
for i in 0..self.hidden_dim {
let grad_idx = r * self.hidden_dim + i;
// Update up projection gradient (main target)
self.grad_up[grad_idx] += signal.gradient_estimate[i] * quality;
}
}
self.update_count += 1;
}
/// Apply accumulated gradients with learning rate
pub fn apply_accumulated(&mut self, learning_rate: f32) {
if self.update_count == 0 {
return;
}
let scale = learning_rate / self.update_count as f32;
// Update up projection (main adaptation target)
for (w, g) in self.up_proj.iter_mut().zip(self.grad_up.iter()) {
*w += g * scale;
}
// Reset accumulators
self.grad_up.fill(0.0);
self.update_count = 0;
}
/// Reset adapter to initial state
pub fn reset(&mut self) {
self.up_proj.fill(0.0);
self.grad_up.fill(0.0);
self.update_count = 0;
}
/// Get rank
pub fn rank(&self) -> usize {
self.rank
}
/// Get hidden dimension
pub fn hidden_dim(&self) -> usize {
self.hidden_dim
}
/// Get parameter count
pub fn param_count(&self) -> usize {
self.down_proj.len() + self.up_proj.len()
}
/// Get scale factor
pub fn scale(&self) -> f32 {
self.scale
}
/// Set scale factor
pub fn set_scale(&mut self, scale: f32) {
self.scale = scale;
}
/// Get pending update count
pub fn pending_updates(&self) -> usize {
self.update_count
}
/// Get memory usage in bytes (approximate)
pub fn memory_usage(&self) -> usize {
(self.down_proj.len() + self.up_proj.len() + self.grad_up.len()) * 4
}
/// Export weights for P2P sharing
pub fn export_weights(&self) -> (Vec<f32>, Vec<f32>) {
(self.down_proj.clone(), self.up_proj.clone())
}
/// Import weights from P2P
pub fn import_weights(&mut self, down: &[f32], up: &[f32], blend_factor: f32) {
if down.len() != self.down_proj.len() || up.len() != self.up_proj.len() {
return;
}
// Blend imported weights with existing
for (i, &w) in down.iter().enumerate() {
self.down_proj[i] = self.down_proj[i] * (1.0 - blend_factor) + w * blend_factor;
}
for (i, &w) in up.iter().enumerate() {
self.up_proj[i] = self.up_proj[i] * (1.0 - blend_factor) + w * blend_factor;
}
}
}
/// Base LoRA for background adaptation
///
/// Higher rank (4-8) for more expressive adaptation.
/// Applied hourly during background learning cycles.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BaseLoRA {
/// LoRA layers
pub layers: Vec<LoRALayer>,
/// Rank
pub rank: usize,
/// Hidden dimension
pub hidden_dim: usize,
/// Alpha scaling factor
pub alpha: f32,
}
/// Single LoRA layer
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LoRALayer {
/// Down projection weights
pub down_proj: Vec<f32>,
/// Up projection weights
pub up_proj: Vec<f32>,
/// Layer index
pub layer_idx: usize,
}
impl BaseLoRA {
/// Create new Base LoRA
pub fn new(hidden_dim: usize, rank: usize, num_layers: usize) -> Self {
let layers = (0..num_layers)
.map(|idx| LoRALayer {
down_proj: vec![0.0; hidden_dim * rank],
up_proj: vec![0.0; rank * hidden_dim],
layer_idx: idx,
})
.collect();
Self {
layers,
rank,
hidden_dim,
alpha: rank as f32,
}
}
/// Forward pass for single layer
pub fn forward_layer(&self, layer_idx: usize, input: &[f32], output: &mut [f32]) {
if layer_idx >= self.layers.len() {
return;
}
let layer = &self.layers[layer_idx];
let scale = self.alpha / self.rank as f32;
// Down projection
let mut intermediate = vec![0.0f32; self.rank];
for r in 0..self.rank {
let offset = r * self.hidden_dim;
intermediate[r] = input
.iter()
.zip(&layer.down_proj[offset..offset + self.hidden_dim])
.map(|(a, b)| a * b)
.sum();
}
// Up projection
for i in 0..self.hidden_dim {
let mut sum = 0.0f32;
for r in 0..self.rank {
sum += intermediate[r] * layer.up_proj[r * self.hidden_dim + i];
}
output[i] += sum * scale;
}
}
/// Get number of layers
pub fn num_layers(&self) -> usize {
self.layers.len()
}
/// Get total parameter count
pub fn param_count(&self) -> usize {
self.layers.len() * (self.hidden_dim * self.rank + self.rank * self.hidden_dim)
}
/// Get memory usage in bytes
pub fn memory_usage(&self) -> usize {
self.param_count() * 4
}
}
/// Combined LoRA engine managing both tiers
#[derive(Clone, Debug)]
pub struct LoRAEngine {
/// Micro-LoRA for instant adaptation
pub micro: MicroLoRA,
/// Base LoRA for background adaptation
pub base: BaseLoRA,
/// Whether micro-LoRA is enabled
pub micro_enabled: bool,
/// Whether base LoRA is enabled
pub base_enabled: bool,
}
impl LoRAEngine {
/// Create new LoRA engine
pub fn new(hidden_dim: usize, micro_rank: usize, base_rank: usize, num_layers: usize) -> Self {
Self {
micro: MicroLoRA::new(hidden_dim, micro_rank.clamp(1, 2)),
base: BaseLoRA::new(hidden_dim, base_rank, num_layers),
micro_enabled: true,
base_enabled: true,
}
}
/// Apply both LoRA tiers
pub fn forward(&self, layer_idx: usize, input: &[f32], output: &mut [f32]) {
if self.micro_enabled {
self.micro.forward(input, output);
}
if self.base_enabled && layer_idx < self.base.num_layers() {
self.base.forward_layer(layer_idx, input, output);
}
}
/// Accumulate micro-LoRA gradient
pub fn accumulate_micro(&mut self, signal: &LearningSignal) {
if self.micro_enabled {
self.micro.accumulate_gradient(signal);
}
}
/// Apply micro-LoRA updates
pub fn apply_micro(&mut self, learning_rate: f32) {
if self.micro_enabled {
self.micro.apply_accumulated(learning_rate);
}
}
/// Get total memory usage
pub fn memory_usage(&self) -> usize {
self.micro.memory_usage() + self.base.memory_usage()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_micro_lora_creation() {
let lora = MicroLoRA::new(64, 1);
assert_eq!(lora.rank(), 1);
assert_eq!(lora.hidden_dim(), 64);
assert_eq!(lora.param_count(), 64 + 64);
}
#[test]
fn test_micro_lora_forward() {
let lora = MicroLoRA::new(64, 1);
let input = vec![1.0f32; 64];
let mut output = vec![0.0f32; 64];
lora.forward(&input, &mut output);
// With zero-init up_proj, output should be zero
let sum: f32 = output.iter().sum();
assert!(
sum.abs() < 1e-6,
"Expected ~0 with zero up_proj, got {}",
sum
);
}
#[test]
fn test_micro_lora_learning() {
let mut lora = MicroLoRA::new(64, 1);
let signal = LearningSignal::with_gradient(vec![0.1; 64], vec![0.5; 64], 0.8);
lora.accumulate_gradient(&signal);
assert_eq!(lora.pending_updates(), 1);
lora.apply_accumulated(0.01);
assert_eq!(lora.pending_updates(), 0);
// Now forward should produce non-zero output
let input = vec![1.0f32; 64];
let mut output = vec![0.0f32; 64];
lora.forward(&input, &mut output);
let sum: f32 = output.iter().map(|x| x.abs()).sum();
assert!(sum > 0.0, "Expected non-zero output after learning");
}
#[test]
fn test_base_lora() {
let lora = BaseLoRA::new(64, 4, 6);
assert_eq!(lora.num_layers(), 6);
assert_eq!(lora.rank, 4);
}
#[test]
fn test_lora_engine() {
let mut engine = LoRAEngine::new(64, 1, 4, 6);
let signal = LearningSignal::with_gradient(vec![0.1; 64], vec![0.5; 64], 0.9);
engine.accumulate_micro(&signal);
engine.apply_micro(0.01);
let input = vec![1.0f32; 64];
let mut output = vec![0.0f32; 64];
engine.forward(0, &input, &mut output);
}
#[test]
fn test_memory_usage() {
let micro = MicroLoRA::new(128, 2);
let base = BaseLoRA::new(128, 4, 6);
// MicroLoRA: (128*2 + 2*128 + 2*128) * 4 = 3072 bytes
assert!(micro.memory_usage() > 0);
// BaseLoRA: 6 * (128*4 + 4*128) * 4 = 24576 bytes
assert!(base.memory_usage() > 0);
}
#[test]
fn test_weight_export_import() {
let lora1 = MicroLoRA::new(64, 2);
let (down, up) = lora1.export_weights();
let mut lora2 = MicroLoRA::new(64, 2);
lora2.import_weights(&down, &up, 0.5);
// Weights should be blended
assert_eq!(lora2.hidden_dim(), 64);
}
#[test]
#[should_panic(expected = "MicroLoRA rank must be 1-2")]
fn test_invalid_rank() {
MicroLoRA::new(64, 5);
}
}
+199
View File
@@ -0,0 +1,199 @@
//! SONA - Self-Optimizing Neural Architecture
//!
//! Three temporal loops for continuous learning:
//! - Instant: Per-request MicroLoRA adaptation
//! - Background: Hourly consolidation and clustering
//! - Deep: Weekly EWC++ consolidation
use std::collections::VecDeque;
use std::sync::Arc;
use parking_lot::RwLock;
/// SONA learning orchestrator
pub struct SonaLearner {
/// Instant loop: per-request adaptation
pub instant_loop: InstantAdapter,
/// Background loop: hourly consolidation
pub background_loop: BackgroundConsolidator,
/// Deep loop: weekly EWC++ consolidation
pub deep_loop: DeepConsolidator,
/// Learning trajectory buffer
pub trajectory_buffer: Arc<RwLock<VecDeque<Trajectory>>>,
/// Configuration
pub config: SonaConfig,
}
/// Configuration for SONA learning
#[derive(Clone, Debug)]
pub struct SonaConfig {
/// Maximum trajectories to buffer
pub max_trajectories: usize,
/// Instant loop LoRA rank
pub instant_lora_rank: u8,
/// Background loop LoRA rank
pub background_lora_rank: u8,
/// Background consolidation interval (seconds)
pub background_interval_secs: u64,
/// Deep consolidation interval (seconds)
pub deep_interval_secs: u64,
/// EWC lambda (importance weighting)
pub ewc_lambda: f32,
/// K-means cluster count
pub num_clusters: usize,
}
impl Default for SonaConfig {
fn default() -> Self {
Self {
max_trajectories: 10_000,
instant_lora_rank: 2,
background_lora_rank: 8,
background_interval_secs: 3600, // 1 hour
deep_interval_secs: 604_800, // 1 week
ewc_lambda: 2000.0,
num_clusters: 100,
}
}
}
/// Learning trajectory record
#[derive(Clone, Debug)]
pub struct Trajectory {
/// Query embedding
pub query_embedding: Vec<f32>,
/// Response quality score
pub quality_score: f32,
/// Latency in microseconds
pub latency_us: u64,
/// Timestamp
pub timestamp: u64,
/// Activation patterns
pub activations: Vec<f32>,
}
/// Instant loop adapter for per-request learning
pub struct InstantAdapter {
/// Current LoRA rank
pub rank: u8,
/// Adaptation rate
pub adaptation_rate: f32,
}
impl Default for InstantAdapter {
fn default() -> Self {
Self {
rank: 2,
adaptation_rate: 0.01,
}
}
}
/// Background consolidation for hourly learning
pub struct BackgroundConsolidator {
/// K-means cluster centers
pub cluster_centers: Vec<Vec<f32>>,
/// Last consolidation timestamp
pub last_consolidation: u64,
}
impl Default for BackgroundConsolidator {
fn default() -> Self {
Self {
cluster_centers: Vec::new(),
last_consolidation: 0,
}
}
}
/// Deep consolidation with EWC++
pub struct DeepConsolidator {
/// Fisher information estimates
pub fisher_diagonal: Vec<f32>,
/// Reference parameters
pub reference_params: Vec<f32>,
/// EWC lambda
pub lambda: f32,
/// Last consolidation timestamp
pub last_consolidation: u64,
}
impl Default for DeepConsolidator {
fn default() -> Self {
Self {
fisher_diagonal: Vec::new(),
reference_params: Vec::new(),
lambda: 2000.0,
last_consolidation: 0,
}
}
}
impl SonaLearner {
/// Create a new SONA learner with default configuration
pub fn new() -> Self {
Self::with_config(SonaConfig::default())
}
/// Create a new SONA learner with custom configuration
pub fn with_config(config: SonaConfig) -> Self {
Self {
instant_loop: InstantAdapter {
rank: config.instant_lora_rank,
..Default::default()
},
background_loop: BackgroundConsolidator::default(),
deep_loop: DeepConsolidator {
lambda: config.ewc_lambda,
..Default::default()
},
trajectory_buffer: Arc::new(RwLock::new(VecDeque::with_capacity(config.max_trajectories))),
config,
}
}
/// Record a learning trajectory
pub fn record_trajectory(&self, trajectory: Trajectory) {
let mut buffer = self.trajectory_buffer.write();
if buffer.len() >= self.config.max_trajectories {
buffer.pop_front();
}
buffer.push_back(trajectory);
}
/// Get trajectory count
pub fn trajectory_count(&self) -> usize {
self.trajectory_buffer.read().len()
}
}
impl Default for SonaLearner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sona_learner_creation() {
let learner = SonaLearner::new();
assert_eq!(learner.config.instant_lora_rank, 2);
assert_eq!(learner.trajectory_count(), 0);
}
#[test]
fn test_trajectory_recording() {
let learner = SonaLearner::new();
let trajectory = Trajectory {
query_embedding: vec![0.1, 0.2, 0.3],
quality_score: 0.95,
latency_us: 100,
timestamp: 12345,
activations: vec![0.5, 0.5],
};
learner.record_trajectory(trajectory);
assert_eq!(learner.trajectory_count(), 1);
}
}
@@ -0,0 +1,715 @@
//! ReasoningBank - Pattern storage and extraction for SONA in edge-net
//!
//! Implements trajectory clustering using K-means++ for pattern discovery.
//! Optimized for WASM with FxHashMap and spatial indexing.
use crate::ai::sona::types::{LearnedPattern, PatternType, QueryTrajectory};
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
/// ReasoningBank configuration
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PatternConfig {
/// Number of clusters for K-means++
pub k_clusters: usize,
/// Embedding dimension
pub embedding_dim: usize,
/// Maximum K-means iterations
pub max_iterations: usize,
/// Convergence threshold
pub convergence_threshold: f32,
/// Minimum cluster size to keep
pub min_cluster_size: usize,
/// Maximum trajectories to store
pub max_trajectories: usize,
/// Quality threshold for pattern
pub quality_threshold: f32,
}
impl Default for PatternConfig {
fn default() -> Self {
// OPTIMIZED DEFAULTS for edge deployment:
// - 50 clusters for smaller memory footprint
// - Lower max_trajectories for edge devices
Self {
k_clusters: 50, // Smaller for edge
embedding_dim: 128, // Smaller for edge
max_iterations: 100,
convergence_threshold: 0.001,
min_cluster_size: 3, // Lower for smaller samples
max_trajectories: 500, // Smaller for edge
quality_threshold: 0.3, // Lower threshold for more learning
}
}
}
/// Internal trajectory entry with embedding
#[derive(Clone, Debug)]
struct TrajectoryEntry {
/// Trajectory embedding (query + avg activations)
embedding: Vec<f32>,
/// Quality score
quality: f32,
/// Cluster assignment
cluster: Option<usize>,
/// Original trajectory ID
trajectory_id: u64,
}
/// Spatial bucket for fast approximate nearest neighbor search
struct SpatialBucket {
pattern_ids: Vec<u64>,
}
/// ReasoningBank for pattern storage and extraction
/// Optimized with spatial indexing for O(1) approximate lookups
pub struct ReasoningBank {
/// Configuration
config: PatternConfig,
/// Stored trajectories
trajectories: Vec<TrajectoryEntry>,
/// Extracted patterns
patterns: FxHashMap<u64, LearnedPattern>,
/// Next pattern ID
next_pattern_id: u64,
/// Spatial index for fast approximate nearest neighbor
spatial_index: FxHashMap<u64, SpatialBucket>,
}
impl ReasoningBank {
/// Create new ReasoningBank
pub fn new(config: PatternConfig) -> Self {
Self {
config,
trajectories: Vec::new(),
patterns: FxHashMap::default(),
next_pattern_id: 0,
spatial_index: FxHashMap::default(),
}
}
/// Hash a vector into a spatial bucket (locality-sensitive hashing)
fn spatial_hash(vector: &[f32]) -> u64 {
// Simple grid-based quantization for fast approximate matching
// Quantize each dimension to 8 levels (3 bits)
let mut hash = 0u64;
for (i, &val) in vector.iter().take(20).enumerate() {
// Normalize to [0, 7] range
let quantized = ((val + 1.0) * 3.5).clamp(0.0, 7.0) as u64;
hash |= quantized << (i * 3);
}
hash
}
/// Add trajectory to bank
pub fn add_trajectory(&mut self, trajectory: &QueryTrajectory) {
// Compute embedding from trajectory
let embedding = self.compute_embedding(trajectory);
let entry = TrajectoryEntry {
embedding,
quality: trajectory.final_quality,
cluster: None,
trajectory_id: trajectory.id,
};
// Enforce capacity
if self.trajectories.len() >= self.config.max_trajectories {
// Remove oldest entries
let to_remove = self.trajectories.len() - self.config.max_trajectories + 1;
self.trajectories.drain(0..to_remove);
}
self.trajectories.push(entry);
}
/// Compute embedding from trajectory
fn compute_embedding(&self, trajectory: &QueryTrajectory) -> Vec<f32> {
let dim = self.config.embedding_dim;
let mut embedding = vec![0.0f32; dim];
// Start with query embedding
let query_len = trajectory.query_embedding.len().min(dim);
embedding[..query_len].copy_from_slice(&trajectory.query_embedding[..query_len]);
// Average in step activations (weighted by reward)
if !trajectory.steps.is_empty() {
let mut total_reward = 0.0f32;
for step in &trajectory.steps {
let weight = step.reward.max(0.0);
total_reward += weight;
for (i, &act) in step.activations.iter().enumerate() {
if i < dim {
embedding[i] += act * weight;
}
}
}
if total_reward > 0.0 {
for e in &mut embedding {
*e /= total_reward + 1.0; // +1 for query contribution
}
}
}
// L2 normalize
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-8 {
for e in &mut embedding {
*e /= norm;
}
}
embedding
}
/// Extract patterns using K-means++
pub fn extract_patterns(&mut self) -> Vec<LearnedPattern> {
if self.trajectories.is_empty() {
return Vec::new();
}
let k = self.config.k_clusters.min(self.trajectories.len());
if k == 0 {
return Vec::new();
}
// K-means++ initialization
let centroids = self.kmeans_plus_plus_init(k);
// Run K-means
let (final_centroids, assignments) = self.run_kmeans(centroids);
// Create patterns from clusters
let mut patterns = Vec::new();
for (cluster_idx, centroid) in final_centroids.into_iter().enumerate() {
// Collect cluster members
let members: Vec<_> = self
.trajectories
.iter()
.enumerate()
.filter(|(i, _)| assignments.get(*i) == Some(&cluster_idx))
.map(|(_, t)| t)
.collect();
if members.len() < self.config.min_cluster_size {
continue;
}
// Compute cluster statistics
let cluster_size = members.len();
let total_weight: f32 = members.iter().map(|t| t.quality).sum();
let avg_quality = total_weight / cluster_size as f32;
if avg_quality < self.config.quality_threshold {
continue;
}
let pattern_id = self.next_pattern_id;
self.next_pattern_id += 1;
let pattern = LearnedPattern {
id: pattern_id,
centroid: centroid.clone(),
cluster_size,
total_weight,
avg_quality,
created_at: (js_sys::Date::now() / 1000.0) as u64,
last_accessed: (js_sys::Date::now() / 1000.0) as u64,
access_count: 0,
pattern_type: PatternType::General,
};
// Add to spatial index
let hash = Self::spatial_hash(&centroid);
self.spatial_index
.entry(hash)
.or_insert_with(|| SpatialBucket { pattern_ids: Vec::with_capacity(10) })
.pattern_ids
.push(pattern_id);
self.patterns.insert(pattern_id, pattern.clone());
patterns.push(pattern);
}
// Update trajectory cluster assignments
for (i, cluster) in assignments.into_iter().enumerate() {
if i < self.trajectories.len() {
self.trajectories[i].cluster = Some(cluster);
}
}
patterns
}
/// K-means++ initialization
fn kmeans_plus_plus_init(&self, k: usize) -> Vec<Vec<f32>> {
let mut centroids = Vec::with_capacity(k);
let n = self.trajectories.len();
if n == 0 || k == 0 {
return centroids;
}
// First centroid: use first trajectory (deterministic for reproducibility)
let first_idx = 0;
centroids.push(self.trajectories[first_idx].embedding.clone());
// Remaining centroids: D^2 weighting
for _ in 1..k {
// Compute distances to nearest centroid
let mut distances: Vec<f32> = self
.trajectories
.iter()
.map(|t| {
centroids
.iter()
.map(|c| self.squared_distance(&t.embedding, c))
.fold(f32::MAX, f32::min)
})
.collect();
// Normalize to probabilities
let total: f32 = distances.iter().sum();
if total > 0.0 {
for d in &mut distances {
*d /= total;
}
}
// Select next centroid (deterministic: highest distance)
let (next_idx, _) = distances
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap_or((0, &0.0));
centroids.push(self.trajectories[next_idx].embedding.clone());
}
centroids
}
/// Run K-means algorithm
fn run_kmeans(&self, mut centroids: Vec<Vec<f32>>) -> (Vec<Vec<f32>>, Vec<usize>) {
let n = self.trajectories.len();
let k = centroids.len();
let dim = self.config.embedding_dim;
let mut assignments = vec![0usize; n];
for _iter in 0..self.config.max_iterations {
// Assign points to nearest centroid
let mut changed = false;
for (i, t) in self.trajectories.iter().enumerate() {
let (nearest, _) = centroids
.iter()
.enumerate()
.map(|(j, c)| (j, self.squared_distance(&t.embedding, c)))
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.unwrap_or((0, 0.0));
if assignments[i] != nearest {
assignments[i] = nearest;
changed = true;
}
}
if !changed {
break;
}
// Update centroids
let mut new_centroids = vec![vec![0.0f32; dim]; k];
let mut counts = vec![0usize; k];
for (i, t) in self.trajectories.iter().enumerate() {
let cluster = assignments[i];
counts[cluster] += 1;
for (j, &e) in t.embedding.iter().enumerate() {
if j < dim {
new_centroids[cluster][j] += e;
}
}
}
// Average and check convergence
let mut max_shift = 0.0f32;
for (i, new_c) in new_centroids.iter_mut().enumerate() {
if counts[i] > 0 {
for e in new_c.iter_mut() {
*e /= counts[i] as f32;
}
let shift = self.squared_distance(new_c, &centroids[i]).sqrt();
max_shift = max_shift.max(shift);
}
}
centroids = new_centroids;
if max_shift < self.config.convergence_threshold {
break;
}
}
(centroids, assignments)
}
/// Squared Euclidean distance
fn squared_distance(&self, a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y) * (x - y))
.sum()
}
/// Find similar patterns (OPTIMIZED with spatial indexing)
pub fn find_similar(&self, query: &[f32], k: usize) -> Vec<&LearnedPattern> {
if self.patterns.is_empty() {
return Vec::new();
}
let query_hash = Self::spatial_hash(query);
let mut candidate_ids = Vec::with_capacity(k * 3);
// Get patterns from same bucket
if let Some(bucket) = self.spatial_index.get(&query_hash) {
candidate_ids.extend_from_slice(&bucket.pattern_ids);
}
// Check neighboring buckets (increase recall)
for bit_flip in 0..6 {
let neighbor_hash = query_hash ^ (1u64 << (bit_flip * 3));
if let Some(bucket) = self.spatial_index.get(&neighbor_hash) {
candidate_ids.extend_from_slice(&bucket.pattern_ids);
}
}
// Fallback: if too few candidates, scan more
if candidate_ids.len() < k {
for bucket in self.spatial_index.values().take(10) {
candidate_ids.extend_from_slice(&bucket.pattern_ids);
if candidate_ids.len() >= k * 2 {
break;
}
}
}
// Compute exact similarity for candidates
let mut scored: Vec<_> = candidate_ids
.iter()
.filter_map(|&id| self.patterns.get(&id))
.map(|p| (p, p.similarity(query)))
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scored.into_iter().take(k).map(|(p, _)| p).collect()
}
/// Find similar patterns with mutable access (updates access counts)
pub fn find_similar_mut(&mut self, query: &[f32], k: usize) -> Vec<LearnedPattern> {
let query_hash = Self::spatial_hash(query);
let mut candidate_ids = Vec::with_capacity(k * 3);
// Get patterns from same bucket
if let Some(bucket) = self.spatial_index.get(&query_hash) {
candidate_ids.extend_from_slice(&bucket.pattern_ids);
}
// Check neighboring buckets
for bit_flip in 0..6 {
let neighbor_hash = query_hash ^ (1u64 << (bit_flip * 3));
if let Some(bucket) = self.spatial_index.get(&neighbor_hash) {
candidate_ids.extend_from_slice(&bucket.pattern_ids);
}
}
// Fallback
if candidate_ids.len() < k {
for bucket in self.spatial_index.values().take(10) {
candidate_ids.extend_from_slice(&bucket.pattern_ids);
if candidate_ids.len() >= k * 2 {
break;
}
}
}
// Compute similarity and update access counts
let mut results = Vec::with_capacity(k);
for &id in &candidate_ids {
if let Some(pattern) = self.patterns.get_mut(&id) {
let sim = pattern.similarity(query);
pattern.touch();
results.push((pattern.clone(), sim));
}
}
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
results.into_iter().take(k).map(|(p, _)| p).collect()
}
/// Get pattern by ID
pub fn get_pattern(&self, id: u64) -> Option<&LearnedPattern> {
self.patterns.get(&id)
}
/// Get mutable pattern by ID
pub fn get_pattern_mut(&mut self, id: u64) -> Option<&mut LearnedPattern> {
self.patterns.get_mut(&id)
}
/// Get trajectory count
pub fn trajectory_count(&self) -> usize {
self.trajectories.len()
}
/// Get pattern count
pub fn pattern_count(&self) -> usize {
self.patterns.len()
}
/// Clear trajectories (keep patterns)
pub fn clear_trajectories(&mut self) {
self.trajectories.clear();
}
/// Prune low-quality patterns
pub fn prune_patterns(&mut self, min_quality: f32, min_accesses: u32, max_age_secs: u64) {
let to_remove: Vec<u64> = self
.patterns
.iter()
.filter(|(_, p)| p.should_prune(min_quality, min_accesses, max_age_secs))
.map(|(id, _)| *id)
.collect();
for id in &to_remove {
self.patterns.remove(id);
}
// Update spatial index
for bucket in self.spatial_index.values_mut() {
bucket.pattern_ids.retain(|id| self.patterns.contains_key(id));
}
}
/// Consolidate similar patterns
pub fn consolidate(&mut self, similarity_threshold: f32) {
let pattern_ids: Vec<u64> = self.patterns.keys().copied().collect();
let mut merged = Vec::new();
for i in 0..pattern_ids.len() {
for j in i + 1..pattern_ids.len() {
let id1 = pattern_ids[i];
let id2 = pattern_ids[j];
if merged.contains(&id1) || merged.contains(&id2) {
continue;
}
if let (Some(p1), Some(p2)) = (self.patterns.get(&id1), self.patterns.get(&id2)) {
let sim = p1.similarity(&p2.centroid);
if sim > similarity_threshold {
// Merge p2 into p1
let merged_pattern = p1.merge(p2);
self.patterns.insert(id1, merged_pattern);
merged.push(id2);
}
}
}
}
// Remove merged patterns
for id in merged {
self.patterns.remove(&id);
}
// Update spatial index
for bucket in self.spatial_index.values_mut() {
bucket.pattern_ids.retain(|id| self.patterns.contains_key(id));
}
}
/// Export patterns for P2P sharing (high quality only)
pub fn export_shareable(&self, min_quality: f32, max_count: usize) -> Vec<LearnedPattern> {
let mut patterns: Vec<_> = self
.patterns
.values()
.filter(|p| p.avg_quality >= min_quality)
.cloned()
.collect();
patterns.sort_by(|a, b| {
let score_a = a.avg_quality * a.cluster_size as f32;
let score_b = b.avg_quality * b.cluster_size as f32;
score_b.partial_cmp(&score_a).unwrap_or(std::cmp::Ordering::Equal)
});
patterns.truncate(max_count);
patterns
}
/// Import pattern from P2P (with verification)
pub fn import_pattern(&mut self, mut pattern: LearnedPattern, trust_score: f32) {
// Discount imported patterns by trust score
pattern.avg_quality *= trust_score;
pattern.total_weight *= trust_score;
// Generate new local ID
pattern.id = self.next_pattern_id;
self.next_pattern_id += 1;
// Add to spatial index
let hash = Self::spatial_hash(&pattern.centroid);
self.spatial_index
.entry(hash)
.or_insert_with(|| SpatialBucket { pattern_ids: Vec::with_capacity(10) })
.pattern_ids
.push(pattern.id);
self.patterns.insert(pattern.id, pattern);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_trajectory(id: u64, embedding: Vec<f32>, quality: f32) -> QueryTrajectory {
let mut t = QueryTrajectory::new(id, embedding);
t.finalize(quality, 1000);
t
}
#[test]
fn test_bank_creation() {
let bank = ReasoningBank::new(PatternConfig::default());
assert_eq!(bank.trajectory_count(), 0);
assert_eq!(bank.pattern_count(), 0);
}
#[test]
fn test_add_trajectory() {
let config = PatternConfig {
embedding_dim: 4,
..Default::default()
};
let mut bank = ReasoningBank::new(config);
let t = make_trajectory(1, vec![0.1, 0.2, 0.3, 0.4], 0.8);
bank.add_trajectory(&t);
assert_eq!(bank.trajectory_count(), 1);
}
#[test]
fn test_extract_patterns() {
let config = PatternConfig {
embedding_dim: 4,
k_clusters: 2,
min_cluster_size: 2,
quality_threshold: 0.0,
..Default::default()
};
let mut bank = ReasoningBank::new(config);
// Add clustered trajectories
for i in 0..5 {
let t = make_trajectory(i, vec![1.0, 0.0, 0.0, 0.0], 0.8);
bank.add_trajectory(&t);
}
for i in 5..10 {
let t = make_trajectory(i, vec![0.0, 1.0, 0.0, 0.0], 0.7);
bank.add_trajectory(&t);
}
let patterns = bank.extract_patterns();
assert!(!patterns.is_empty());
}
#[test]
fn test_find_similar() {
let config = PatternConfig {
embedding_dim: 4,
k_clusters: 2,
min_cluster_size: 2,
quality_threshold: 0.0,
..Default::default()
};
let mut bank = ReasoningBank::new(config);
for i in 0..10 {
let emb = if i < 5 {
vec![1.0, 0.0, 0.0, 0.0]
} else {
vec![0.0, 1.0, 0.0, 0.0]
};
bank.add_trajectory(&make_trajectory(i, emb, 0.8));
}
bank.extract_patterns();
let query = vec![0.9, 0.1, 0.0, 0.0];
let similar = bank.find_similar(&query, 1);
assert!(!similar.is_empty());
}
#[test]
fn test_consolidate() {
let config = PatternConfig {
embedding_dim: 4,
k_clusters: 3,
min_cluster_size: 1,
quality_threshold: 0.0,
..Default::default()
};
let mut bank = ReasoningBank::new(config);
// Create very similar trajectories
for i in 0..9 {
let emb = vec![1.0 + (i as f32 * 0.001), 0.0, 0.0, 0.0];
bank.add_trajectory(&make_trajectory(i, emb, 0.8));
}
bank.extract_patterns();
let before = bank.pattern_count();
bank.consolidate(0.99);
let after = bank.pattern_count();
assert!(after <= before);
}
#[test]
fn test_export_import() {
let config = PatternConfig {
embedding_dim: 4,
k_clusters: 2,
min_cluster_size: 2,
quality_threshold: 0.0,
..Default::default()
};
let mut bank1 = ReasoningBank::new(config.clone());
let mut bank2 = ReasoningBank::new(config);
// Build patterns in bank1
for i in 0..10 {
bank1.add_trajectory(&make_trajectory(i, vec![1.0, 0.0, 0.0, 0.0], 0.8));
}
bank1.extract_patterns();
// Export and import to bank2
let exported = bank1.export_shareable(0.5, 10);
assert!(!exported.is_empty());
for pattern in exported {
bank2.import_pattern(pattern, 0.9); // 90% trust
}
assert!(bank2.pattern_count() > 0);
}
}
@@ -0,0 +1,444 @@
//! Lock-free trajectory buffer for SONA in edge-net
//!
//! Provides efficient, non-blocking trajectory recording during P2P task execution.
//! Optimized for WASM with no external dependencies (uses parking_lot).
use crate::ai::sona::types::{QueryTrajectory, TrajectoryStep};
use parking_lot::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
/// Ring buffer for trajectory storage
/// Uses RwLock for WASM compatibility (crossbeam not available)
pub struct TrajectoryBuffer {
/// Ring buffer storage
buffer: RwLock<Vec<Option<QueryTrajectory>>>,
/// Write position
write_pos: AtomicU64,
/// Read position (for drain operations)
read_pos: AtomicU64,
/// Capacity
capacity: usize,
/// Count of dropped trajectories (buffer full)
dropped: AtomicU64,
/// Total trajectories seen
total_seen: AtomicU64,
}
impl TrajectoryBuffer {
/// Create new buffer with capacity
pub fn new(capacity: usize) -> Self {
let capacity = capacity.max(16); // Minimum 16 slots
Self {
buffer: RwLock::new(vec![None; capacity]),
write_pos: AtomicU64::new(0),
read_pos: AtomicU64::new(0),
capacity,
dropped: AtomicU64::new(0),
total_seen: AtomicU64::new(0),
}
}
/// Record trajectory (non-blocking attempt)
/// Returns true if recorded, false if buffer full
pub fn record(&self, trajectory: QueryTrajectory) -> bool {
self.total_seen.fetch_add(1, Ordering::Relaxed);
// Try to get write lock without blocking for too long
if let Some(mut buffer) = self.buffer.try_write() {
let pos = self.write_pos.fetch_add(1, Ordering::Relaxed) as usize % self.capacity;
buffer[pos] = Some(trajectory);
true
} else {
self.dropped.fetch_add(1, Ordering::Relaxed);
false
}
}
/// Try to pop single trajectory
pub fn pop(&self) -> Option<QueryTrajectory> {
let mut buffer = self.buffer.write();
let write_pos = self.write_pos.load(Ordering::Relaxed);
let read_pos = self.read_pos.load(Ordering::Relaxed);
if read_pos >= write_pos {
return None;
}
let pos = read_pos as usize % self.capacity;
let trajectory = buffer[pos].take();
if trajectory.is_some() {
self.read_pos.fetch_add(1, Ordering::Relaxed);
}
trajectory
}
/// Drain all trajectories
pub fn drain(&self) -> Vec<QueryTrajectory> {
let mut buffer = self.buffer.write();
let mut result = Vec::with_capacity(self.len());
for slot in buffer.iter_mut() {
if let Some(traj) = slot.take() {
result.push(traj);
}
}
// Reset positions
self.write_pos.store(0, Ordering::Relaxed);
self.read_pos.store(0, Ordering::Relaxed);
result
}
/// Drain up to n trajectories
pub fn drain_n(&self, n: usize) -> Vec<QueryTrajectory> {
let mut buffer = self.buffer.write();
let mut result = Vec::with_capacity(n.min(self.capacity));
let write_pos = self.write_pos.load(Ordering::Relaxed);
let mut read_pos = self.read_pos.load(Ordering::Relaxed);
for _ in 0..n {
if read_pos >= write_pos {
break;
}
let pos = read_pos as usize % self.capacity;
if let Some(traj) = buffer[pos].take() {
result.push(traj);
read_pos += 1;
} else {
break;
}
}
self.read_pos.store(read_pos, Ordering::Relaxed);
result
}
/// Get approximate current length
pub fn len(&self) -> usize {
let write = self.write_pos.load(Ordering::Relaxed);
let read = self.read_pos.load(Ordering::Relaxed);
(write.saturating_sub(read)) as usize
}
/// Check if empty
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Check if full
pub fn is_full(&self) -> bool {
self.len() >= self.capacity
}
/// Get capacity
pub fn capacity(&self) -> usize {
self.capacity
}
/// Get dropped count
pub fn dropped_count(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
/// Get total seen count
pub fn total_seen(&self) -> u64 {
self.total_seen.load(Ordering::Relaxed)
}
/// Get success rate
pub fn success_rate(&self) -> f64 {
let total = self.total_seen.load(Ordering::Relaxed);
let dropped = self.dropped.load(Ordering::Relaxed);
if total == 0 {
1.0
} else {
(total - dropped) as f64 / total as f64
}
}
/// Reset statistics (not the buffer contents)
pub fn reset_stats(&self) {
self.dropped.store(0, Ordering::Relaxed);
self.total_seen.store(0, Ordering::Relaxed);
}
}
/// Builder for constructing trajectories during task execution
pub struct TrajectoryBuilder {
/// Trajectory ID
id: u64,
/// Query/task embedding
query_embedding: Vec<f32>,
/// Steps collected
steps: Vec<TrajectoryStep>,
/// Start time (ms since epoch)
start_time_ms: u64,
/// Node ID
node_id: Option<String>,
/// Task type
task_type: Option<String>,
/// Context IDs
context_ids: Vec<String>,
}
impl TrajectoryBuilder {
/// Start new trajectory
pub fn new(id: u64, query_embedding: Vec<f32>) -> Self {
Self {
id,
query_embedding,
steps: Vec::with_capacity(16),
start_time_ms: js_sys::Date::now() as u64,
node_id: None,
task_type: None,
context_ids: Vec::new(),
}
}
/// Start trajectory with node context
pub fn with_node(id: u64, query_embedding: Vec<f32>, node_id: &str) -> Self {
let mut builder = Self::new(id, query_embedding);
builder.node_id = Some(node_id.to_string());
builder
}
/// Add execution step
pub fn add_step(&mut self, activations: Vec<f32>, attention_weights: Vec<f32>, reward: f32) {
let step_idx = self.steps.len();
self.steps.push(TrajectoryStep::new(
activations,
attention_weights,
reward,
step_idx,
));
}
/// Add step with layer name
pub fn add_named_step(
&mut self,
name: &str,
activations: Vec<f32>,
attention_weights: Vec<f32>,
reward: f32,
) {
let step_idx = self.steps.len();
self.steps.push(
TrajectoryStep::new(activations, attention_weights, reward, step_idx).with_layer(name),
);
}
/// Set task type
pub fn set_task_type(&mut self, task_type: &str) {
self.task_type = Some(task_type.to_string());
}
/// Add context ID (e.g., RAC event ID)
pub fn add_context(&mut self, context_id: &str) {
self.context_ids.push(context_id.to_string());
}
/// Get current step count
pub fn step_count(&self) -> usize {
self.steps.len()
}
/// Get elapsed time in milliseconds
pub fn elapsed_ms(&self) -> u64 {
let now = js_sys::Date::now() as u64;
now.saturating_sub(self.start_time_ms)
}
/// Finalize and build trajectory
pub fn build(self, final_quality: f32) -> QueryTrajectory {
let latency_us = self.elapsed_ms() * 1000;
let mut trajectory = QueryTrajectory {
id: self.id,
query_embedding: self.query_embedding,
steps: self.steps,
final_quality,
latency_us,
node_id: self.node_id,
task_type: self.task_type,
context_ids: self.context_ids,
};
trajectory
}
/// Build with explicit latency
pub fn build_with_latency(self, final_quality: f32, latency_us: u64) -> QueryTrajectory {
QueryTrajectory {
id: self.id,
query_embedding: self.query_embedding,
steps: self.steps,
final_quality,
latency_us,
node_id: self.node_id,
task_type: self.task_type,
context_ids: self.context_ids,
}
}
}
/// Trajectory ID generator
pub struct TrajectoryIdGen {
counter: AtomicU64,
/// Node prefix for unique IDs across P2P network
node_prefix: u64,
}
impl TrajectoryIdGen {
/// Create new generator
pub fn new() -> Self {
Self {
counter: AtomicU64::new(0),
node_prefix: 0,
}
}
/// Create with starting ID
pub fn with_start(start: u64) -> Self {
Self {
counter: AtomicU64::new(start),
node_prefix: 0,
}
}
/// Create with node prefix for P2P uniqueness
pub fn with_node_prefix(node_id: &str) -> Self {
// Use first 16 bits of node_id hash as prefix
let hash = node_id.bytes().fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64));
Self {
counter: AtomicU64::new(0),
node_prefix: (hash & 0xFFFF) << 48,
}
}
/// Generate next ID
pub fn next(&self) -> u64 {
let counter = self.counter.fetch_add(1, Ordering::Relaxed);
self.node_prefix | counter
}
/// Get current value without incrementing
pub fn current(&self) -> u64 {
self.node_prefix | self.counter.load(Ordering::Relaxed)
}
}
impl Default for TrajectoryIdGen {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_buffer_basic_ops() {
let buffer = TrajectoryBuffer::new(10);
assert!(buffer.is_empty());
assert_eq!(buffer.capacity(), 10);
let trajectory = QueryTrajectory::new(1, vec![0.1, 0.2]);
assert!(buffer.record(trajectory));
assert_eq!(buffer.len(), 1);
assert!(!buffer.is_empty());
}
#[test]
fn test_buffer_drain() {
let buffer = TrajectoryBuffer::new(10);
for i in 0..5 {
let trajectory = QueryTrajectory::new(i, vec![0.1]);
buffer.record(trajectory);
}
let drained = buffer.drain();
assert_eq!(drained.len(), 5);
assert!(buffer.is_empty());
}
#[test]
fn test_buffer_drain_n() {
let buffer = TrajectoryBuffer::new(10);
for i in 0..5 {
let trajectory = QueryTrajectory::new(i, vec![0.1]);
buffer.record(trajectory);
}
let partial = buffer.drain_n(3);
assert_eq!(partial.len(), 3);
assert_eq!(buffer.len(), 2);
}
#[test]
fn test_builder() {
let mut builder = TrajectoryBuilder::new(42, vec![0.1, 0.2, 0.3]);
builder.add_step(vec![0.5], vec![0.4, 0.6], 0.7);
builder.add_step(vec![0.6], vec![0.3, 0.7], 0.8);
builder.set_task_type("compute");
builder.add_context("rac-event-123");
assert_eq!(builder.step_count(), 2);
let trajectory = builder.build(0.85);
assert_eq!(trajectory.id, 42);
assert_eq!(trajectory.steps.len(), 2);
assert_eq!(trajectory.final_quality, 0.85);
assert_eq!(trajectory.task_type, Some("compute".to_string()));
assert!(trajectory.latency_us > 0);
}
#[test]
fn test_id_generator() {
let gen = TrajectoryIdGen::new();
assert_eq!(gen.next(), 0);
assert_eq!(gen.next(), 1);
assert_eq!(gen.next(), 2);
assert_eq!(gen.current(), 3);
}
#[test]
fn test_id_generator_with_prefix() {
let gen1 = TrajectoryIdGen::with_node_prefix("node-alpha");
let gen2 = TrajectoryIdGen::with_node_prefix("node-beta");
let id1 = gen1.next();
let id2 = gen2.next();
// Different prefixes should produce different IDs
assert_ne!(id1, id2);
}
#[test]
fn test_success_rate() {
let buffer = TrajectoryBuffer::new(2);
// Record 4 trajectories into buffer of size 2
// Some should be dropped due to contention simulation
for i in 0..4 {
buffer.record(QueryTrajectory::new(i, vec![]));
}
// Success rate should be calculable
let rate = buffer.success_rate();
assert!(rate >= 0.0 && rate <= 1.0);
}
}
+592
View File
@@ -0,0 +1,592 @@
//! SONA Core Types for Edge-Net
//!
//! Adapted from ruvLLM SONA for P2P distributed compute networks.
//! Optimized for WASM and edge device deployment.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Learning signal generated from task execution trajectory
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LearningSignal {
/// Query/task embedding vector
pub query_embedding: Vec<f32>,
/// Estimated gradient direction
pub gradient_estimate: Vec<f32>,
/// Quality score [0.0, 1.0]
pub quality_score: f32,
/// Signal generation timestamp (Unix ms)
pub timestamp_ms: u64,
/// Additional metadata
pub metadata: SignalMetadata,
}
/// Metadata for learning signals
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SignalMetadata {
/// Source trajectory ID
pub trajectory_id: u64,
/// Number of steps in trajectory
pub step_count: usize,
/// Node ID that generated this signal
pub node_id: Option<String>,
/// Task type for routing
pub task_type: Option<String>,
/// Custom tags for P2P sharing
pub tags: HashMap<String, String>,
}
impl LearningSignal {
/// Create signal from query trajectory using REINFORCE gradient estimation
pub fn from_trajectory(trajectory: &QueryTrajectory) -> Self {
let gradient = Self::estimate_gradient(trajectory);
Self {
query_embedding: trajectory.query_embedding.clone(),
gradient_estimate: gradient,
quality_score: trajectory.final_quality,
timestamp_ms: js_sys::Date::now() as u64,
metadata: SignalMetadata {
trajectory_id: trajectory.id,
step_count: trajectory.steps.len(),
node_id: trajectory.node_id.clone(),
task_type: trajectory.task_type.clone(),
tags: HashMap::new(),
},
}
}
/// Create signal with pre-computed gradient
pub fn with_gradient(embedding: Vec<f32>, gradient: Vec<f32>, quality: f32) -> Self {
Self {
query_embedding: embedding,
gradient_estimate: gradient,
quality_score: quality,
timestamp_ms: js_sys::Date::now() as u64,
metadata: SignalMetadata::default(),
}
}
/// Estimate gradient using REINFORCE with baseline
fn estimate_gradient(trajectory: &QueryTrajectory) -> Vec<f32> {
if trajectory.steps.is_empty() {
return trajectory.query_embedding.clone();
}
let dim = trajectory.query_embedding.len();
let mut gradient = vec![0.0f32; dim];
// Compute baseline (average reward)
let baseline =
trajectory.steps.iter().map(|s| s.reward).sum::<f32>() / trajectory.steps.len() as f32;
// REINFORCE: gradient = sum((reward - baseline) * activation)
for step in &trajectory.steps {
let advantage = step.reward - baseline;
let activation_len = step.activations.len().min(dim);
for i in 0..activation_len {
gradient[i] += advantage * step.activations[i];
}
}
// L2 normalize
let norm: f32 = gradient.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 1e-8 {
gradient.iter_mut().for_each(|x| *x /= norm);
}
gradient
}
/// Scale gradient by quality
pub fn scaled_gradient(&self) -> Vec<f32> {
self.gradient_estimate
.iter()
.map(|&g| g * self.quality_score)
.collect()
}
}
/// Query/task trajectory recording for P2P learning
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct QueryTrajectory {
/// Unique trajectory identifier
pub id: u64,
/// Query/task embedding vector
pub query_embedding: Vec<f32>,
/// Execution steps
pub steps: Vec<TrajectoryStep>,
/// Final quality score [0.0, 1.0]
pub final_quality: f32,
/// Total latency in microseconds
pub latency_us: u64,
/// Node ID that executed this trajectory
pub node_id: Option<String>,
/// Task type for routing optimization
pub task_type: Option<String>,
/// P2P context IDs (RAC events, etc.)
pub context_ids: Vec<String>,
}
impl QueryTrajectory {
/// Create new trajectory
pub fn new(id: u64, query_embedding: Vec<f32>) -> Self {
Self {
id,
query_embedding,
steps: Vec::with_capacity(16),
final_quality: 0.0,
latency_us: 0,
node_id: None,
task_type: None,
context_ids: Vec::new(),
}
}
/// Create trajectory with node context
pub fn with_node(id: u64, query_embedding: Vec<f32>, node_id: &str) -> Self {
let mut t = Self::new(id, query_embedding);
t.node_id = Some(node_id.to_string());
t
}
/// Add execution step
pub fn add_step(&mut self, step: TrajectoryStep) {
self.steps.push(step);
}
/// Finalize trajectory with quality score
pub fn finalize(&mut self, quality: f32, latency_us: u64) {
self.final_quality = quality;
self.latency_us = latency_us;
}
/// Get total reward
pub fn total_reward(&self) -> f32 {
self.steps.iter().map(|s| s.reward).sum()
}
/// Get average reward
pub fn avg_reward(&self) -> f32 {
if self.steps.is_empty() {
0.0
} else {
self.total_reward() / self.steps.len() as f32
}
}
/// Set task type for routing optimization
pub fn set_task_type(&mut self, task_type: &str) {
self.task_type = Some(task_type.to_string());
}
}
/// Single step in a trajectory
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TrajectoryStep {
/// Layer/module activations (subset for efficiency)
pub activations: Vec<f32>,
/// Attention weights (flattened)
pub attention_weights: Vec<f32>,
/// Reward signal for this step
pub reward: f32,
/// Step index
pub step_idx: usize,
/// Optional layer name
pub layer_name: Option<String>,
}
impl TrajectoryStep {
/// Create new step
pub fn new(
activations: Vec<f32>,
attention_weights: Vec<f32>,
reward: f32,
step_idx: usize,
) -> Self {
Self {
activations,
attention_weights,
reward,
step_idx,
layer_name: None,
}
}
/// Create step with layer name
pub fn with_layer(mut self, name: &str) -> Self {
self.layer_name = Some(name.to_string());
self
}
}
/// Learned pattern from trajectory clustering
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct LearnedPattern {
/// Pattern identifier
pub id: u64,
/// Cluster centroid embedding
pub centroid: Vec<f32>,
/// Number of trajectories in cluster
pub cluster_size: usize,
/// Sum of trajectory weights
pub total_weight: f32,
/// Average quality of member trajectories
pub avg_quality: f32,
/// Creation timestamp (Unix seconds)
pub created_at: u64,
/// Last access timestamp
pub last_accessed: u64,
/// Total access count
pub access_count: u32,
/// Pattern type/category
pub pattern_type: PatternType,
}
/// Pattern classification
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
pub enum PatternType {
#[default]
General,
Compute,
Embedding,
Inference,
Verification,
P2PRouting,
}
impl LearnedPattern {
/// Create new pattern
pub fn new(id: u64, centroid: Vec<f32>) -> Self {
let now = (js_sys::Date::now() / 1000.0) as u64;
Self {
id,
centroid,
cluster_size: 1,
total_weight: 1.0,
avg_quality: 0.0,
created_at: now,
last_accessed: now,
access_count: 0,
pattern_type: PatternType::default(),
}
}
/// Merge two patterns
pub fn merge(&self, other: &Self) -> Self {
let total_size = self.cluster_size + other.cluster_size;
let w1 = self.cluster_size as f32 / total_size as f32;
let w2 = other.cluster_size as f32 / total_size as f32;
let centroid: Vec<f32> = self
.centroid
.iter()
.zip(&other.centroid)
.map(|(&a, &b)| a * w1 + b * w2)
.collect();
Self {
id: self.id,
centroid,
cluster_size: total_size,
total_weight: self.total_weight + other.total_weight,
avg_quality: self.avg_quality * w1 + other.avg_quality * w2,
created_at: self.created_at.min(other.created_at),
last_accessed: self.last_accessed.max(other.last_accessed),
access_count: self.access_count + other.access_count,
pattern_type: self.pattern_type.clone(),
}
}
/// Decay pattern importance
pub fn decay(&mut self, factor: f32) {
self.total_weight *= factor;
}
/// Record access
pub fn touch(&mut self) {
self.access_count += 1;
self.last_accessed = (js_sys::Date::now() / 1000.0) as u64;
}
/// Check if pattern should be pruned
pub fn should_prune(&self, min_quality: f32, min_accesses: u32, max_age_secs: u64) -> bool {
let now = (js_sys::Date::now() / 1000.0) as u64;
let age = now.saturating_sub(self.last_accessed);
self.avg_quality < min_quality && self.access_count < min_accesses && age > max_age_secs
}
/// Compute cosine similarity with query
pub fn similarity(&self, query: &[f32]) -> f32 {
if self.centroid.len() != query.len() {
return 0.0;
}
let dot: f32 = self.centroid.iter().zip(query).map(|(a, b)| a * b).sum();
let norm_a: f32 = self.centroid.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = query.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a > 1e-8 && norm_b > 1e-8 {
dot / (norm_a * norm_b)
} else {
0.0
}
}
}
/// SONA configuration for edge-net
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SonaConfig {
/// Hidden dimension
pub hidden_dim: usize,
/// Embedding dimension
pub embedding_dim: usize,
/// Micro-LoRA rank (1-2 for edge devices)
pub micro_lora_rank: usize,
/// Base LoRA rank
pub base_lora_rank: usize,
/// Micro-LoRA learning rate
pub micro_lora_lr: f32,
/// Base LoRA learning rate
pub base_lora_lr: f32,
/// EWC lambda
pub ewc_lambda: f32,
/// Pattern extraction clusters
pub pattern_clusters: usize,
/// Trajectory buffer capacity
pub trajectory_capacity: usize,
/// Background learning interval (ms)
pub background_interval_ms: u64,
/// Deep consolidation interval (ms) - weekly
pub deep_interval_ms: u64,
/// Quality threshold for learning
pub quality_threshold: f32,
/// Enable SIMD optimizations
pub enable_simd: bool,
/// Enable P2P pattern sharing via RAC
pub enable_p2p_sharing: bool,
}
impl Default for SonaConfig {
fn default() -> Self {
// OPTIMIZED DEFAULTS for edge/WASM deployment:
// - Rank-2 is faster than Rank-1 due to better SIMD vectorization
// - Smaller buffer for memory-constrained devices
// - Lower cluster count for faster search
Self {
hidden_dim: 128, // Smaller for edge devices
embedding_dim: 128,
micro_lora_rank: 2, // OPTIMIZED: Rank-2 faster than Rank-1
base_lora_rank: 4, // Smaller for memory
micro_lora_lr: 0.002, // OPTIMIZED: +55% quality improvement
base_lora_lr: 0.0001,
ewc_lambda: 2000.0, // OPTIMIZED: Better forgetting prevention
pattern_clusters: 50, // Smaller for edge
trajectory_capacity: 500, // Smaller buffer for edge
background_interval_ms: 3600000, // 1 hour
deep_interval_ms: 604800000, // 1 week
quality_threshold: 0.3, // OPTIMIZED: Lower threshold for more learning
enable_simd: true,
enable_p2p_sharing: true, // Enable RAC pattern sharing
}
}
}
impl SonaConfig {
/// Create config optimized for maximum throughput (real-time P2P)
pub fn max_throughput() -> Self {
Self {
hidden_dim: 128,
embedding_dim: 128,
micro_lora_rank: 2,
base_lora_rank: 4,
micro_lora_lr: 0.0005, // Conservative for stability
base_lora_lr: 0.0001,
ewc_lambda: 2000.0,
pattern_clusters: 50,
trajectory_capacity: 200,
background_interval_ms: 7200000, // 2 hours
deep_interval_ms: 604800000,
quality_threshold: 0.4,
enable_simd: true,
enable_p2p_sharing: true,
}
}
/// Create config optimized for maximum quality
pub fn max_quality() -> Self {
Self {
hidden_dim: 256,
embedding_dim: 256,
micro_lora_rank: 2,
base_lora_rank: 8,
micro_lora_lr: 0.002, // Optimal learning rate
base_lora_lr: 0.001, // Aggressive base learning
ewc_lambda: 2000.0,
pattern_clusters: 100,
trajectory_capacity: 1000,
background_interval_ms: 1800000, // 30 minutes
deep_interval_ms: 259200000, // 3 days
quality_threshold: 0.2, // Learn from more trajectories
enable_simd: true,
enable_p2p_sharing: true,
}
}
/// Create config for minimal edge deployment (<5MB memory)
pub fn edge_minimal() -> Self {
Self {
hidden_dim: 64,
embedding_dim: 64,
micro_lora_rank: 1, // Minimal rank for memory
base_lora_rank: 2,
micro_lora_lr: 0.001,
base_lora_lr: 0.0001,
ewc_lambda: 1000.0,
pattern_clusters: 20,
trajectory_capacity: 100, // Very small buffer
background_interval_ms: 3600000,
deep_interval_ms: 604800000,
quality_threshold: 0.5,
enable_simd: true,
enable_p2p_sharing: true,
}
}
/// Create config for P2P compute nodes
pub fn p2p_compute() -> Self {
Self {
hidden_dim: 128,
embedding_dim: 128,
micro_lora_rank: 2,
base_lora_rank: 4,
micro_lora_lr: 0.001,
base_lora_lr: 0.0001,
ewc_lambda: 2000.0,
pattern_clusters: 50,
trajectory_capacity: 500,
background_interval_ms: 3600000,
deep_interval_ms: 604800000,
quality_threshold: 0.3,
enable_simd: true,
enable_p2p_sharing: true, // Enable pattern sharing
}
}
}
/// P2P shareable pattern for RAC events
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ShareablePattern {
/// Pattern ID
pub id: u64,
/// Centroid (can be quantized for efficiency)
pub centroid: Vec<f32>,
/// Quality score
pub avg_quality: f32,
/// Cluster size (credibility)
pub cluster_size: usize,
/// Origin node ID
pub origin_node: String,
/// Signature for verification
pub signature: Option<Vec<u8>>,
}
impl From<&LearnedPattern> for ShareablePattern {
fn from(pattern: &LearnedPattern) -> Self {
Self {
id: pattern.id,
centroid: pattern.centroid.clone(),
avg_quality: pattern.avg_quality,
cluster_size: pattern.cluster_size,
origin_node: String::new(),
signature: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_learning_signal_from_trajectory() {
let mut trajectory = QueryTrajectory::new(1, vec![0.1, 0.2, 0.3]);
trajectory.add_step(TrajectoryStep::new(
vec![0.5, 0.3, 0.2],
vec![0.4, 0.4, 0.2],
0.8,
0,
));
trajectory.finalize(0.8, 1000);
let signal = LearningSignal::from_trajectory(&trajectory);
assert_eq!(signal.quality_score, 0.8);
assert_eq!(signal.gradient_estimate.len(), 3);
assert_eq!(signal.metadata.trajectory_id, 1);
}
#[test]
fn test_pattern_merge() {
let p1 = LearnedPattern {
id: 1,
centroid: vec![1.0, 0.0],
cluster_size: 10,
total_weight: 5.0,
avg_quality: 0.8,
created_at: 100,
last_accessed: 200,
access_count: 5,
pattern_type: PatternType::General,
};
let p2 = LearnedPattern {
id: 2,
centroid: vec![0.0, 1.0],
cluster_size: 10,
total_weight: 5.0,
avg_quality: 0.9,
created_at: 150,
last_accessed: 250,
access_count: 3,
pattern_type: PatternType::General,
};
let merged = p1.merge(&p2);
assert_eq!(merged.cluster_size, 20);
assert!((merged.centroid[0] - 0.5).abs() < 1e-6);
assert!((merged.centroid[1] - 0.5).abs() < 1e-6);
assert!((merged.avg_quality - 0.85).abs() < 1e-6);
}
#[test]
fn test_pattern_similarity() {
let pattern = LearnedPattern::new(1, vec![1.0, 0.0, 0.0]);
assert!((pattern.similarity(&[1.0, 0.0, 0.0]) - 1.0).abs() < 1e-6);
assert!(pattern.similarity(&[0.0, 1.0, 0.0]).abs() < 1e-6);
}
#[test]
fn test_trajectory_rewards() {
let mut trajectory = QueryTrajectory::new(1, vec![0.1]);
trajectory.add_step(TrajectoryStep::new(vec![], vec![], 0.5, 0));
trajectory.add_step(TrajectoryStep::new(vec![], vec![], 0.7, 1));
trajectory.add_step(TrajectoryStep::new(vec![], vec![], 0.9, 2));
assert!((trajectory.total_reward() - 2.1).abs() < 1e-6);
assert!((trajectory.avg_reward() - 0.7).abs() < 1e-6);
}
#[test]
fn test_config_profiles() {
let edge = SonaConfig::edge_minimal();
assert_eq!(edge.hidden_dim, 64);
assert_eq!(edge.micro_lora_rank, 1);
let quality = SonaConfig::max_quality();
assert_eq!(quality.hidden_dim, 256);
assert_eq!(quality.base_lora_rank, 8);
}
}