Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
309
vendor/ruvector/crates/ruvector-dag/src/sona/engine.rs
vendored
Normal file
309
vendor/ruvector/crates/ruvector-dag/src/sona/engine.rs
vendored
Normal file
@@ -0,0 +1,309 @@
|
||||
//! DagSonaEngine: Main orchestration for SONA learning
|
||||
|
||||
use super::{
|
||||
DagReasoningBank, DagTrajectory, DagTrajectoryBuffer, EwcConfig, EwcPlusPlus, MicroLoRA,
|
||||
MicroLoRAConfig, ReasoningBankConfig,
|
||||
};
|
||||
use crate::dag::{OperatorType, QueryDag};
|
||||
use ndarray::Array1;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
pub struct DagSonaEngine {
|
||||
micro_lora: MicroLoRA,
|
||||
trajectory_buffer: DagTrajectoryBuffer,
|
||||
reasoning_bank: DagReasoningBank,
|
||||
#[allow(dead_code)]
|
||||
ewc: EwcPlusPlus,
|
||||
embedding_dim: usize,
|
||||
}
|
||||
|
||||
impl DagSonaEngine {
|
||||
pub fn new(embedding_dim: usize) -> Self {
|
||||
Self {
|
||||
micro_lora: MicroLoRA::new(MicroLoRAConfig::default(), embedding_dim),
|
||||
trajectory_buffer: DagTrajectoryBuffer::new(1000),
|
||||
reasoning_bank: DagReasoningBank::new(ReasoningBankConfig {
|
||||
pattern_dim: embedding_dim,
|
||||
..Default::default()
|
||||
}),
|
||||
ewc: EwcPlusPlus::new(EwcConfig::default()),
|
||||
embedding_dim,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-query instant adaptation (<100μs)
|
||||
pub fn pre_query(&mut self, dag: &QueryDag) -> Vec<f32> {
|
||||
let embedding = self.compute_dag_embedding(dag);
|
||||
|
||||
// Query similar patterns
|
||||
let similar = self.reasoning_bank.query_similar(&embedding, 3);
|
||||
|
||||
// If we have similar patterns, adapt MicroLoRA
|
||||
if !similar.is_empty() {
|
||||
let adaptation_signal = self.compute_adaptation_signal(&similar, &embedding);
|
||||
self.micro_lora
|
||||
.adapt(&Array1::from_vec(adaptation_signal), 0.01);
|
||||
}
|
||||
|
||||
// Return enhanced embedding
|
||||
self.micro_lora
|
||||
.forward(&Array1::from_vec(embedding))
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
/// Post-query trajectory recording
|
||||
pub fn post_query(
|
||||
&mut self,
|
||||
dag: &QueryDag,
|
||||
execution_time_ms: f64,
|
||||
baseline_time_ms: f64,
|
||||
attention_mechanism: &str,
|
||||
) {
|
||||
let embedding = self.compute_dag_embedding(dag);
|
||||
let trajectory = DagTrajectory::new(
|
||||
self.hash_dag(dag),
|
||||
embedding,
|
||||
attention_mechanism.to_string(),
|
||||
execution_time_ms,
|
||||
baseline_time_ms,
|
||||
);
|
||||
|
||||
self.trajectory_buffer.push(trajectory);
|
||||
}
|
||||
|
||||
/// Background learning cycle (called periodically)
|
||||
pub fn background_learn(&mut self) {
|
||||
let trajectories = self.trajectory_buffer.drain();
|
||||
if trajectories.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store high-quality patterns
|
||||
for t in &trajectories {
|
||||
if t.quality() > 0.6 {
|
||||
self.reasoning_bank
|
||||
.store_pattern(t.dag_embedding.clone(), t.quality());
|
||||
}
|
||||
}
|
||||
|
||||
// Recompute clusters periodically (every 100 patterns)
|
||||
if self.reasoning_bank.pattern_count() % 100 == 0 {
|
||||
self.reasoning_bank.recompute_clusters();
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_dag_embedding(&self, dag: &QueryDag) -> Vec<f32> {
|
||||
// Compute embedding from DAG structure
|
||||
let mut embedding = vec![0.0; self.embedding_dim];
|
||||
|
||||
if dag.node_count() == 0 {
|
||||
return embedding;
|
||||
}
|
||||
|
||||
// Encode operator type distribution (20 different types)
|
||||
let mut type_counts = vec![0usize; 20];
|
||||
for node in dag.nodes() {
|
||||
let type_idx = match &node.op_type {
|
||||
OperatorType::SeqScan { .. } => 0,
|
||||
OperatorType::IndexScan { .. } => 1,
|
||||
OperatorType::HnswScan { .. } => 2,
|
||||
OperatorType::IvfFlatScan { .. } => 3,
|
||||
OperatorType::NestedLoopJoin => 4,
|
||||
OperatorType::HashJoin { .. } => 5,
|
||||
OperatorType::MergeJoin { .. } => 6,
|
||||
OperatorType::Aggregate { .. } => 7,
|
||||
OperatorType::GroupBy { .. } => 8,
|
||||
OperatorType::Filter { .. } => 9,
|
||||
OperatorType::Project { .. } => 10,
|
||||
OperatorType::Sort { .. } => 11,
|
||||
OperatorType::Limit { .. } => 12,
|
||||
OperatorType::VectorDistance { .. } => 13,
|
||||
OperatorType::Rerank { .. } => 14,
|
||||
OperatorType::Materialize => 15,
|
||||
OperatorType::Result => 16,
|
||||
#[allow(deprecated)]
|
||||
OperatorType::Scan => 0, // Treat as SeqScan
|
||||
#[allow(deprecated)]
|
||||
OperatorType::Join => 4, // Treat as NestedLoopJoin
|
||||
};
|
||||
if type_idx < type_counts.len() {
|
||||
type_counts[type_idx] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize and place in embedding
|
||||
let total = dag.node_count() as f32;
|
||||
for (i, count) in type_counts.iter().enumerate() {
|
||||
if i < self.embedding_dim / 2 {
|
||||
embedding[i] = *count as f32 / total;
|
||||
}
|
||||
}
|
||||
|
||||
// Encode structural features (depth, breadth, connectivity)
|
||||
let depth = self.compute_dag_depth(dag);
|
||||
let avg_fanout = dag.node_count() as f32 / (dag.leaves().len().max(1) as f32);
|
||||
|
||||
if self.embedding_dim > 20 {
|
||||
embedding[20] = (depth as f32) / 10.0; // Normalize depth
|
||||
embedding[21] = avg_fanout / 5.0; // Normalize fanout
|
||||
}
|
||||
|
||||
// Encode cost statistics
|
||||
let costs: Vec<f64> = dag.nodes().map(|n| n.estimated_cost).collect();
|
||||
if !costs.is_empty() && self.embedding_dim > 22 {
|
||||
let avg_cost = costs.iter().sum::<f64>() / costs.len() as f64;
|
||||
let max_cost = costs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
embedding[22] = (avg_cost / 1000.0) as f32; // Normalize
|
||||
embedding[23] = (max_cost / 1000.0) as f32;
|
||||
}
|
||||
|
||||
// Normalize entire embedding
|
||||
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm > 0.0 {
|
||||
embedding.iter_mut().for_each(|x| *x /= norm);
|
||||
}
|
||||
|
||||
embedding
|
||||
}
|
||||
|
||||
fn compute_dag_depth(&self, dag: &QueryDag) -> usize {
|
||||
// BFS to find maximum depth
|
||||
use std::collections::VecDeque;
|
||||
|
||||
let mut max_depth = 0;
|
||||
let mut queue = VecDeque::new();
|
||||
|
||||
if let Some(root) = dag.root() {
|
||||
queue.push_back((root, 0));
|
||||
}
|
||||
|
||||
while let Some((node_id, depth)) = queue.pop_front() {
|
||||
max_depth = max_depth.max(depth);
|
||||
for &child in dag.children(node_id) {
|
||||
queue.push_back((child, depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
max_depth
|
||||
}
|
||||
|
||||
fn compute_adaptation_signal(
|
||||
&self,
|
||||
_similar: &[(u64, f32)],
|
||||
_current_embedding: &[f32],
|
||||
) -> Vec<f32> {
|
||||
// Weighted average of similar pattern embeddings
|
||||
// For now, just return zeros as we'd need to store pattern vectors
|
||||
vec![0.0; self.embedding_dim]
|
||||
}
|
||||
|
||||
fn hash_dag(&self, dag: &QueryDag) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
// Hash node types and edges
|
||||
for node in dag.nodes() {
|
||||
node.id.hash(&mut hasher);
|
||||
// Hash operator type discriminant
|
||||
match &node.op_type {
|
||||
OperatorType::SeqScan { table } => {
|
||||
0u8.hash(&mut hasher);
|
||||
table.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::IndexScan { index, table } => {
|
||||
1u8.hash(&mut hasher);
|
||||
index.hash(&mut hasher);
|
||||
table.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::HnswScan { index, ef_search } => {
|
||||
2u8.hash(&mut hasher);
|
||||
index.hash(&mut hasher);
|
||||
ef_search.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::IvfFlatScan { index, nprobe } => {
|
||||
3u8.hash(&mut hasher);
|
||||
index.hash(&mut hasher);
|
||||
nprobe.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::NestedLoopJoin => 4u8.hash(&mut hasher),
|
||||
OperatorType::HashJoin { hash_key } => {
|
||||
5u8.hash(&mut hasher);
|
||||
hash_key.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::MergeJoin { merge_key } => {
|
||||
6u8.hash(&mut hasher);
|
||||
merge_key.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::Aggregate { functions } => {
|
||||
7u8.hash(&mut hasher);
|
||||
for func in functions {
|
||||
func.hash(&mut hasher);
|
||||
}
|
||||
}
|
||||
OperatorType::GroupBy { keys } => {
|
||||
8u8.hash(&mut hasher);
|
||||
for key in keys {
|
||||
key.hash(&mut hasher);
|
||||
}
|
||||
}
|
||||
OperatorType::Filter { predicate } => {
|
||||
9u8.hash(&mut hasher);
|
||||
predicate.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::Project { columns } => {
|
||||
10u8.hash(&mut hasher);
|
||||
for col in columns {
|
||||
col.hash(&mut hasher);
|
||||
}
|
||||
}
|
||||
OperatorType::Sort { keys, descending } => {
|
||||
11u8.hash(&mut hasher);
|
||||
for key in keys {
|
||||
key.hash(&mut hasher);
|
||||
}
|
||||
for &desc in descending {
|
||||
desc.hash(&mut hasher);
|
||||
}
|
||||
}
|
||||
OperatorType::Limit { count } => {
|
||||
12u8.hash(&mut hasher);
|
||||
count.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::VectorDistance { metric } => {
|
||||
13u8.hash(&mut hasher);
|
||||
metric.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::Rerank { model } => {
|
||||
14u8.hash(&mut hasher);
|
||||
model.hash(&mut hasher);
|
||||
}
|
||||
OperatorType::Materialize => 15u8.hash(&mut hasher),
|
||||
OperatorType::Result => 16u8.hash(&mut hasher),
|
||||
#[allow(deprecated)]
|
||||
OperatorType::Scan => 0u8.hash(&mut hasher),
|
||||
#[allow(deprecated)]
|
||||
OperatorType::Join => 4u8.hash(&mut hasher),
|
||||
}
|
||||
}
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
pub fn pattern_count(&self) -> usize {
|
||||
self.reasoning_bank.pattern_count()
|
||||
}
|
||||
|
||||
pub fn trajectory_count(&self) -> usize {
|
||||
self.trajectory_buffer.total_count()
|
||||
}
|
||||
|
||||
pub fn cluster_count(&self) -> usize {
|
||||
self.reasoning_bank.cluster_count()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DagSonaEngine {
|
||||
fn default() -> Self {
|
||||
Self::new(256)
|
||||
}
|
||||
}
|
||||
100
vendor/ruvector/crates/ruvector-dag/src/sona/ewc.rs
vendored
Normal file
100
vendor/ruvector/crates/ruvector-dag/src/sona/ewc.rs
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
//! EWC++: Elastic Weight Consolidation to prevent forgetting
|
||||
|
||||
use ndarray::Array1;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EwcConfig {
|
||||
pub lambda: f32, // Importance weight (2000-15000)
|
||||
pub decay: f32, // Fisher decay rate
|
||||
pub online: bool, // Use online EWC
|
||||
}
|
||||
|
||||
impl Default for EwcConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
lambda: 5000.0,
|
||||
decay: 0.99,
|
||||
online: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EwcPlusPlus {
|
||||
config: EwcConfig,
|
||||
fisher_diag: Option<Array1<f32>>,
|
||||
optimal_params: Option<Array1<f32>>,
|
||||
task_count: usize,
|
||||
}
|
||||
|
||||
impl EwcPlusPlus {
|
||||
pub fn new(config: EwcConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
fisher_diag: None,
|
||||
optimal_params: None,
|
||||
task_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Consolidate current parameters after training
|
||||
pub fn consolidate(&mut self, params: &Array1<f32>, fisher: &Array1<f32>) {
|
||||
if self.config.online && self.fisher_diag.is_some() {
|
||||
// Online EWC: accumulate Fisher information
|
||||
let current_fisher = self.fisher_diag.as_ref().unwrap();
|
||||
self.fisher_diag =
|
||||
Some(current_fisher * self.config.decay + fisher * (1.0 - self.config.decay));
|
||||
} else {
|
||||
self.fisher_diag = Some(fisher.clone());
|
||||
}
|
||||
|
||||
self.optimal_params = Some(params.clone());
|
||||
self.task_count += 1;
|
||||
}
|
||||
|
||||
/// Compute EWC penalty for given parameters
|
||||
pub fn penalty(&self, params: &Array1<f32>) -> f32 {
|
||||
match (&self.fisher_diag, &self.optimal_params) {
|
||||
(Some(fisher), Some(optimal)) => {
|
||||
let diff = params - optimal;
|
||||
let weighted = &diff * &diff * fisher;
|
||||
0.5 * self.config.lambda * weighted.sum()
|
||||
}
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute gradient of EWC penalty
|
||||
pub fn penalty_gradient(&self, params: &Array1<f32>) -> Option<Array1<f32>> {
|
||||
match (&self.fisher_diag, &self.optimal_params) {
|
||||
(Some(fisher), Some(optimal)) => {
|
||||
let diff = params - optimal;
|
||||
Some(self.config.lambda * fisher * &diff)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute Fisher information from gradients
|
||||
pub fn compute_fisher(gradients: &[Array1<f32>]) -> Array1<f32> {
|
||||
if gradients.is_empty() {
|
||||
return Array1::zeros(0);
|
||||
}
|
||||
|
||||
let dim = gradients[0].len();
|
||||
let mut fisher = Array1::zeros(dim);
|
||||
|
||||
for grad in gradients {
|
||||
fisher = fisher + grad.mapv(|x| x * x);
|
||||
}
|
||||
|
||||
fisher / gradients.len() as f32
|
||||
}
|
||||
|
||||
pub fn has_prior(&self) -> bool {
|
||||
self.fisher_diag.is_some()
|
||||
}
|
||||
|
||||
pub fn task_count(&self) -> usize {
|
||||
self.task_count
|
||||
}
|
||||
}
|
||||
80
vendor/ruvector/crates/ruvector-dag/src/sona/micro_lora.rs
vendored
Normal file
80
vendor/ruvector/crates/ruvector-dag/src/sona/micro_lora.rs
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
//! MicroLoRA: Ultra-fast per-query adaptation
|
||||
|
||||
use ndarray::{Array1, Array2};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MicroLoRAConfig {
|
||||
pub rank: usize, // 1-2 for micro
|
||||
pub alpha: f32, // Scaling factor
|
||||
pub dropout: f32, // Dropout rate
|
||||
}
|
||||
|
||||
impl Default for MicroLoRAConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rank: 2,
|
||||
alpha: 1.0,
|
||||
dropout: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MicroLoRA {
|
||||
config: MicroLoRAConfig,
|
||||
a_matrix: Array2<f32>, // (in_dim, rank)
|
||||
b_matrix: Array2<f32>, // (rank, out_dim)
|
||||
#[allow(dead_code)]
|
||||
in_dim: usize,
|
||||
#[allow(dead_code)]
|
||||
out_dim: usize,
|
||||
}
|
||||
|
||||
impl MicroLoRA {
|
||||
pub fn new(config: MicroLoRAConfig, dim: usize) -> Self {
|
||||
let rank = config.rank;
|
||||
// Initialize A with small random values, B with zeros
|
||||
let a_matrix = Array2::from_shape_fn((dim, rank), |_| (rand::random::<f32>() - 0.5) * 0.01);
|
||||
let b_matrix = Array2::zeros((rank, dim));
|
||||
|
||||
Self {
|
||||
config,
|
||||
a_matrix,
|
||||
b_matrix,
|
||||
in_dim: dim,
|
||||
out_dim: dim,
|
||||
}
|
||||
}
|
||||
|
||||
/// Forward pass: x + alpha * (x @ A @ B)
|
||||
pub fn forward(&self, x: &Array1<f32>) -> Array1<f32> {
|
||||
let low_rank = x.dot(&self.a_matrix).dot(&self.b_matrix);
|
||||
x + &(low_rank * self.config.alpha)
|
||||
}
|
||||
|
||||
/// Adapt weights based on gradient signal
|
||||
pub fn adapt(&mut self, gradient: &Array1<f32>, learning_rate: f32) {
|
||||
// Update B matrix based on gradient (rank-1 update)
|
||||
// This is the "instant" adaptation - must be <100μs
|
||||
let grad_norm = gradient.mapv(|x| x * x).sum().sqrt();
|
||||
if grad_norm > 1e-8 {
|
||||
let normalized = gradient / grad_norm;
|
||||
// Outer product update to B
|
||||
for i in 0..self.config.rank {
|
||||
for j in 0..self.out_dim {
|
||||
self.b_matrix[[i, j]] +=
|
||||
learning_rate * self.a_matrix.column(i).sum() * normalized[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset to initial state
|
||||
pub fn reset(&mut self) {
|
||||
self.b_matrix.fill(0.0);
|
||||
}
|
||||
|
||||
/// Get parameter count
|
||||
pub fn param_count(&self) -> usize {
|
||||
self.a_matrix.len() + self.b_matrix.len()
|
||||
}
|
||||
}
|
||||
13
vendor/ruvector/crates/ruvector-dag/src/sona/mod.rs
vendored
Normal file
13
vendor/ruvector/crates/ruvector-dag/src/sona/mod.rs
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
//! SONA: Self-Optimizing Neural Architecture for DAG Learning
|
||||
|
||||
mod engine;
|
||||
mod ewc;
|
||||
mod micro_lora;
|
||||
mod reasoning_bank;
|
||||
mod trajectory;
|
||||
|
||||
pub use engine::DagSonaEngine;
|
||||
pub use ewc::{EwcConfig, EwcPlusPlus};
|
||||
pub use micro_lora::{MicroLoRA, MicroLoRAConfig};
|
||||
pub use reasoning_bank::{DagPattern, DagReasoningBank, ReasoningBankConfig};
|
||||
pub use trajectory::{DagTrajectory, DagTrajectoryBuffer};
|
||||
257
vendor/ruvector/crates/ruvector-dag/src/sona/reasoning_bank.rs
vendored
Normal file
257
vendor/ruvector/crates/ruvector-dag/src/sona/reasoning_bank.rs
vendored
Normal file
@@ -0,0 +1,257 @@
|
||||
//! Reasoning Bank: K-means++ clustering for pattern storage
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DagPattern {
|
||||
pub id: u64,
|
||||
pub vector: Vec<f32>,
|
||||
pub quality_score: f32,
|
||||
pub usage_count: usize,
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReasoningBankConfig {
|
||||
pub num_clusters: usize,
|
||||
pub pattern_dim: usize,
|
||||
pub max_patterns: usize,
|
||||
pub similarity_threshold: f32,
|
||||
}
|
||||
|
||||
impl Default for ReasoningBankConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
num_clusters: 100,
|
||||
pattern_dim: 256,
|
||||
max_patterns: 10000,
|
||||
similarity_threshold: 0.7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DagReasoningBank {
|
||||
config: ReasoningBankConfig,
|
||||
patterns: Vec<DagPattern>,
|
||||
centroids: Vec<Vec<f32>>,
|
||||
cluster_assignments: Vec<usize>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
impl DagReasoningBank {
|
||||
pub fn new(config: ReasoningBankConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
patterns: Vec::new(),
|
||||
centroids: Vec::new(),
|
||||
cluster_assignments: Vec::new(),
|
||||
next_id: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Store a new pattern
|
||||
pub fn store_pattern(&mut self, vector: Vec<f32>, quality: f32) -> u64 {
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
|
||||
let pattern = DagPattern {
|
||||
id,
|
||||
vector,
|
||||
quality_score: quality,
|
||||
usage_count: 0,
|
||||
metadata: HashMap::new(),
|
||||
};
|
||||
|
||||
self.patterns.push(pattern);
|
||||
|
||||
// Evict if over capacity
|
||||
if self.patterns.len() > self.config.max_patterns {
|
||||
self.evict_lowest_quality();
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Query similar patterns using cosine similarity
|
||||
pub fn query_similar(&self, query: &[f32], k: usize) -> Vec<(u64, f32)> {
|
||||
let mut similarities: Vec<(u64, f32)> = self
|
||||
.patterns
|
||||
.iter()
|
||||
.map(|p| (p.id, cosine_similarity(&p.vector, query)))
|
||||
.filter(|(_, sim)| *sim >= self.config.similarity_threshold)
|
||||
.collect();
|
||||
|
||||
similarities.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
|
||||
similarities.truncate(k);
|
||||
similarities
|
||||
}
|
||||
|
||||
/// Run K-means++ clustering
|
||||
pub fn recompute_clusters(&mut self) {
|
||||
if self.patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let k = self.config.num_clusters.min(self.patterns.len());
|
||||
|
||||
// K-means++ initialization
|
||||
self.centroids = kmeans_pp_init(&self.patterns, k);
|
||||
|
||||
// K-means iterations
|
||||
for _ in 0..10 {
|
||||
// Assign points to clusters
|
||||
self.cluster_assignments = self
|
||||
.patterns
|
||||
.iter()
|
||||
.map(|p| self.nearest_centroid(&p.vector))
|
||||
.collect();
|
||||
|
||||
// Update centroids
|
||||
self.update_centroids();
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_centroid(&self, point: &[f32]) -> usize {
|
||||
self.centroids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, c)| (i, euclidean_distance(point, c)))
|
||||
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn update_centroids(&mut self) {
|
||||
let k = self.centroids.len();
|
||||
let dim = if !self.centroids.is_empty() {
|
||||
self.centroids[0].len()
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Initialize new centroids
|
||||
let mut new_centroids = vec![vec![0.0; dim]; k];
|
||||
let mut counts = vec![0usize; k];
|
||||
|
||||
// Sum points in each cluster
|
||||
for (pattern, &cluster) in self.patterns.iter().zip(self.cluster_assignments.iter()) {
|
||||
if cluster < k {
|
||||
for (i, &val) in pattern.vector.iter().enumerate() {
|
||||
new_centroids[cluster][i] += val;
|
||||
}
|
||||
counts[cluster] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Average to get centroids
|
||||
for (centroid, count) in new_centroids.iter_mut().zip(counts.iter()) {
|
||||
if *count > 0 {
|
||||
for val in centroid.iter_mut() {
|
||||
*val /= *count as f32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.centroids = new_centroids;
|
||||
}
|
||||
|
||||
fn evict_lowest_quality(&mut self) {
|
||||
// Remove pattern with lowest quality * usage score
|
||||
if let Some(min_idx) = self
|
||||
.patterns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by(|(_, a), (_, b)| {
|
||||
let score_a = a.quality_score * (a.usage_count as f32 + 1.0).ln();
|
||||
let score_b = b.quality_score * (b.usage_count as f32 + 1.0).ln();
|
||||
score_a.partial_cmp(&score_b).unwrap()
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
{
|
||||
self.patterns.remove(min_idx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pattern_count(&self) -> usize {
|
||||
self.patterns.len()
|
||||
}
|
||||
|
||||
pub fn cluster_count(&self) -> usize {
|
||||
self.centroids.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
|
||||
let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
|
||||
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if norm_a > 0.0 && norm_b > 0.0 {
|
||||
dot / (norm_a * norm_b)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
|
||||
a.iter()
|
||||
.zip(b.iter())
|
||||
.map(|(x, y)| (x - y).powi(2))
|
||||
.sum::<f32>()
|
||||
.sqrt()
|
||||
}
|
||||
|
||||
fn kmeans_pp_init(patterns: &[DagPattern], k: usize) -> Vec<Vec<f32>> {
|
||||
use rand::Rng;
|
||||
|
||||
if patterns.is_empty() || k == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let mut centroids = Vec::with_capacity(k);
|
||||
let _dim = patterns[0].vector.len();
|
||||
|
||||
// Choose first centroid randomly
|
||||
let first_idx = rng.gen_range(0..patterns.len());
|
||||
centroids.push(patterns[first_idx].vector.clone());
|
||||
|
||||
// Choose remaining centroids using D^2 weighting
|
||||
for _ in 1..k {
|
||||
let mut distances = Vec::with_capacity(patterns.len());
|
||||
let mut total_distance = 0.0f32;
|
||||
|
||||
// Compute minimum distance to existing centroids for each point
|
||||
for pattern in patterns {
|
||||
let min_dist = centroids
|
||||
.iter()
|
||||
.map(|c| euclidean_distance(&pattern.vector, c))
|
||||
.min_by(|a, b| a.partial_cmp(b).unwrap())
|
||||
.unwrap_or(0.0);
|
||||
let squared = min_dist * min_dist;
|
||||
distances.push(squared);
|
||||
total_distance += squared;
|
||||
}
|
||||
|
||||
// Select next centroid with probability proportional to D^2
|
||||
if total_distance > 0.0 {
|
||||
let mut threshold = rng.gen::<f32>() * total_distance;
|
||||
for (idx, &dist) in distances.iter().enumerate() {
|
||||
threshold -= dist;
|
||||
if threshold <= 0.0 {
|
||||
centroids.push(patterns[idx].vector.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: choose random point
|
||||
let idx = rng.gen_range(0..patterns.len());
|
||||
centroids.push(patterns[idx].vector.clone());
|
||||
}
|
||||
|
||||
if centroids.len() >= k {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
centroids
|
||||
}
|
||||
97
vendor/ruvector/crates/ruvector-dag/src/sona/trajectory.rs
vendored
Normal file
97
vendor/ruvector/crates/ruvector-dag/src/sona/trajectory.rs
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
//! Trajectory Buffer: Lock-free buffer for learning trajectories
|
||||
|
||||
use crossbeam::queue::ArrayQueue;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
/// A single learning trajectory
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DagTrajectory {
|
||||
pub query_hash: u64,
|
||||
pub dag_embedding: Vec<f32>,
|
||||
pub attention_mechanism: String,
|
||||
pub execution_time_ms: f64,
|
||||
pub improvement_ratio: f32,
|
||||
pub timestamp: std::time::Instant,
|
||||
}
|
||||
|
||||
impl DagTrajectory {
|
||||
pub fn new(
|
||||
query_hash: u64,
|
||||
dag_embedding: Vec<f32>,
|
||||
attention_mechanism: String,
|
||||
execution_time_ms: f64,
|
||||
baseline_time_ms: f64,
|
||||
) -> Self {
|
||||
let improvement_ratio = if baseline_time_ms > 0.0 {
|
||||
(baseline_time_ms - execution_time_ms) as f32 / baseline_time_ms as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
Self {
|
||||
query_hash,
|
||||
dag_embedding,
|
||||
attention_mechanism,
|
||||
execution_time_ms,
|
||||
improvement_ratio,
|
||||
timestamp: std::time::Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute quality score (0-1)
|
||||
pub fn quality(&self) -> f32 {
|
||||
// Quality based on improvement and execution time
|
||||
let time_score = 1.0 / (1.0 + self.execution_time_ms as f32 / 1000.0);
|
||||
let improvement_score = (self.improvement_ratio + 1.0) / 2.0;
|
||||
0.5 * time_score + 0.5 * improvement_score
|
||||
}
|
||||
}
|
||||
|
||||
/// Lock-free trajectory buffer
|
||||
pub struct DagTrajectoryBuffer {
|
||||
queue: ArrayQueue<DagTrajectory>,
|
||||
count: AtomicUsize,
|
||||
#[allow(dead_code)]
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl DagTrajectoryBuffer {
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
Self {
|
||||
queue: ArrayQueue::new(capacity),
|
||||
count: AtomicUsize::new(0),
|
||||
capacity,
|
||||
}
|
||||
}
|
||||
|
||||
/// Push trajectory, dropping oldest if full
|
||||
pub fn push(&self, trajectory: DagTrajectory) {
|
||||
if self.queue.push(trajectory.clone()).is_err() {
|
||||
// Queue full, pop oldest and retry
|
||||
let _ = self.queue.pop();
|
||||
let _ = self.queue.push(trajectory);
|
||||
}
|
||||
self.count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Drain all trajectories for processing
|
||||
pub fn drain(&self) -> Vec<DagTrajectory> {
|
||||
let mut result = Vec::with_capacity(self.queue.len());
|
||||
while let Some(t) = self.queue.pop() {
|
||||
result.push(t);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.queue.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.queue.is_empty()
|
||||
}
|
||||
|
||||
pub fn total_count(&self) -> usize {
|
||||
self.count.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user