* 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
|
||
|---|---|---|
| .. | ||
| benches | ||
| docs | ||
| examples | ||
| src | ||
| Cargo.toml | ||
| README.md | ||
Ruvector Tiny Dancer Core
Production-grade AI agent routing system with FastGRNN neural inference for 70-85% LLM cost reduction.
🚀 Introduction
The Problem: AI applications often send every request to expensive, powerful models, even when simpler models could handle the task. This wastes money and resources.
The Solution: Tiny Dancer acts as a smart traffic controller for your AI requests. It quickly analyzes each request and decides whether to route it to a fast, cheap model or a powerful, expensive one.
How It Works:
- You send a request with potential responses (candidates)
- Tiny Dancer scores each candidate in microseconds
- High-confidence candidates go to lightweight models (fast & cheap)
- Low-confidence candidates go to powerful models (accurate but expensive)
The Result: Save 70-85% on AI costs while maintaining quality.
Real-World Example: Instead of sending 100 memory items to GPT-4 for evaluation, Tiny Dancer filters them down to the top 3-5 in microseconds, then sends only those to the expensive model.
✨ Features
- ⚡ Sub-millisecond Latency: 144ns feature extraction, 7.5µs model inference
- 💰 70-85% Cost Reduction: Intelligent routing to appropriately-sized models
- 🧠 FastGRNN Architecture: <1MB models with 80-90% sparsity
- 🔒 Circuit Breaker: Graceful degradation with automatic recovery
- 📊 Uncertainty Quantification: Conformal prediction for reliable routing
- 🗄️ AgentDB Integration: Persistent SQLite storage with WAL mode
- 🎯 Multi-Signal Scoring: Semantic similarity, recency, frequency, success rate
- 🔧 Model Optimization: INT8 quantization, magnitude pruning
📊 Benchmark Results
Feature Extraction:
10 candidates: 1.73µs (173ns per candidate)
50 candidates: 9.44µs (189ns per candidate)
100 candidates: 18.48µs (185ns per candidate)
Model Inference:
Single: 7.50µs
Batch 10: 74.94µs (7.49µs per item)
Batch 100: 735.45µs (7.35µs per item)
Complete Routing:
10 candidates: 8.83µs
50 candidates: 48.23µs
100 candidates: 92.86µs
🚀 Quick Start
Installation
Add to your Cargo.toml:
[dependencies]
ruvector-tiny-dancer-core = "0.1.1"
Basic Usage
use ruvector_tiny_dancer_core::{
Router,
types::{RouterConfig, RoutingRequest, Candidate},
};
use std::collections::HashMap;
// Create router
let config = RouterConfig {
model_path: "./models/fastgrnn.safetensors".to_string(),
confidence_threshold: 0.85,
max_uncertainty: 0.15,
enable_circuit_breaker: true,
..Default::default()
};
let router = Router::new(config)?;
// Prepare candidates
let candidates = vec![
Candidate {
id: "candidate-1".to_string(),
embedding: vec![0.5; 384],
metadata: HashMap::new(),
created_at: chrono::Utc::now().timestamp(),
access_count: 10,
success_rate: 0.95,
},
];
// Route request
let request = RoutingRequest {
query_embedding: vec![0.5; 384],
candidates,
metadata: None,
};
let response = router.route(request)?;
// Process decisions
for decision in response.decisions {
println!("Candidate: {}", decision.candidate_id);
println!("Confidence: {:.2}", decision.confidence);
println!("Use lightweight: {}", decision.use_lightweight);
println!("Inference time: {}µs", response.inference_time_us);
}
📚 Tutorials
Tutorial 1: Basic Routing
use ruvector_tiny_dancer_core::{Router, types::*};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create default router
let router = Router::default()?;
// Create a simple request
let request = RoutingRequest {
query_embedding: vec![0.9; 384],
candidates: vec![
Candidate {
id: "high-quality".to_string(),
embedding: vec![0.85; 384],
metadata: Default::default(),
created_at: chrono::Utc::now().timestamp(),
access_count: 100,
success_rate: 0.98,
}
],
metadata: None,
};
// Route and inspect results
let response = router.route(request)?;
let decision = &response.decisions[0];
if decision.use_lightweight {
println!("✅ High confidence - route to lightweight model");
} else {
println!("⚠️ Low confidence - route to powerful model");
}
Ok(())
}
Tutorial 2: Feature Engineering
use ruvector_tiny_dancer_core::feature_engineering::{FeatureEngineer, FeatureConfig};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Custom feature weights
let config = FeatureConfig {
similarity_weight: 0.5, // Prioritize semantic similarity
recency_weight: 0.3, // Recent items are important
frequency_weight: 0.1,
success_weight: 0.05,
metadata_weight: 0.05,
recency_decay: 0.001,
};
let engineer = FeatureEngineer::with_config(config);
// Extract features
let query = vec![0.5; 384];
let candidate = Candidate { /* ... */ };
let features = engineer.extract_features(&query, &candidate, None)?;
println!("Semantic similarity: {:.4}", features.semantic_similarity);
println!("Recency score: {:.4}", features.recency_score);
println!("Combined score: {:.4}",
features.features.iter().sum::<f32>());
Ok(())
}
Tutorial 3: Circuit Breaker
use ruvector_tiny_dancer_core::Router;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let router = Router::default()?;
// Check circuit breaker status
match router.circuit_breaker_status() {
Some(true) => {
println!("✅ Circuit closed - system healthy");
// Normal routing
}
Some(false) => {
println!("⚠️ Circuit open - using fallback");
// Route to default powerful model
}
None => {
println!("Circuit breaker disabled");
}
}
Ok(())
}
Tutorial 4: Model Optimization
use ruvector_tiny_dancer_core::model::{FastGRNN, FastGRNNConfig};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create model
let config = FastGRNNConfig {
input_dim: 5,
hidden_dim: 8,
output_dim: 1,
..Default::default()
};
let mut model = FastGRNN::new(config)?;
println!("Original size: {} bytes", model.size_bytes());
// Apply quantization
model.quantize()?;
println!("After quantization: {} bytes", model.size_bytes());
// Apply pruning
model.prune(0.9)?; // 90% sparsity
println!("After pruning: {} bytes", model.size_bytes());
Ok(())
}
Tutorial 5: SQLite Storage
use ruvector_tiny_dancer_core::storage::Storage;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create storage
let storage = Storage::new("./routing.db")?;
// Insert candidate
let candidate = Candidate { /* ... */ };
storage.insert_candidate(&candidate)?;
// Query candidates
let candidates = storage.query_candidates(50)?;
println!("Retrieved {} candidates", candidates.len());
// Record routing
storage.record_routing(
"candidate-1",
&vec![0.5; 384],
0.92, // confidence
true, // use_lightweight
0.08, // uncertainty
8_500, // inference_time_us
)?;
// Get statistics
let stats = storage.get_statistics()?;
println!("Total routes: {}", stats.total_routes);
println!("Lightweight: {}", stats.lightweight_routes);
println!("Avg inference: {:.2}µs", stats.avg_inference_time_us);
Ok(())
}
🎯 Advanced Usage
Hot Model Reloading
// Reload model without downtime
router.reload_model()?;
Custom Configuration
let config = RouterConfig {
model_path: "./models/custom.safetensors".to_string(),
confidence_threshold: 0.90, // Higher threshold
max_uncertainty: 0.10, // Lower tolerance
enable_circuit_breaker: true,
circuit_breaker_threshold: 3, // Faster circuit opening
enable_quantization: true,
database_path: Some("./data/routing.db".to_string()),
};
Batch Processing
let inputs = vec![
vec![0.5; 5],
vec![0.3; 5],
vec![0.8; 5],
];
let scores = model.forward_batch(&inputs)?;
// Process 3 inputs in ~22µs total
📈 Performance Optimization
SIMD Acceleration
Feature extraction uses simsimd for hardware-accelerated similarity:
- Cosine similarity: 144ns (384-dim vectors)
- Batch processing: Linear scaling with candidate count
Zero-Copy Operations
- Memory-mapped models with
memmap2 - Zero-allocation inference paths
- Efficient buffer reuse
Parallel Processing
- Rayon-based parallel feature extraction
- Batch inference for multiple candidates
- Concurrent storage operations with WAL
🔧 Configuration
| Parameter | Default | Description |
|---|---|---|
confidence_threshold |
0.85 | Minimum confidence for lightweight routing |
max_uncertainty |
0.15 | Maximum uncertainty tolerance |
circuit_breaker_threshold |
5 | Failures before circuit opens |
recency_decay |
0.001 | Exponential decay rate for recency |
📊 Cost Analysis
For 10,000 daily queries at $0.02 per query:
| Scenario | Reduction | Daily Savings | Annual Savings |
|---|---|---|---|
| Conservative | 70% | $132 | $48,240 |
| Aggressive | 85% | $164 | $59,876 |
Break-even: ~2 months with typical engineering costs
🔗 Related Projects
- WASM: ruvector-tiny-dancer-wasm - Browser/edge deployment
- Node.js: ruvector-tiny-dancer-node - TypeScript bindings
- Ruvector: ruvector-core - Vector database
📚 Resources
- Documentation: docs.rs/ruvector-tiny-dancer-core
- GitHub: github.com/ruvnet/ruvector
- Website: ruv.io
- Examples: github.com/ruvnet/ruvector/tree/main/examples
🤝 Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
📄 License
MIT License - see LICENSE for details.
🙏 Acknowledgments
- FastGRNN architecture inspired by Microsoft Research
- RouteLLM for routing methodology
- Cloudflare Workers for WASM deployment patterns
Built with ❤️ by the Ruvector Team