mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-07-09 17:28:42 +00:00
* feat(timesfm): TimesFM 1.0 200M decoder-only inference port to candle
Native Rust/candle port of google-research/timesfm (pytorch_patched_decoder.py)
for temporal embeddings + zero-shot forecasting inside RuVector. Behind an opt-in
`candle` feature (default = [], cpu-fallback pattern like ruvector-hailo); no
lockfile churn (candle 0.9.2 already pinned by ruvllm).
- config.rs: TimesfmConfig (1280 dim, 20 layers, 16 heads, 80 head_dim, patch 32/128)
- model.rs: ResidualBlock patch embedding, sinusoidal pos-emb (no RoPE), 20x decoder
(fused qkv, learnable per-head-dim softplus scaling, causal+padding mask), RevIN
instance norm, forward [B,N,128,10] + autoregressive decode to arbitrary horizon
- scripts/convert_weights.py: HF safetensors → VarBuilder key remap (--dry-run)
- 12 tests (shape + RevIN numerical regression); clippy -D warnings clean
Adversarial review caught + fixed a real RevIN bug (masked_mean_std did a global
mean/std instead of the reference's first-qualifying-patch selection) + added
regression tests. Honest scope: dimensionally + structurally faithful, but real
numerical weight-parity vs the published safetensors is NOT yet verified (tests
run on dummy weights). Open low-impact faithfulness deviations documented in code.
Co-Authored-By: claude-flow <ruv@ruv.net>
* style(timesfm): rustfmt the crate (format the RevIN-fix edits) — green the Rustfmt gate for this crate
Our crate is now fmt-clean + clippy-clean; the remaining workspace-wide fmt
diffs are pre-existing in other crates, out of scope for this PR.
Co-Authored-By: claude-flow <ruv@ruv.net>
* feat(timesfm): weight-parity validated against official PyTorch reference
Drives the candle TimesFM 1.0 200M port from "compiles on dummy weights" to
a real numerical PASS against google/timesfm-1.0-200m.
Measured (f32 CPU, deterministic 512-pt series, horizon 128):
max-abs-diff = 8.58e-6 MAE = 3.25e-6 rel-error = 5.83e-7
(target was <1e-2; we hit the f32 accumulation floor ~1e-5.)
Bridge: the real torch_model.ckpt state_dict (253 keys) maps 1:1 through
scripts/convert_weights.py with zero unmapped/missing keys.
Bug found + fixed (src/model.rs build_mask): the attention mask used
f32::NEG_INFINITY for masked positions. With real 0/1 paddings the padding
term `padding * -inf` computes `0 * -inf = NaN`, poisoning the whole mask
so softmax emitted NaN for every row (every forecast value was NaN). The
old `nan_to_zero` guard silently failed (where_cond dtype mismatch -> fallback
`NaN * 1 = NaN`). Replaced with the reference's large *finite* negative
(-0.7 * f32::MAX) and element-wise `minimum` merge, exactly matching
convert_paddings_to_mask + causal_mask + merge_masks. No NaN, exact parity.
Added:
- examples/parity.rs end-to-end parity runner with metrics + verdict
- tests/parity.rs gated integration test (skips cleanly w/o the
814MB artifacts; never fabricates a pass)
- scripts/gen_reference.py reference forecast generator (official decoder)
Co-Authored-By: claude-flow <ruv@ruv.net>
* bench(timesfm): forward-only latency bench — 45ms/forecast (200M, ctx512/h128, warm CPU); parity validated 8.58e-6
* feat(timesfm): predictive-pruning module for Darwin (ADR-191 §2)
Add crates/timesfm/src/prune.rs: forecast an optimization curve's plateau
from its first K points with TimesFM and decide PRUNE vs CONTINUE against a
viability threshold (lower=better, like exploitability). Decoupled — operates
on a generic Vec<f32>, no cross-repo poker-darwin dep.
- decide_prune(): forecast tail to target horizon, plateau = mean of last
horizon/4 steps; PRUNE iff plateau > threshold. Guards: non-finite forecast
=> CONTINUE conf 0 (never kill on a broken forecast); already-viable
(best_so_far <= threshold) => CONTINUE. Scale-invariant confidence.
- examples/predictive_prune.rs + tests/prune.rs: two synthetic curves with
REAL weights — doomed (floor 0.20) => PRUNE (forecast plateau 1.98, conf
0.72); healthy (already below 0.05) => CONTINUE. Both decisions correct.
Skips cleanly when weights absent (no fabricated pass).
- Honest calibration note: TimesFM mean-reverts upward on short synthetic
decays so absolute plateau is biased high; decision rides the robust
relative-ordering + already-viable signals, not absolute calibration.
- Doc-comment shows how poker-darwin calls this on its champion curve.
Tests: 12 shape + parity + prune = 14/14 green (candle); light build green.
Co-Authored-By: claude-flow <ruv@ruv.net>
* test(timesfm): bench24 harness for GCP 24-case deployment test (ADR-191 Phase B)
24 distinct forecast cases (varied period/trend/amp/noise/freq_id; ctx=512,
horizon=128) on real weights. Per-case latency + finiteness assert, aggregate
mean/p50/p95/p99, throughput, peak RSS, machine-readable JSON line. Non-finite
output is a hard FAIL (exit 1), never a silent pass.
Local baseline (ruvultra, 32-thread CPU): 24/24 finite, mean 42.5ms p95 44.2ms,
throughput 23.5 fps, peak RSS 1.55GB.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci) + feat(timesfm): README, publish=true, research-nightly shard, rustfmt
CI fixes:
- timesfm added to research-nightly shard (-p timesfm)
- timesfm excluded from core-and-rest shard (--exclude timesfm)
- cargo fmt -p timesfm: model.rs + 4 example files formatted
- cargo fmt -p ruvector-graph: typed_graph_bench.rs + 4 src files
(pre-existing rustfmt failure blocking the PR)
crates/timesfm/README.md (new):
- Architecture diagram (ResidualBlock → 20× decoder → RevIN → output)
- Feature flags table (candle/cuda/metal/hub)
- Quick-start: inference + weight loading workflow
- Known limitations section (weight parity, MLP mask, pos-emb shift)
- References (ICML 2024 paper, HuggingFace model card)
crates/timesfm/Cargo.toml:
- publish = true (was false)
- readme = "README.md"
Co-Authored-By: claude-flow <ruv@ruv.net>
* chore: cargo fmt ruvector-proof-gate (pre-existing rustfmt CI blocker)
Co-Authored-By: claude-flow <ruv@ruv.net>
* chore: cargo fmt temporal-coherence + tiny-dancer-core (pre-existing)
Co-Authored-By: claude-flow <ruv@ruv.net>
* chore: cargo fmt tiny-dancer-node + ruvllm openmythos (pre-existing)
Co-Authored-By: claude-flow <ruv@ruv.net>
* chore: cargo fmt rvf-runtime/store.rs (pre-existing)
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ci): timesfm tests run with --features candle in research-nightly
The research-nightly shard was running timesfm without --features candle,
causing a compile error (all model code is behind the feature gate).
Fix: remove timesfm from the shared nextest run; add a dedicated step
that runs only timesfm tests with --features candle.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ruvllm): remove broken private-item doc link (DepthLora)
Code Quality CI was failing: public doc in mod.rs linked to private
recurrent::DepthLora. Replace with plain backtick name.
Pre-existing issue surfaced by rustfmt touching the file.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ruvllm): fix all private-item rustdoc links in openmythos/mod.rs
Three doc comments linked to private items (LtiInjection, RecurrentBlock,
DepthLora) in the recurrent module. rustdoc's -D warnings caught them.
Replaced with plain-text names. Pre-existing, surfaced by rustfmt touching
the file.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(ruvllm): fix private attention module doc link
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(timesfm): gate bench/bench24 examples behind candle feature
The bench and bench24 examples import candle_core/candle_nn/timesfm::model
unconditionally, breaking Clippy and stock workspace builds that run without
--features candle. Add [[example]] required-features = ["candle"] so they are
skipped when the feature is off, matching parity/predictive_prune which already
self-gate via #[cfg(feature = "candle")].
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(maxsim): add ruvector-maxsim to workspace + make clippy-clean
The research-nightly CI shard referenced -p ruvector-maxsim (added
|
||
|---|---|---|
| .. | ||
| examples | ||
| src | ||
| tests | ||
| Cargo.toml | ||
| README.md | ||
Ruvector GNN
A Graph Neural Network layer that makes HNSW vector search get smarter over time.
Most vector indexes return the same results every time you search. ruvector-gnn adds a GNN layer on top of HNSW that learns from your query patterns -- so search results actually improve with use. It runs message passing directly on the HNSW graph structure with SIMD acceleration, keeping latency low even on large indexes. Part of the RuVector ecosystem.
| ruvector-gnn | Standard HNSW Search | |
|---|---|---|
| Search quality | GNN re-ranks neighbors using learned attention weights -- results improve over time | Static ranking -- same results every time |
| Graph awareness | Operates directly on HNSW topology; understands graph structure | Treats index as a flat lookup table |
| Attention mechanisms | Multi-head GAT weighs which neighbors matter for each query | No attention -- all neighbors weighted equally |
| Inductive learning | GraphSAGE generalizes to unseen nodes without retraining | Cannot learn from new data |
| Hardware acceleration | SIMD-optimized aggregation; memory-mapped weights for large models | Basic distance calculations only |
| Deployment | Native Rust, Node.js (NAPI-RS), and WASM from the same crate | Typically single-platform |
Installation
Add ruvector-gnn to your Cargo.toml:
[dependencies]
ruvector-gnn = "0.1.1"
Feature Flags
[dependencies]
# Default with SIMD and memory mapping
ruvector-gnn = { version = "0.1.1", features = ["simd", "mmap"] }
# WASM-compatible build
ruvector-gnn = { version = "0.1.1", default-features = false, features = ["wasm"] }
# Node.js bindings
ruvector-gnn = { version = "0.1.1", features = ["napi"] }
Available features:
simd(default): SIMD-optimized operationsmmap(default): Memory-mapped weight storagewasm: WebAssembly-compatible buildnapi: Node.js bindings via NAPI-RS
Key Features
| Feature | What It Does | Why It Matters |
|---|---|---|
| GCN Layers | Graph Convolutional Network forward pass over HNSW neighbors | Learns structural patterns in your data without manual feature engineering |
| GAT Layers | Multi-head Graph Attention with interpretable weights | Automatically discovers which neighbors are most relevant per query |
| GraphSAGE | Inductive learning with neighbor sampling | Handles new, unseen nodes without retraining the full model |
| SIMD Aggregation | Hardware-accelerated message passing | Keeps GNN overhead under 15 ms for 100K-node graphs |
| Memory Mapping | Large model weights loaded via mmap | Run models bigger than RAM; only pages what's needed |
| INT8/FP16 Quantization | Compressed weight storage | 2-4x smaller models with minimal accuracy loss |
| Custom Aggregators | Mean, max, and LSTM aggregation modes | Tune the aggregation strategy to your data distribution |
| Skip Connections | Residual connections for deep GNN stacks | Train deeper networks without vanishing gradients |
| Batch Processing | Parallel message passing with Rayon | Saturates all cores during training and inference |
| Layer Normalization | Normalize activations between layers | Stable training dynamics across different graph sizes |
Quick Start
Basic GCN Layer
use ruvector_gnn::{GCNLayer, GNNConfig, MessagePassing};
use ndarray::Array2;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Configure GCN layer
let config = GNNConfig {
input_dim: 128,
output_dim: 64,
hidden_dim: 128,
num_heads: 4, // For GAT
dropout: 0.1,
activation: Activation::ReLU,
};
// Create GCN layer
let gcn = GCNLayer::new(config)?;
// Node features (num_nodes x input_dim)
let features = Array2::zeros((1000, 128));
// Adjacency list (HNSW neighbors)
let adjacency: Vec<Vec<usize>> = /* from HNSW index */;
// Forward pass
let output = gcn.forward(&features, &adjacency)?;
println!("Output shape: {:?}", output.shape());
Ok(())
}
Graph Attention Network
use ruvector_gnn::{GATLayer, AttentionConfig};
// Configure multi-head attention
let config = AttentionConfig {
input_dim: 128,
output_dim: 64,
num_heads: 8,
concat_heads: true,
dropout: 0.1,
leaky_relu_slope: 0.2,
};
let gat = GATLayer::new(config)?;
// Forward with attention
let (output, attention_weights) = gat.forward_with_attention(&features, &adjacency)?;
// Attention weights for interpretability
for (node_id, weights) in attention_weights.iter().enumerate() {
println!("Node {}: attention weights = {:?}", node_id, weights);
}
GraphSAGE with Custom Aggregator
use ruvector_gnn::{GraphSAGE, SAGEConfig, Aggregator};
let config = SAGEConfig {
input_dim: 128,
output_dim: 64,
num_layers: 2,
aggregator: Aggregator::Mean,
sample_sizes: vec![10, 5], // Neighbor sampling per layer
normalize: true,
};
let sage = GraphSAGE::new(config)?;
// Mini-batch training with neighbor sampling
let embeddings = sage.forward_minibatch(
&features,
&adjacency,
&batch_nodes, // Target nodes
)?;
Integration with Ruvector Core
use ruvector_core::VectorDB;
use ruvector_gnn::{HNSWMessagePassing, GNNEmbedder};
// Load vector database
let db = VectorDB::open("vectors.db")?;
// Create GNN that operates on HNSW structure
let gnn = GNNEmbedder::new(GNNConfig {
input_dim: db.dimensions(),
output_dim: 64,
num_layers: 2,
..Default::default()
})?;
// Get HNSW neighbors for message passing
let hnsw_graph = db.get_hnsw_graph()?;
// Compute GNN embeddings
let gnn_embeddings = gnn.encode(&db.get_all_vectors()?, &hnsw_graph)?;
// Enhanced search using GNN embeddings
let results = db.search_with_gnn(&query_vector, &gnn, 10)?;
API Overview
Core Types
// GNN layer configuration
pub struct GNNConfig {
pub input_dim: usize,
pub output_dim: usize,
pub hidden_dim: usize,
pub num_heads: usize,
pub dropout: f32,
pub activation: Activation,
}
// Message passing interface
pub trait MessagePassing {
fn aggregate(&self, features: &Array2<f32>, neighbors: &[Vec<usize>]) -> Array2<f32>;
fn update(&self, aggregated: &Array2<f32>, self_features: &Array2<f32>) -> Array2<f32>;
fn forward(&self, features: &Array2<f32>, adjacency: &[Vec<usize>]) -> Result<Array2<f32>>;
}
// Layer types
pub struct GCNLayer { /* ... */ }
pub struct GATLayer { /* ... */ }
pub struct GraphSAGE { /* ... */ }
Layer Operations
impl GCNLayer {
pub fn new(config: GNNConfig) -> Result<Self>;
pub fn forward(&self, x: &Array2<f32>, adj: &[Vec<usize>]) -> Result<Array2<f32>>;
pub fn save_weights(&self, path: &str) -> Result<()>;
pub fn load_weights(&mut self, path: &str) -> Result<()>;
}
impl GATLayer {
pub fn new(config: AttentionConfig) -> Result<Self>;
pub fn forward(&self, x: &Array2<f32>, adj: &[Vec<usize>]) -> Result<Array2<f32>>;
pub fn forward_with_attention(&self, x: &Array2<f32>, adj: &[Vec<usize>])
-> Result<(Array2<f32>, Vec<Vec<f32>>)>;
}
Performance
Benchmarks (100K Nodes, Avg Degree 16)
Operation Latency (p50) GFLOPS
-----------------------------------------------------
GCN forward (1 layer) ~15ms 12.5
GAT forward (8 heads) ~45ms 8.2
GraphSAGE (2 layers) ~25ms 10.1
Message aggregation ~5ms 25.0
Memory Usage
Model Size Peak Memory
---------------------------------------
128 -> 64 (1 layer) ~50MB
128 -> 64 (4 layers) ~150MB
With mmap weights ~10MB (+ disk)
Related Crates
- ruvector-core - Core vector database engine
- ruvector-gnn-node - Node.js bindings
- ruvector-gnn-wasm - WebAssembly bindings
- ruvector-graph - Graph database engine
Documentation
- Main README - Complete project overview
- API Documentation - Full API reference
- GitHub Repository - Source code
License
MIT License - see LICENSE for details.