mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-05-27 00:25:10 +00:00
feat: Add benchmarks for new features + persistence integration tests
Benchmarks (store.rs, 8 new bench tests): - Batch scoring 10k blocks vs individual scoring - 5-bit and 7-bit dequant fast paths (4096 values) - 5-bit quantize fast path (4096 values) - SVD adaptive rank selection (64x64 matrix) - format_report and format_json throughput - MetricsSeries trend computation (100 snapshots) Persistence tests (10 tests, feature-gated): - FileBlockIO: write/read, multi-tier, delete, overwrite, missing key - FileMetaLog: append/get, upsert, iter, missing key, multi-block 354 tests pass (with --features persistence). https://claude.ai/code/session_01Ksy165BL5nGpVoWaAfTE7t
This commit is contained in:
parent
8fa851c917
commit
f73f13c08a
2 changed files with 469 additions and 0 deletions
|
|
@ -1934,4 +1934,248 @@ mod tests {
|
|||
assert_eq!(*ts, 200);
|
||||
assert_eq!(m.total_blocks, 5);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Benchmarks
|
||||
// -----------------------------------------------------------------------
|
||||
//
|
||||
// Run with: cargo test bench_ -- --nocapture
|
||||
// These use std::time::Instant and std::hint::black_box for stable timing.
|
||||
|
||||
#[test]
|
||||
fn bench_batch_scoring_10k() {
|
||||
use std::time::Instant;
|
||||
use crate::tiering::{
|
||||
TierConfig, BlockMeta as TBlockMeta, Tier as TTier,
|
||||
compute_scores_batch, compute_score,
|
||||
};
|
||||
|
||||
let cfg = TierConfig::default();
|
||||
let metas: Vec<TBlockMeta> = (0..10_000).map(|i| {
|
||||
TBlockMeta {
|
||||
ema_rate: (i as f32) * 0.0001,
|
||||
access_window: 0x5555_5555_5555_5555,
|
||||
last_access: 50 + (i as u64 % 100),
|
||||
access_count: i as u64,
|
||||
current_tier: TTier::Tier1,
|
||||
tier_since: 0,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let iters = 1000;
|
||||
|
||||
// Individual scoring
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
for m in &metas {
|
||||
std::hint::black_box(compute_score(&cfg, 100, m));
|
||||
}
|
||||
}
|
||||
let individual = start.elapsed();
|
||||
|
||||
// Batch scoring
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
std::hint::black_box(compute_scores_batch(&cfg, 100, &metas));
|
||||
}
|
||||
let batch = start.elapsed();
|
||||
|
||||
eprintln!("Individual scoring 10k x {iters}: {:?} ({:.0} ns/block)",
|
||||
individual, individual.as_nanos() as f64 / (iters * 10_000) as f64);
|
||||
eprintln!("Batch scoring 10k x {iters}: {:?} ({:.0} ns/block)",
|
||||
batch, batch.as_nanos() as f64 / (iters * 10_000) as f64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_dequant_5bit_4096() {
|
||||
use std::time::Instant;
|
||||
|
||||
let data: Vec<f32> = (0..4096).map(|i| (i as f32 - 2048.0) * 0.01).collect();
|
||||
let (packed, scale) = quantize_block(&data, 5);
|
||||
let mut out = vec![0.0f32; 4096];
|
||||
|
||||
let iters = 10_000;
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
std::hint::black_box(dequantize_block(&packed, scale, 5, 4096, &mut out));
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let total_bytes = 4096u64 * 4 * iters as u64;
|
||||
let gbs = total_bytes as f64 / elapsed.as_secs_f64() / 1e9;
|
||||
eprintln!("Dequant 5-bit 4096 x {iters}: {:?} ({:.2} GB/s output throughput)",
|
||||
elapsed, gbs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_dequant_7bit_4096() {
|
||||
use std::time::Instant;
|
||||
|
||||
let data: Vec<f32> = (0..4096).map(|i| (i as f32 - 2048.0) * 0.01).collect();
|
||||
let (packed, scale) = quantize_block(&data, 7);
|
||||
let mut out = vec![0.0f32; 4096];
|
||||
|
||||
let iters = 10_000;
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
std::hint::black_box(dequantize_block(&packed, scale, 7, 4096, &mut out));
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let total_bytes = 4096u64 * 4 * iters as u64;
|
||||
let gbs = total_bytes as f64 / elapsed.as_secs_f64() / 1e9;
|
||||
eprintln!("Dequant 7-bit 4096 x {iters}: {:?} ({:.2} GB/s output throughput)",
|
||||
elapsed, gbs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_quant_5bit_4096() {
|
||||
use std::time::Instant;
|
||||
|
||||
let data: Vec<f32> = (0..4096).map(|i| (i as f32 - 2048.0) * 0.01).collect();
|
||||
|
||||
let iters = 10_000;
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
std::hint::black_box(quantize_block(&data, 5));
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let total_bytes = 4096u64 * 4 * iters as u64;
|
||||
let gbs = total_bytes as f64 / elapsed.as_secs_f64() / 1e9;
|
||||
eprintln!("Quant 5-bit 4096 x {iters}: {:?} ({:.2} GB/s input throughput)",
|
||||
elapsed, gbs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_svd_adaptive_64x64() {
|
||||
use std::time::Instant;
|
||||
use crate::delta::FactorSet;
|
||||
|
||||
let (rows, cols) = (64, 64);
|
||||
let data: Vec<f32> = (0..rows * cols)
|
||||
.map(|i| (i as f32 * 0.37).sin() + (i as f32 * 0.73).cos())
|
||||
.collect();
|
||||
|
||||
let iters = 100;
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
std::hint::black_box(
|
||||
FactorSet::from_data_adaptive(&data, rows, cols, 16, 0.05)
|
||||
);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
eprintln!("SVD adaptive 64x64 (max_rank=16, target=0.05) x {iters}: {:?} ({:.2} ms/iter)",
|
||||
elapsed, elapsed.as_secs_f64() * 1000.0 / iters as f64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_format_report() {
|
||||
use std::time::Instant;
|
||||
use crate::metrics::StoreMetrics;
|
||||
|
||||
let m = StoreMetrics {
|
||||
total_blocks: 10_000,
|
||||
tier0_blocks: 500,
|
||||
tier1_blocks: 4000,
|
||||
tier2_blocks: 3500,
|
||||
tier3_blocks: 2000,
|
||||
tier1_bytes: 4_000_000,
|
||||
tier2_bytes: 2_500_000,
|
||||
tier3_bytes: 750_000,
|
||||
total_reads: 1_000_000,
|
||||
total_writes: 500_000,
|
||||
total_evictions: 5000,
|
||||
total_upgrades: 12_000,
|
||||
total_downgrades: 8000,
|
||||
total_reconstructions: 200,
|
||||
total_checksum_failures: 0,
|
||||
total_compactions: 150,
|
||||
tier_flips_last_minute: 0.023,
|
||||
avg_score_tier1: 0.85,
|
||||
avg_score_tier2: 0.45,
|
||||
avg_score_tier3: 0.12,
|
||||
};
|
||||
|
||||
let iters = 10_000;
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
std::hint::black_box(m.format_report());
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
eprintln!("format_report x {iters}: {:?} ({:.0} ns/call)",
|
||||
elapsed, elapsed.as_nanos() as f64 / iters as f64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_format_json() {
|
||||
use std::time::Instant;
|
||||
use crate::metrics::StoreMetrics;
|
||||
|
||||
let m = StoreMetrics {
|
||||
total_blocks: 10_000,
|
||||
tier0_blocks: 500,
|
||||
tier1_blocks: 4000,
|
||||
tier2_blocks: 3500,
|
||||
tier3_blocks: 2000,
|
||||
tier1_bytes: 4_000_000,
|
||||
tier2_bytes: 2_500_000,
|
||||
tier3_bytes: 750_000,
|
||||
total_reads: 1_000_000,
|
||||
total_writes: 500_000,
|
||||
total_evictions: 5000,
|
||||
total_upgrades: 12_000,
|
||||
total_downgrades: 8000,
|
||||
total_reconstructions: 200,
|
||||
total_checksum_failures: 0,
|
||||
total_compactions: 150,
|
||||
tier_flips_last_minute: 0.023,
|
||||
avg_score_tier1: 0.85,
|
||||
avg_score_tier2: 0.45,
|
||||
avg_score_tier3: 0.12,
|
||||
};
|
||||
|
||||
let iters = 10_000;
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
std::hint::black_box(m.format_json());
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
eprintln!("format_json x {iters}: {:?} ({:.0} ns/call)",
|
||||
elapsed, elapsed.as_nanos() as f64 / iters as f64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bench_metrics_series_trend_100() {
|
||||
use std::time::Instant;
|
||||
use crate::metrics::{StoreMetrics, MetricsSeries};
|
||||
|
||||
let mut series = MetricsSeries::new(256);
|
||||
for i in 0..100u64 {
|
||||
series.record(i, StoreMetrics {
|
||||
total_blocks: 1000 + i,
|
||||
tier1_blocks: 400 + i % 50,
|
||||
tier2_blocks: 350,
|
||||
tier3_blocks: 250,
|
||||
tier1_bytes: 400_000 + i * 100,
|
||||
tier2_bytes: 250_000,
|
||||
tier3_bytes: 75_000,
|
||||
total_evictions: i * 3,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
let iters = 10_000;
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
std::hint::black_box(series.trend());
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
eprintln!("MetricsSeries trend (100 snapshots) x {iters}: {:?} ({:.0} ns/call)",
|
||||
elapsed, elapsed.as_nanos() as f64 / iters as f64);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
225
crates/ruvector-temporal-tensor/tests/persistence_tests.rs
Normal file
225
crates/ruvector-temporal-tensor/tests/persistence_tests.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#![cfg(feature = "persistence")]
|
||||
|
||||
use ruvector_temporal_tensor::persistence::{FileBlockIO, FileMetaLog};
|
||||
use ruvector_temporal_tensor::store::{
|
||||
BlockIO, BlockKey, BlockMeta, DType, MetaLog, ReconstructPolicy, Tier,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn test_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!("ruvector_test_{}", name));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
fn cleanup(dir: &PathBuf) {
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
fn make_key(id: u128, idx: u32) -> BlockKey {
|
||||
BlockKey {
|
||||
tensor_id: id,
|
||||
block_index: idx,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_meta(key: BlockKey, tier: Tier) -> BlockMeta {
|
||||
BlockMeta {
|
||||
key,
|
||||
dtype: DType::F32,
|
||||
tier,
|
||||
bits: 8,
|
||||
scale: 0.5,
|
||||
zero_point: 0,
|
||||
created_at: 100,
|
||||
last_access_at: 200,
|
||||
access_count: 5,
|
||||
ema_rate: 0.1,
|
||||
window: 0xFF,
|
||||
checksum: 0xDEADBEEF,
|
||||
reconstruct: ReconstructPolicy::None,
|
||||
tier_age: 10,
|
||||
lineage_parent: None,
|
||||
block_bytes: 64,
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// FileBlockIO tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_file_block_io_write_read() {
|
||||
let dir = test_dir("block_io_write_read");
|
||||
let mut bio = FileBlockIO::new(&dir).unwrap();
|
||||
|
||||
let key = make_key(1, 0);
|
||||
let data = vec![0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89];
|
||||
bio.write_block(Tier::Tier1, key, &data).unwrap();
|
||||
|
||||
let mut dst = vec![0u8; 32];
|
||||
let n = bio.read_block(Tier::Tier1, key, &mut dst).unwrap();
|
||||
assert_eq!(n, data.len());
|
||||
assert_eq!(&dst[..n], &data[..]);
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_block_io_different_tiers() {
|
||||
let dir = test_dir("block_io_tiers");
|
||||
let mut bio = FileBlockIO::new(&dir).unwrap();
|
||||
|
||||
let key = make_key(1, 0);
|
||||
let data1 = vec![1u8; 16];
|
||||
let data2 = vec![2u8; 8];
|
||||
let data3 = vec![3u8; 4];
|
||||
|
||||
bio.write_block(Tier::Tier1, key, &data1).unwrap();
|
||||
bio.write_block(Tier::Tier2, key, &data2).unwrap();
|
||||
bio.write_block(Tier::Tier3, key, &data3).unwrap();
|
||||
|
||||
let mut buf = vec![0u8; 32];
|
||||
|
||||
let n1 = bio.read_block(Tier::Tier1, key, &mut buf).unwrap();
|
||||
assert_eq!(&buf[..n1], &data1[..]);
|
||||
|
||||
let n2 = bio.read_block(Tier::Tier2, key, &mut buf).unwrap();
|
||||
assert_eq!(&buf[..n2], &data2[..]);
|
||||
|
||||
let n3 = bio.read_block(Tier::Tier3, key, &mut buf).unwrap();
|
||||
assert_eq!(&buf[..n3], &data3[..]);
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_block_io_delete() {
|
||||
let dir = test_dir("block_io_delete");
|
||||
let mut bio = FileBlockIO::new(&dir).unwrap();
|
||||
|
||||
let key = make_key(1, 0);
|
||||
bio.write_block(Tier::Tier1, key, &[1, 2, 3]).unwrap();
|
||||
bio.delete_block(Tier::Tier1, key).unwrap();
|
||||
|
||||
let mut buf = vec![0u8; 32];
|
||||
let result = bio.read_block(Tier::Tier1, key, &mut buf);
|
||||
assert!(result.is_err() || result.unwrap() == 0);
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_block_io_overwrite() {
|
||||
let dir = test_dir("block_io_overwrite");
|
||||
let mut bio = FileBlockIO::new(&dir).unwrap();
|
||||
|
||||
let key = make_key(1, 0);
|
||||
bio.write_block(Tier::Tier1, key, &[1, 2, 3]).unwrap();
|
||||
bio.write_block(Tier::Tier1, key, &[4, 5, 6, 7]).unwrap();
|
||||
|
||||
let mut buf = vec![0u8; 32];
|
||||
let n = bio.read_block(Tier::Tier1, key, &mut buf).unwrap();
|
||||
assert_eq!(&buf[..n], &[4, 5, 6, 7]);
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_block_io_missing_key() {
|
||||
let dir = test_dir("block_io_missing");
|
||||
let bio = FileBlockIO::new(&dir).unwrap();
|
||||
|
||||
let mut buf = vec![0u8; 32];
|
||||
let result = bio.read_block(Tier::Tier1, make_key(99, 0), &mut buf);
|
||||
assert!(result.is_err() || result.unwrap() == 0);
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// FileMetaLog tests
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_file_meta_log_append_get() {
|
||||
let dir = test_dir("meta_log_append");
|
||||
let mut log = FileMetaLog::new(&dir).unwrap();
|
||||
|
||||
let key = make_key(1, 0);
|
||||
let meta = make_meta(key, Tier::Tier1);
|
||||
log.append(&meta).unwrap();
|
||||
|
||||
let retrieved = log.get(key).unwrap();
|
||||
assert_eq!(retrieved.key, key);
|
||||
assert_eq!(retrieved.tier, Tier::Tier1);
|
||||
assert_eq!(retrieved.bits, 8);
|
||||
assert!((retrieved.scale - 0.5).abs() < 1e-6);
|
||||
assert_eq!(retrieved.checksum, 0xDEADBEEF);
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_meta_log_upsert() {
|
||||
let dir = test_dir("meta_log_upsert");
|
||||
let mut log = FileMetaLog::new(&dir).unwrap();
|
||||
|
||||
let key = make_key(1, 0);
|
||||
let meta1 = make_meta(key, Tier::Tier1);
|
||||
log.append(&meta1).unwrap();
|
||||
|
||||
let mut meta2 = make_meta(key, Tier::Tier2);
|
||||
meta2.bits = 7;
|
||||
log.append(&meta2).unwrap();
|
||||
|
||||
let retrieved = log.get(key).unwrap();
|
||||
assert_eq!(retrieved.tier, Tier::Tier2);
|
||||
assert_eq!(retrieved.bits, 7);
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_meta_log_iter() {
|
||||
let dir = test_dir("meta_log_iter");
|
||||
let mut log = FileMetaLog::new(&dir).unwrap();
|
||||
|
||||
for i in 0..5u128 {
|
||||
let key = make_key(i, 0);
|
||||
log.append(&make_meta(key, Tier::Tier1)).unwrap();
|
||||
}
|
||||
|
||||
let count = log.iter().count();
|
||||
assert_eq!(count, 5);
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_meta_log_missing_key() {
|
||||
let dir = test_dir("meta_log_missing");
|
||||
let log = FileMetaLog::new(&dir).unwrap();
|
||||
assert!(log.get(make_key(99, 0)).is_none());
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_meta_log_multiple_blocks_same_tensor() {
|
||||
let dir = test_dir("meta_log_multi_block");
|
||||
let mut log = FileMetaLog::new(&dir).unwrap();
|
||||
|
||||
for idx in 0..3u32 {
|
||||
let key = make_key(1, idx);
|
||||
log.append(&make_meta(key, Tier::Tier1)).unwrap();
|
||||
}
|
||||
|
||||
assert!(log.get(make_key(1, 0)).is_some());
|
||||
assert!(log.get(make_key(1, 1)).is_some());
|
||||
assert!(log.get(make_key(1, 2)).is_some());
|
||||
assert!(log.get(make_key(1, 3)).is_none());
|
||||
|
||||
cleanup(&dir);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue