Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"sessionId":"1028ef57-d609-4db3-a666-ea135a27e8b4","pid":9771,"acquiredAt":1776117015934}
{"sessionId":"bbf21ebf-4f6e-435a-8831-d2ec21ca8842","pid":1884462,"procStart":"22289809","acquiredAt":1778159033500}
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ bench_data/
acceleras.log
hailo_sdk.client.log

# Python virtual environments (Hailo SDK venvs, DFC venvs, etc.)
venv-hailo/
venv-hailo-dfc/
venv*/
.venv*/

# Large zip/archive artifacts
docs/ruvllm/*.zip

# Iter 228 — per-crate Cargo.lock files for the hailo workspace members
# (post iter-219 workspace rejoin). The parent workspace's Cargo.lock
# is canonical; cargo regenerates these locally as a side effect of
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ exclude = ["crates/micro-hnsw-wasm", "crates/ruvector-hyperbolic-hnsw", "crates/
# land in iters 92-97.
"crates/ruos-thermal"]
members = [
"crates/ruvector-symphonyqg",
"crates/ruvector-acorn",
"crates/ruvector-acorn-wasm",
"crates/ruvector-rabitq",
Expand Down
4 changes: 1 addition & 3 deletions crates/ruvector-graph/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,7 @@ impl GraphDB {

/// Delete all hyperedges that contain a given node
pub fn delete_hyperedges_by_node(&self, node_id: &NodeId) -> Result<usize> {
let ids: Vec<HyperedgeId> = self
.hyperedge_node_index
.get_hyperedges_by_node(node_id);
let ids: Vec<HyperedgeId> = self.hyperedge_node_index.get_hyperedges_by_node(node_id);
let mut deleted = 0;
for id in &ids {
if self.delete_hyperedge(id)? {
Expand Down
1 change: 0 additions & 1 deletion crates/ruvector-hailo/src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
//! configured network group. Configuration changes still need external
//! serialisation; we provide that via `Mutex` higher up in `lib.rs`.


use crate::error::HailoError;
#[cfg(feature = "hailo")]
use std::ptr;
Expand Down
4 changes: 1 addition & 3 deletions crates/ruvector-hailo/src/hef_embedder_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,7 @@ impl HefEmbedderPool {
// pays a queue cost but doesn't lose throughput — slot 0 is
// about to be free and the next inference is already in
// flight on another slot.
let g = self.slots[0]
.lock()
.unwrap_or_else(|p| p.into_inner());
let g = self.slots[0].lock().unwrap_or_else(|p| p.into_inner());
g.embed(text)
}
}
Expand Down
1 change: 0 additions & 1 deletion crates/ruvector-hailo/src/inference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
//! → L2-normalise to unit vector
//! → return Vec<f32; 384>


use crate::device::HailoDevice;
use crate::error::HailoError;
use crate::tokenizer::WordPieceTokenizer;
Expand Down
6 changes: 3 additions & 3 deletions crates/ruvector-hailo/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,9 @@ impl HailoEmbedder {
)?,
))
} else {
Some(HefBackend::Single(
crate::hef_embedder::HefEmbedder::open(dev, model_dir)?,
))
Some(HefBackend::Single(crate::hef_embedder::HefEmbedder::open(
dev, model_dir,
)?))
}
} else {
None
Expand Down
1 change: 0 additions & 1 deletion crates/ruvector-hailo/src/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
//! Continuation pieces are prefixed `##`.
//! 5. Wrap with `[CLS] … [SEP]`, pad/truncate to a fixed `max_seq`.


use crate::error::HailoError;
use std::collections::HashMap;
use std::path::Path;
Expand Down
35 changes: 35 additions & 0 deletions crates/ruvector-symphonyqg/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "ruvector-symphonyqg"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "SymphonyQG: co-designed 1-bit quantization + SIMD-batch-aligned graph for in-register ANN search — fastest single-machine ANN at high recall (SIGMOD 2025)"

[[bin]]
name = "symphony-demo"
path = "src/main.rs"

[[bench]]
name = "symphony_bench"
harness = false

[features]
default = []
# Per-query data-parallel batch search via rayon. Each query is independent
# so this scales linearly with cores. Enable for server workloads with
# concurrent query streams. Adds rayon to the runtime dependency tree.
parallel = ["dep:rayon"]

[dependencies]
rand = { workspace = true }
rand_distr = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
rayon = { workspace = true, optional = true }

[dev-dependencies]
criterion = { workspace = true }
81 changes: 81 additions & 0 deletions crates/ruvector-symphonyqg/benches/symphony_bench.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
use rand::SeedableRng;
use rand_distr::{Distribution, Normal, Uniform};

use ruvector_symphonyqg::{build_all, Config, Metric};

fn generate(n: usize, dim: usize, seed: u64) -> Vec<Vec<f32>> {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let unif = Uniform::new(-1.0f64, 1.0);
let noise = Normal::new(0.0f64, 0.3).unwrap();
let n_c = 20.min(n / 2).max(1);
let centroids: Vec<Vec<f64>> = (0..n_c)
.map(|_| (0..dim).map(|_| unif.sample(&mut rng)).collect())
.collect();
use rand::Rng as _;
(0..n)
.map(|_| {
let c = &centroids[rng.gen_range(0..n_c)];
c.iter()
.map(|&x| (x + noise.sample(&mut rng)) as f32)
.collect()
})
.collect()
}

fn bench_search(c: &mut Criterion) {
let dim = 128;
let n = 5_000;
let ef = 100;
let k = 10;
let data = generate(n, dim, 0xBEEF);
let query = generate(1, dim, 0xDEAD)[0].clone();

let cfg = Config {
dim,
m_base: 16,
ef_construction: 200,
metric: Metric::Euclidean,
seed: 1,
};
let (flat, graph_exact, symphony) = build_all(&data, &cfg);

let mut grp = c.benchmark_group("search_n5k_dim128");

grp.bench_function("FlatExact", |b| {
b.iter(|| flat.search(black_box(&query), black_box(k)))
});

for &ef_v in &[50usize, 100, 200] {
grp.bench_with_input(BenchmarkId::new("GraphExact", ef_v), &ef_v, |b, &ef| {
b.iter(|| graph_exact.search(black_box(&query), black_box(k), black_box(ef)))
});
grp.bench_with_input(BenchmarkId::new("SymphonyQG", ef_v), &ef_v, |b, &ef| {
b.iter(|| symphony.search(black_box(&query), black_box(k), black_box(ef)))
});
}
grp.finish();
}

fn bench_encode(c: &mut Criterion) {
let dim = 128;
let n = 100;
let data = generate(n, dim, 42);
let cfg = Config {
dim,
m_base: 8,
ef_construction: 50,
..Config::default()
};
let q = generate(1, dim, 99)[0].clone();

c.bench_function("encode_query_dim128", |b| {
b.iter(|| {
let graph = ruvector_symphonyqg::build::build(black_box(&data), &cfg);
graph.encode_query(black_box(&q))
})
});
}

criterion_group!(benches, bench_search, bench_encode);
criterion_main!(benches);
91 changes: 91 additions & 0 deletions crates/ruvector-symphonyqg/examples/parallel_search.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Measure the rayon-parallel `search_batch` speedup.
//!
//! Run with: `cargo run -p ruvector-symphonyqg --release --example parallel_search --features parallel`
//!
//! Builds an n=10K corpus once, then runs 1000 queries via:
//! (a) sequential `search()` loop
//! (b) `search_batch()` (parallel when --features parallel)
//! and reports the speedup ratio.

use ruvector_symphonyqg::*;
use std::time::Instant;

fn gaussian_vecs(n: usize, dim: usize, seed: u64) -> Vec<Vec<f32>> {
let mut s = (seed as u32) | 1;
(0..n)
.map(|_| {
(0..dim)
.map(|_| {
s ^= s << 13;
s ^= s >> 17;
s ^= s << 5;
(s as f32 / u32::MAX as f32) - 0.5
})
.collect()
})
.collect()
}

fn main() {
let n = 10_000;
let dim = 128;
let n_queries = 1000;
let k = 10;
let ef = 100;

let cfg = Config {
dim,
m_base: 16,
ef_construction: 200,
..Config::default()
};
let vecs = gaussian_vecs(n, dim, 42);
let probe_data = gaussian_vecs(n_queries, dim, 99);
let probes: Vec<&[f32]> = probe_data.iter().map(|v| v.as_slice()).collect();

println!(
"Building n={} dim={} ...",
n, dim
);
let (_, _, sym) = build_all(&vecs, &cfg);

println!("Running {} queries (k={}, ef={}) ...", n_queries, k, ef);

// Warm-up
let _ = sym.search(&probes[0], k, ef);

let t0 = Instant::now();
let seq: Vec<Vec<SearchResult>> = probes.iter().map(|q| sym.search(q, k, ef)).collect();
let seq_ms = t0.elapsed().as_secs_f64() * 1000.0;

let t0 = Instant::now();
let bat = sym.search_batch(&probes, k, ef);
let bat_ms = t0.elapsed().as_secs_f64() * 1000.0;

// Sanity: results must match
assert_eq!(seq.len(), bat.len());
for (s, b) in seq.iter().zip(bat.iter()) {
assert_eq!(s.len(), b.len());
for (sr, br) in s.iter().zip(b.iter()) {
assert_eq!(sr.idx, br.idx);
}
}

println!(
"{:>16} | {:>9}",
"mode", "wall_ms"
);
println!("{}", "-".repeat(30));
println!("{:>16} | {:>9.1}", "sequential", seq_ms);
println!("{:>16} | {:>9.1}", "search_batch", bat_ms);
println!();
println!("speedup: {:.2}×", seq_ms / bat_ms);
println!(
"(parallel feature {})",
if cfg!(feature = "parallel") {
"ON"
} else {
"OFF — rebuild with --features parallel for speedup"
}
);
}
Loading
Loading