ruvector/crates/ruvector-tiny-dancer-core
rUv e2439ff62f
feat(timesfm): TimesFM 1.0 200M decoder-only inference port to candle (#603)
* 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 578400d1d,
2026-06-21) but the crate was never a workspace member, so the shard aborted
with 'package ID ruvector-maxsim did not match any packages' before reaching
the timesfm candle test step in the same shard. Add the crate to workspace
members so the shard resolves and timesfm tests actually run.

The crate's self-imposed #![warn(missing_docs)] plus an unused param and a dead
ground_truth() helper would otherwise fail the workspace 'Clippy (deny warnings)'
job once it's a member, so: document the public error/types fields, underscore
the unused gen_corpus dims param, and drop the dead ground_truth() (main builds
ground truth inline). cargo clippy -p ruvector-maxsim --all-targets -- -D warnings
is clean; 19 tests pass.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(clippy): clear pre-existing workspace clippy + fmt debt under -D warnings

The timesfm candle compile error was masking the rest of the workspace from
'Clippy (deny warnings)' (cargo clippy --workspace --all-targets -- -D warnings);
once timesfm/maxsim compile, these pre-existing lints (also red on main) surface.
All trivial, no behavior change:

- proof-gate: needless &seq.to_le_bytes() borrows (hash bytes identical via
  AsRef), allow items_after_test_module, allow dead queries field in example
- photonlayer-wasm: swap approx-PI 3.14 test literal for 2.5 (arbitrary fill)
- coherence-hnsw / gnn example: allow(needless_range_loop) where index is reused
- gnn / hnsw-repair: allow(too_many_arguments) on bench fns; sort_by->sort_by_key;
  &mut Vec -> &mut [_]
- graph bench: drop black_box around unit validate_node().unwrap()
- sota-bench: drop unused imports, .max().min()->.clamp(), remove redundant parens
- maxsim: rustfmt + Cargo.lock sync (now a workspace member)

cargo clippy --workspace --all-targets --no-deps -- -D warnings: clean (exit 0)
cargo fmt --all -- --check: clean (exit 0)

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(deny): ignore RUSTSEC-2026-0186 (memmap2 unsound, transitive)

cargo-deny's advisories check fails on RUSTSEC-2026-0186 — an 'unsound'
(not exploitable) Unchecked-pointer-offset advisory against memmap2 0.9.x,
pulled transitively via safetensors/candle mmap loading and other crates.
No fixed 0.9 release exists yet and we don't pass attacker-controlled offsets
to memmap2. Add it to the justified ignore list (re-review 2026-08-01),
matching the existing deny.toml pattern. 'cargo deny check advisories' is now
clean locally.

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: ruvnet <ruvnet@gmail.com>
2026-06-25 13:52:42 -04:00
..
benches chore(workspace): clippy-clean every crate under -D warnings + fmt + repair pre-existing broken benches 2026-04-25 17:00:20 -04:00
docs fix: Fix case sensitivity bug preventing native module from loading 2025-11-21 21:34:52 +00:00
examples feat(timesfm): TimesFM 1.0 200M decoder-only inference port to candle (#603) 2026-06-25 13:52:42 -04:00
src feat(timesfm): TimesFM 1.0 200M decoder-only inference port to candle (#603) 2026-06-25 13:52:42 -04:00
Cargo.toml feat(tiny-dancer): real FastGRNN training pipeline (ADR-252) 2026-06-15 10:50:14 -04:00
README.md fix: Fix case sensitivity bug preventing native module from loading 2025-11-21 21:34:52 +00:00

Ruvector Tiny Dancer Core

Crates.io Documentation License: MIT Build Status Rust Version

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:

  1. You send a request with potential responses (candidates)
  2. Tiny Dancer scores each candidate in microseconds
  3. High-confidence candidates go to lightweight models (fast & cheap)
  4. 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

📚 Resources

🤝 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