Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
233
vendor/ruvector/crates/ruvector-delta-wasm/src/apply.rs
vendored
Normal file
233
vendor/ruvector/crates/ruvector-delta-wasm/src/apply.rs
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
//! Delta application optimizations
|
||||
//!
|
||||
//! Provides optimized routines for applying deltas to vectors.
|
||||
|
||||
use ruvector_delta_core::{Delta, DeltaValue, VectorDelta};
|
||||
|
||||
/// Apply delta to a vector in-place
|
||||
pub fn apply_delta(base: &mut [f32], delta: &VectorDelta) -> Result<(), &'static str> {
|
||||
if base.len() != delta.dimensions {
|
||||
return Err("Dimension mismatch");
|
||||
}
|
||||
|
||||
match &delta.value {
|
||||
DeltaValue::Identity => {
|
||||
// No change
|
||||
}
|
||||
DeltaValue::Sparse(ops) => {
|
||||
for op in ops {
|
||||
let idx = op.index as usize;
|
||||
if idx < base.len() {
|
||||
base[idx] += op.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
DeltaValue::Dense(deltas) => {
|
||||
apply_dense(base, deltas);
|
||||
}
|
||||
DeltaValue::Replace(new_values) => {
|
||||
base.copy_from_slice(new_values);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Optimized dense application
|
||||
fn apply_dense(base: &mut [f32], deltas: &[f32]) {
|
||||
// Process in chunks of 8 for better CPU utilization
|
||||
let chunks = base.len() / 8;
|
||||
let remainder = base.len() % 8;
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 8;
|
||||
base[offset] += deltas[offset];
|
||||
base[offset + 1] += deltas[offset + 1];
|
||||
base[offset + 2] += deltas[offset + 2];
|
||||
base[offset + 3] += deltas[offset + 3];
|
||||
base[offset + 4] += deltas[offset + 4];
|
||||
base[offset + 5] += deltas[offset + 5];
|
||||
base[offset + 6] += deltas[offset + 6];
|
||||
base[offset + 7] += deltas[offset + 7];
|
||||
}
|
||||
|
||||
let start = chunks * 8;
|
||||
for i in 0..remainder {
|
||||
base[start + i] += deltas[start + i];
|
||||
}
|
||||
}
|
||||
|
||||
/// SIMD-accelerated delta application
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn apply_delta_simd(base: &mut [f32], delta: &VectorDelta) -> Result<(), &'static str> {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
if base.len() != delta.dimensions {
|
||||
return Err("Dimension mismatch");
|
||||
}
|
||||
|
||||
match &delta.value {
|
||||
DeltaValue::Identity => Ok(()),
|
||||
DeltaValue::Sparse(ops) => {
|
||||
for op in ops {
|
||||
let idx = op.index as usize;
|
||||
if idx < base.len() {
|
||||
base[idx] += op.value;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
DeltaValue::Dense(deltas) => {
|
||||
let chunks = base.len() / 4;
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let base_ptr = base.as_mut_ptr().add(offset);
|
||||
let delta_ptr = deltas.as_ptr().add(offset);
|
||||
|
||||
let base_vec = v128_load(base_ptr as *const v128);
|
||||
let delta_vec = v128_load(delta_ptr as *const v128);
|
||||
let result = f32x4_add(base_vec, delta_vec);
|
||||
|
||||
v128_store(base_ptr as *mut v128, result);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remainder
|
||||
for i in (chunks * 4)..base.len() {
|
||||
base[i] += deltas[i];
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
DeltaValue::Replace(new_values) => {
|
||||
base.copy_from_slice(new_values);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply delta with scaling factor
|
||||
pub fn apply_scaled(base: &mut [f32], delta: &VectorDelta, scale: f32) -> Result<(), &'static str> {
|
||||
if base.len() != delta.dimensions {
|
||||
return Err("Dimension mismatch");
|
||||
}
|
||||
|
||||
match &delta.value {
|
||||
DeltaValue::Identity => {
|
||||
// No change
|
||||
}
|
||||
DeltaValue::Sparse(ops) => {
|
||||
for op in ops {
|
||||
let idx = op.index as usize;
|
||||
if idx < base.len() {
|
||||
base[idx] += op.value * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
DeltaValue::Dense(deltas) => {
|
||||
for (b, d) in base.iter_mut().zip(deltas.iter()) {
|
||||
*b += d * scale;
|
||||
}
|
||||
}
|
||||
DeltaValue::Replace(new_values) => {
|
||||
// For replace, scale interpolates between old and new
|
||||
for (b, n) in base.iter_mut().zip(new_values.iter()) {
|
||||
*b = *b * (1.0 - scale) + *n * scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Batch apply to multiple vectors
|
||||
pub fn apply_batch(bases: &mut [&mut [f32]], delta: &VectorDelta) -> Result<(), &'static str> {
|
||||
for base in bases {
|
||||
apply_delta(*base, delta)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply multiple deltas to a single vector
|
||||
pub fn apply_sequence(base: &mut [f32], deltas: &[VectorDelta]) -> Result<(), &'static str> {
|
||||
for delta in deltas {
|
||||
apply_delta(base, delta)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ruvector_delta_core::Delta;
|
||||
|
||||
#[test]
|
||||
fn test_apply_sparse() {
|
||||
let old = vec![1.0f32, 2.0, 3.0, 4.0, 5.0];
|
||||
let new = vec![1.0f32, 2.5, 3.0, 4.5, 5.0];
|
||||
|
||||
let delta = VectorDelta::compute(&old, &new);
|
||||
|
||||
let mut result = old.clone();
|
||||
apply_delta(&mut result, &delta).unwrap();
|
||||
|
||||
for (r, n) in result.iter().zip(new.iter()) {
|
||||
assert!((r - n).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_dense() {
|
||||
let old = vec![1.0f32, 2.0, 3.0];
|
||||
let new = vec![2.0f32, 3.0, 4.0];
|
||||
|
||||
let delta = VectorDelta::compute(&old, &new);
|
||||
|
||||
let mut result = old.clone();
|
||||
apply_delta(&mut result, &delta).unwrap();
|
||||
|
||||
for (r, n) in result.iter().zip(new.iter()) {
|
||||
assert!((r - n).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_scaled() {
|
||||
let mut base = vec![0.0f32, 0.0, 0.0];
|
||||
let delta = VectorDelta::from_dense(vec![1.0, 2.0, 3.0]);
|
||||
|
||||
apply_scaled(&mut base, &delta, 0.5).unwrap();
|
||||
|
||||
assert!((base[0] - 0.5).abs() < 1e-6);
|
||||
assert!((base[1] - 1.0).abs() < 1e-6);
|
||||
assert!((base[2] - 1.5).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_sequence() {
|
||||
let mut base = vec![0.0f32, 0.0, 0.0];
|
||||
|
||||
let deltas = vec![
|
||||
VectorDelta::from_dense(vec![1.0, 0.0, 0.0]),
|
||||
VectorDelta::from_dense(vec![0.0, 1.0, 0.0]),
|
||||
VectorDelta::from_dense(vec![0.0, 0.0, 1.0]),
|
||||
];
|
||||
|
||||
apply_sequence(&mut base, &deltas).unwrap();
|
||||
|
||||
assert!((base[0] - 1.0).abs() < 1e-6);
|
||||
assert!((base[1] - 1.0).abs() < 1e-6);
|
||||
assert!((base[2] - 1.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dimension_mismatch() {
|
||||
let mut base = vec![0.0f32; 5];
|
||||
let delta = VectorDelta::new(10); // Different dimensions
|
||||
|
||||
let result = apply_delta(&mut base, &delta);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
214
vendor/ruvector/crates/ruvector-delta-wasm/src/capture.rs
vendored
Normal file
214
vendor/ruvector/crates/ruvector-delta-wasm/src/capture.rs
vendored
Normal file
@@ -0,0 +1,214 @@
|
||||
//! Delta capture optimizations
|
||||
//!
|
||||
//! Provides optimized routines for capturing deltas from vector pairs.
|
||||
|
||||
use ruvector_delta_core::{Delta, DeltaOp, DeltaValue, VectorDelta};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// Configuration for delta capture
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CaptureConfig {
|
||||
/// Epsilon for considering values as zero
|
||||
pub epsilon: f32,
|
||||
/// Sparsity threshold for using sparse representation
|
||||
pub sparsity_threshold: f32,
|
||||
/// Maximum dimensions for always using sparse
|
||||
pub sparse_max_dims: usize,
|
||||
}
|
||||
|
||||
impl Default for CaptureConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
epsilon: 1e-7,
|
||||
sparsity_threshold: 0.7,
|
||||
sparse_max_dims: 10_000,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimized delta capture with configurable thresholds
|
||||
pub fn capture_delta(old: &[f32], new: &[f32], config: &CaptureConfig) -> VectorDelta {
|
||||
assert_eq!(old.len(), new.len(), "Vectors must have same length");
|
||||
|
||||
let dimensions = old.len();
|
||||
|
||||
// For small vectors, always use sparse initially
|
||||
if dimensions <= 64 {
|
||||
return capture_sparse(old, new, config);
|
||||
}
|
||||
|
||||
// For larger vectors, sample to estimate sparsity
|
||||
let sample_size = (dimensions / 10).max(16).min(256);
|
||||
let mut non_zero_sample = 0;
|
||||
|
||||
for i in (0..dimensions).step_by(dimensions / sample_size) {
|
||||
if (new[i] - old[i]).abs() > config.epsilon {
|
||||
non_zero_sample += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let estimated_sparsity = 1.0 - (non_zero_sample as f32 / sample_size as f32);
|
||||
|
||||
if estimated_sparsity > config.sparsity_threshold {
|
||||
capture_sparse(old, new, config)
|
||||
} else {
|
||||
capture_dense(old, new, config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Capture with sparse representation
|
||||
fn capture_sparse(old: &[f32], new: &[f32], config: &CaptureConfig) -> VectorDelta {
|
||||
let dimensions = old.len();
|
||||
let mut ops: SmallVec<[DeltaOp<f32>; 8]> = SmallVec::new();
|
||||
|
||||
for i in 0..dimensions {
|
||||
let diff = new[i] - old[i];
|
||||
if diff.abs() > config.epsilon {
|
||||
ops.push(DeltaOp::new(i as u32, diff));
|
||||
}
|
||||
}
|
||||
|
||||
VectorDelta::from_sparse(ops, dimensions)
|
||||
}
|
||||
|
||||
/// Capture with dense representation
|
||||
fn capture_dense(old: &[f32], new: &[f32], config: &CaptureConfig) -> VectorDelta {
|
||||
let diffs: Vec<f32> = old
|
||||
.iter()
|
||||
.zip(new.iter())
|
||||
.map(|(o, n)| {
|
||||
let d = n - o;
|
||||
if d.abs() <= config.epsilon {
|
||||
0.0
|
||||
} else {
|
||||
d
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
VectorDelta::from_dense(diffs)
|
||||
}
|
||||
|
||||
/// SIMD-accelerated delta capture (when available)
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn capture_delta_simd(old: &[f32], new: &[f32], config: &CaptureConfig) -> VectorDelta {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
let dimensions = old.len();
|
||||
if dimensions < 4 {
|
||||
return capture_delta(old, new, config);
|
||||
}
|
||||
|
||||
let chunks = dimensions / 4;
|
||||
let remainder = dimensions % 4;
|
||||
|
||||
let mut diffs = Vec::with_capacity(dimensions);
|
||||
let epsilon_vec = f32x4_splat(config.epsilon);
|
||||
let neg_epsilon_vec = f32x4_splat(-config.epsilon);
|
||||
let zero_vec = f32x4_splat(0.0);
|
||||
|
||||
// Process 4 elements at a time
|
||||
for i in 0..chunks {
|
||||
let base = i * 4;
|
||||
|
||||
unsafe {
|
||||
let old_chunk = v128_load(old.as_ptr().add(base) as *const v128);
|
||||
let new_chunk = v128_load(new.as_ptr().add(base) as *const v128);
|
||||
|
||||
// Compute differences
|
||||
let diff = f32x4_sub(new_chunk, old_chunk);
|
||||
|
||||
// Zero out small differences
|
||||
let above_eps = f32x4_gt(diff, epsilon_vec);
|
||||
let below_neg_eps = f32x4_lt(diff, neg_epsilon_vec);
|
||||
let significant = v128_or(above_eps, below_neg_eps);
|
||||
|
||||
let masked = v128_and(diff, significant);
|
||||
|
||||
// Extract to array
|
||||
let d: [f32; 4] = core::mem::transmute(masked);
|
||||
diffs.extend_from_slice(&d);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remainder
|
||||
for i in (chunks * 4)..dimensions {
|
||||
let d = new[i] - old[i];
|
||||
diffs.push(if d.abs() > config.epsilon { d } else { 0.0 });
|
||||
}
|
||||
|
||||
VectorDelta::from_dense(diffs)
|
||||
}
|
||||
|
||||
/// Batch capture for multiple vector pairs
|
||||
pub fn capture_batch(
|
||||
old_vecs: &[&[f32]],
|
||||
new_vecs: &[&[f32]],
|
||||
config: &CaptureConfig,
|
||||
) -> Vec<VectorDelta> {
|
||||
assert_eq!(
|
||||
old_vecs.len(),
|
||||
new_vecs.len(),
|
||||
"Must have same number of vectors"
|
||||
);
|
||||
|
||||
old_vecs
|
||||
.iter()
|
||||
.zip(new_vecs.iter())
|
||||
.map(|(old, new)| capture_delta(old, new, config))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_capture_sparse() {
|
||||
let old = vec![1.0f32; 100];
|
||||
let mut new = old.clone();
|
||||
new[10] = 2.0;
|
||||
new[50] = 3.0;
|
||||
|
||||
let config = CaptureConfig::default();
|
||||
let delta = capture_delta(&old, &new, &config);
|
||||
|
||||
assert!(matches!(delta.value, DeltaValue::Sparse(_)));
|
||||
assert_eq!(delta.value.nnz(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_dense() {
|
||||
let old = vec![1.0f32; 4];
|
||||
let new = vec![2.0f32; 4];
|
||||
|
||||
let config = CaptureConfig::default();
|
||||
let delta = capture_delta(&old, &new, &config);
|
||||
|
||||
// All changed, should be dense
|
||||
assert_eq!(delta.value.nnz(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_identity() {
|
||||
let v = vec![1.0f32, 2.0, 3.0];
|
||||
let config = CaptureConfig::default();
|
||||
let delta = capture_delta(&v, &v, &config);
|
||||
|
||||
assert!(delta.is_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_epsilon_filtering() {
|
||||
let old = vec![1.0f32, 2.0, 3.0];
|
||||
let new = vec![1.0000001, 2.0000001, 3.0000001]; // Very small changes
|
||||
|
||||
let config = CaptureConfig {
|
||||
epsilon: 1e-5,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let delta = capture_delta(&old, &new, &config);
|
||||
assert!(delta.is_identity());
|
||||
}
|
||||
}
|
||||
604
vendor/ruvector/crates/ruvector-delta-wasm/src/lib.rs
vendored
Normal file
604
vendor/ruvector/crates/ruvector-delta-wasm/src/lib.rs
vendored
Normal file
@@ -0,0 +1,604 @@
|
||||
//! # RuVector Delta WASM
|
||||
//!
|
||||
//! WASM bindings for delta operations on vectors.
|
||||
//! Provides high-performance delta capture, application, and SIMD-accelerated operations.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! - Delta capture from vector pairs
|
||||
//! - Efficient delta application
|
||||
//! - SIMD acceleration (when available)
|
||||
//! - Shared memory for zero-copy operations
|
||||
//! - Streaming delta support
|
||||
//!
|
||||
//! ## Example (JavaScript)
|
||||
//!
|
||||
//! ```javascript
|
||||
//! import { DeltaEngine, vectorDelta } from 'ruvector-delta-wasm';
|
||||
//!
|
||||
//! const engine = new DeltaEngine(384);
|
||||
//!
|
||||
//! const oldVec = new Float32Array([1.0, 2.0, 3.0, ...]);
|
||||
//! const newVec = new Float32Array([1.1, 2.0, 3.5, ...]);
|
||||
//!
|
||||
//! const delta = engine.capture(oldVec, newVec);
|
||||
//! console.log('Delta sparsity:', delta.sparsity);
|
||||
//!
|
||||
//! engine.apply(oldVec, delta);
|
||||
//! // oldVec now equals newVec
|
||||
//! ```
|
||||
|
||||
mod apply;
|
||||
mod capture;
|
||||
mod memory;
|
||||
mod simd;
|
||||
|
||||
pub use apply::*;
|
||||
pub use capture::*;
|
||||
pub use memory::*;
|
||||
pub use simd::*;
|
||||
|
||||
use js_sys::{Array, Float32Array, Object, Reflect, Uint8Array};
|
||||
use parking_lot::RwLock;
|
||||
use ruvector_delta_core::{
|
||||
Delta, DeltaEncoding, DeltaOp, DeltaStream, DeltaValue, DeltaWindow, HybridEncoding,
|
||||
SparseEncoding, VectorDelta, WindowConfig, WindowType,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_wasm_bindgen::{from_value, to_value};
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Initialize panic hook for better error messages
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn init() {
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
tracing_wasm::set_as_global_default();
|
||||
}
|
||||
|
||||
/// Get WASM module version
|
||||
#[wasm_bindgen]
|
||||
pub fn version() -> String {
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
}
|
||||
|
||||
/// Check for SIMD support
|
||||
#[wasm_bindgen(js_name = hasSIMD)]
|
||||
pub fn has_simd() -> bool {
|
||||
#[cfg(target_feature = "simd128")]
|
||||
{
|
||||
true
|
||||
}
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// JavaScript-friendly delta representation
|
||||
#[wasm_bindgen]
|
||||
pub struct JsDelta {
|
||||
inner: VectorDelta,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl JsDelta {
|
||||
/// Get the dimensions of this delta
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn dimensions(&self) -> usize {
|
||||
self.inner.dimensions
|
||||
}
|
||||
|
||||
/// Check if this is an identity (no change) delta
|
||||
#[wasm_bindgen(getter, js_name = isIdentity)]
|
||||
pub fn is_identity(&self) -> bool {
|
||||
self.inner.is_identity()
|
||||
}
|
||||
|
||||
/// Get the sparsity ratio (0.0 = dense, 1.0 = fully sparse)
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn sparsity(&self) -> f32 {
|
||||
let nnz = self.inner.value.nnz();
|
||||
if self.inner.dimensions == 0 {
|
||||
1.0
|
||||
} else {
|
||||
1.0 - (nnz as f32 / self.inner.dimensions as f32)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the L2 norm of the delta
|
||||
#[wasm_bindgen(js_name = l2Norm)]
|
||||
pub fn l2_norm(&self) -> f32 {
|
||||
self.inner.l2_norm()
|
||||
}
|
||||
|
||||
/// Get the L1 norm of the delta
|
||||
#[wasm_bindgen(js_name = l1Norm)]
|
||||
pub fn l1_norm(&self) -> f32 {
|
||||
self.inner.l1_norm()
|
||||
}
|
||||
|
||||
/// Get the number of non-zero elements
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn nnz(&self) -> usize {
|
||||
self.inner.value.nnz()
|
||||
}
|
||||
|
||||
/// Get byte size of this delta
|
||||
#[wasm_bindgen(getter, js_name = byteSize)]
|
||||
pub fn byte_size(&self) -> usize {
|
||||
self.inner.byte_size()
|
||||
}
|
||||
|
||||
/// Scale the delta by a factor
|
||||
pub fn scale(&self, factor: f32) -> JsDelta {
|
||||
JsDelta {
|
||||
inner: self.inner.scale(factor),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip delta values to a range
|
||||
pub fn clip(&self, min: f32, max: f32) -> JsDelta {
|
||||
JsDelta {
|
||||
inner: self.inner.clip(min, max),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compose with another delta
|
||||
pub fn compose(&self, other: &JsDelta) -> JsDelta {
|
||||
JsDelta {
|
||||
inner: self.inner.clone().compose(other.inner.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the inverse delta
|
||||
pub fn inverse(&self) -> JsDelta {
|
||||
JsDelta {
|
||||
inner: self.inner.inverse(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Export to dense Float32Array
|
||||
#[wasm_bindgen(js_name = toDense)]
|
||||
pub fn to_dense(&self) -> Float32Array {
|
||||
let dense = self.inner.value.to_dense(self.inner.dimensions);
|
||||
match dense {
|
||||
DeltaValue::Dense(values) => Float32Array::from(&values[..]),
|
||||
_ => Float32Array::new_with_length(self.inner.dimensions as u32),
|
||||
}
|
||||
}
|
||||
|
||||
/// Export sparse representation as array of {index, value}
|
||||
#[wasm_bindgen(js_name = toSparse)]
|
||||
pub fn to_sparse(&self) -> Result<JsValue, JsValue> {
|
||||
let ops = match &self.inner.value {
|
||||
DeltaValue::Identity => Vec::new(),
|
||||
DeltaValue::Sparse(ops) => ops
|
||||
.iter()
|
||||
.map(|op| SparseEntry {
|
||||
index: op.index,
|
||||
value: op.value,
|
||||
})
|
||||
.collect(),
|
||||
DeltaValue::Dense(values) | DeltaValue::Replace(values) => values
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, v)| **v != 0.0)
|
||||
.map(|(i, v)| SparseEntry {
|
||||
index: i as u32,
|
||||
value: *v,
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
to_value(&ops).map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
|
||||
}
|
||||
|
||||
/// Serialize to bytes
|
||||
#[wasm_bindgen(js_name = toBytes)]
|
||||
pub fn to_bytes(&self) -> Result<Uint8Array, JsValue> {
|
||||
let encoding = HybridEncoding::default();
|
||||
let bytes = encoding
|
||||
.encode(&self.inner)
|
||||
.map_err(|e| JsValue::from_str(&format!("Encoding error: {}", e)))?;
|
||||
|
||||
Ok(Uint8Array::from(&bytes[..]))
|
||||
}
|
||||
|
||||
/// Deserialize from bytes
|
||||
#[wasm_bindgen(js_name = fromBytes)]
|
||||
pub fn from_bytes(bytes: Uint8Array) -> Result<JsDelta, JsValue> {
|
||||
let data = bytes.to_vec();
|
||||
let encoding = HybridEncoding::default();
|
||||
let inner = encoding
|
||||
.decode(&data)
|
||||
.map_err(|e| JsValue::from_str(&format!("Decoding error: {}", e)))?;
|
||||
|
||||
Ok(JsDelta { inner })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SparseEntry {
|
||||
index: u32,
|
||||
value: f32,
|
||||
}
|
||||
|
||||
/// Main delta engine for vector operations
|
||||
#[wasm_bindgen]
|
||||
pub struct DeltaEngine {
|
||||
dimensions: usize,
|
||||
sparsity_threshold: f32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl DeltaEngine {
|
||||
/// Create a new delta engine
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(dimensions: usize) -> DeltaEngine {
|
||||
DeltaEngine {
|
||||
dimensions,
|
||||
sparsity_threshold: 0.7,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set sparsity threshold (0.0 to 1.0)
|
||||
#[wasm_bindgen(js_name = setSparsityThreshold)]
|
||||
pub fn set_sparsity_threshold(&mut self, threshold: f32) {
|
||||
self.sparsity_threshold = threshold.clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
/// Capture delta between two vectors
|
||||
pub fn capture(
|
||||
&self,
|
||||
old_vec: Float32Array,
|
||||
new_vec: Float32Array,
|
||||
) -> Result<JsDelta, JsValue> {
|
||||
if old_vec.length() != new_vec.length() {
|
||||
return Err(JsValue::from_str("Vectors must have same length"));
|
||||
}
|
||||
|
||||
if old_vec.length() as usize != self.dimensions {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Vector length {} doesn't match engine dimensions {}",
|
||||
old_vec.length(),
|
||||
self.dimensions
|
||||
)));
|
||||
}
|
||||
|
||||
let old: Vec<f32> = old_vec.to_vec();
|
||||
let new: Vec<f32> = new_vec.to_vec();
|
||||
|
||||
let inner = VectorDelta::compute(&old, &new);
|
||||
|
||||
Ok(JsDelta { inner })
|
||||
}
|
||||
|
||||
/// Apply delta to a vector in-place
|
||||
pub fn apply(&self, vec: Float32Array, delta: &JsDelta) -> Result<(), JsValue> {
|
||||
if vec.length() as usize != self.dimensions {
|
||||
return Err(JsValue::from_str("Vector length mismatch"));
|
||||
}
|
||||
|
||||
let mut data: Vec<f32> = vec.to_vec();
|
||||
delta
|
||||
.inner
|
||||
.apply(&mut data)
|
||||
.map_err(|e| JsValue::from_str(&format!("Apply error: {}", e)))?;
|
||||
|
||||
// Copy back to Float32Array
|
||||
vec.copy_from(&data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply delta and return new vector
|
||||
#[wasm_bindgen(js_name = applyClone)]
|
||||
pub fn apply_clone(&self, vec: Float32Array, delta: &JsDelta) -> Result<Float32Array, JsValue> {
|
||||
let mut data: Vec<f32> = vec.to_vec();
|
||||
delta
|
||||
.inner
|
||||
.apply(&mut data)
|
||||
.map_err(|e| JsValue::from_str(&format!("Apply error: {}", e)))?;
|
||||
|
||||
Ok(Float32Array::from(&data[..]))
|
||||
}
|
||||
|
||||
/// Create delta from sparse entries
|
||||
#[wasm_bindgen(js_name = fromSparse)]
|
||||
pub fn from_sparse(&self, entries: JsValue) -> Result<JsDelta, JsValue> {
|
||||
let sparse: Vec<SparseEntry> =
|
||||
from_value(entries).map_err(|e| JsValue::from_str(&format!("Parse error: {}", e)))?;
|
||||
|
||||
let ops: smallvec::SmallVec<[DeltaOp<f32>; 8]> = sparse
|
||||
.into_iter()
|
||||
.map(|e| DeltaOp::new(e.index, e.value))
|
||||
.collect();
|
||||
|
||||
let inner = VectorDelta::from_sparse(ops, self.dimensions);
|
||||
|
||||
Ok(JsDelta { inner })
|
||||
}
|
||||
|
||||
/// Create delta from dense array
|
||||
#[wasm_bindgen(js_name = fromDense)]
|
||||
pub fn from_dense(&self, values: Float32Array) -> Result<JsDelta, JsValue> {
|
||||
if values.length() as usize != self.dimensions {
|
||||
return Err(JsValue::from_str("Values length doesn't match dimensions"));
|
||||
}
|
||||
|
||||
let inner = VectorDelta::from_dense(values.to_vec());
|
||||
|
||||
Ok(JsDelta { inner })
|
||||
}
|
||||
|
||||
/// Create identity (no change) delta
|
||||
#[wasm_bindgen(js_name = identity)]
|
||||
pub fn identity(&self) -> JsDelta {
|
||||
JsDelta {
|
||||
inner: VectorDelta::new(self.dimensions),
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch capture deltas for multiple vector pairs
|
||||
#[wasm_bindgen(js_name = captureBatch)]
|
||||
pub fn capture_batch(
|
||||
&self,
|
||||
old_vecs: JsValue,
|
||||
new_vecs: JsValue,
|
||||
) -> Result<js_sys::Array, JsValue> {
|
||||
let old_array: js_sys::Array = old_vecs
|
||||
.dyn_into()
|
||||
.map_err(|_| JsValue::from_str("old_vecs must be array"))?;
|
||||
let new_array: js_sys::Array = new_vecs
|
||||
.dyn_into()
|
||||
.map_err(|_| JsValue::from_str("new_vecs must be array"))?;
|
||||
|
||||
if old_array.length() != new_array.length() {
|
||||
return Err(JsValue::from_str("Arrays must have same length"));
|
||||
}
|
||||
|
||||
let result = js_sys::Array::new();
|
||||
|
||||
for i in 0..old_array.length() {
|
||||
let old_vec: Float32Array = old_array
|
||||
.get(i)
|
||||
.dyn_into()
|
||||
.map_err(|_| JsValue::from_str("Expected Float32Array"))?;
|
||||
let new_vec: Float32Array = new_array
|
||||
.get(i)
|
||||
.dyn_into()
|
||||
.map_err(|_| JsValue::from_str("Expected Float32Array"))?;
|
||||
|
||||
let delta = self.capture(old_vec, new_vec)?;
|
||||
result.push(&delta.into());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Compose two deltas into one
|
||||
/// For composing multiple deltas, call this method repeatedly
|
||||
#[wasm_bindgen(js_name = composeTwo)]
|
||||
pub fn compose_two(&self, first: &JsDelta, second: &JsDelta) -> JsDelta {
|
||||
let result = first.inner.clone().compose(second.inner.clone());
|
||||
JsDelta { inner: result }
|
||||
}
|
||||
}
|
||||
|
||||
/// Delta stream for event sourcing
|
||||
#[wasm_bindgen]
|
||||
pub struct JsDeltaStream {
|
||||
inner: DeltaStream<VectorDelta>,
|
||||
dimensions: usize,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl JsDeltaStream {
|
||||
/// Create a new delta stream
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(dimensions: usize) -> JsDeltaStream {
|
||||
JsDeltaStream {
|
||||
inner: DeltaStream::for_vectors(dimensions),
|
||||
dimensions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Push a delta to the stream
|
||||
pub fn push(&mut self, delta: &JsDelta) {
|
||||
self.inner.push(delta.inner.clone());
|
||||
}
|
||||
|
||||
/// Get the current sequence number
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn sequence(&self) -> u32 {
|
||||
self.inner.sequence() as u32
|
||||
}
|
||||
|
||||
/// Get the number of deltas
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn length(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
/// Replay from initial state
|
||||
pub fn replay(&self, initial: Float32Array) -> Result<Float32Array, JsValue> {
|
||||
let init: Vec<f32> = initial.to_vec();
|
||||
let result = self
|
||||
.inner
|
||||
.replay(init)
|
||||
.map_err(|e| JsValue::from_str(&format!("Replay error: {}", e)))?;
|
||||
|
||||
Ok(Float32Array::from(&result[..]))
|
||||
}
|
||||
|
||||
/// Create a checkpoint
|
||||
#[wasm_bindgen(js_name = createCheckpoint)]
|
||||
pub fn create_checkpoint(&mut self, value: Float32Array) {
|
||||
self.inner.create_checkpoint(value.to_vec());
|
||||
}
|
||||
|
||||
/// Get number of checkpoints
|
||||
#[wasm_bindgen(getter, js_name = checkpointCount)]
|
||||
pub fn checkpoint_count(&self) -> usize {
|
||||
self.inner.checkpoint_count()
|
||||
}
|
||||
|
||||
/// Replay from checkpoint
|
||||
#[wasm_bindgen(js_name = replayFromCheckpoint)]
|
||||
pub fn replay_from_checkpoint(&self, checkpoint_idx: usize) -> Result<Float32Array, JsValue> {
|
||||
let result = self
|
||||
.inner
|
||||
.replay_from_checkpoint(checkpoint_idx)
|
||||
.ok_or_else(|| JsValue::from_str("Checkpoint index out of bounds"))?
|
||||
.map_err(|e| JsValue::from_str(&format!("Replay error: {:?}", e)))?;
|
||||
|
||||
Ok(Float32Array::from(&result[..]))
|
||||
}
|
||||
|
||||
/// Compact the stream
|
||||
pub fn compact(&mut self) -> usize {
|
||||
self.inner.compact().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Clear all deltas
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Delta window for time-bounded aggregation
|
||||
#[wasm_bindgen]
|
||||
pub struct JsDeltaWindow {
|
||||
inner: DeltaWindow<VectorDelta>,
|
||||
dimensions: usize,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl JsDeltaWindow {
|
||||
/// Create a tumbling window (size in milliseconds)
|
||||
#[wasm_bindgen(js_name = tumbling)]
|
||||
pub fn tumbling(dimensions: usize, size_ms: u32) -> JsDeltaWindow {
|
||||
let size_ns = (size_ms as u64) * 1_000_000;
|
||||
JsDeltaWindow {
|
||||
inner: DeltaWindow::tumbling(size_ns),
|
||||
dimensions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a sliding window
|
||||
#[wasm_bindgen(js_name = sliding)]
|
||||
pub fn sliding(dimensions: usize, size_ms: u32, slide_ms: u32) -> JsDeltaWindow {
|
||||
let size_ns = (size_ms as u64) * 1_000_000;
|
||||
let slide_ns = (slide_ms as u64) * 1_000_000;
|
||||
JsDeltaWindow {
|
||||
inner: DeltaWindow::sliding(size_ns, slide_ns),
|
||||
dimensions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a count-based window
|
||||
#[wasm_bindgen(js_name = countBased)]
|
||||
pub fn count_based(dimensions: usize, count: usize) -> JsDeltaWindow {
|
||||
JsDeltaWindow {
|
||||
inner: DeltaWindow::count_based(count),
|
||||
dimensions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a delta with timestamp (milliseconds)
|
||||
pub fn add(&mut self, delta: &JsDelta, timestamp_ms: f64) {
|
||||
let timestamp_ns = (timestamp_ms * 1_000_000.0) as u64;
|
||||
self.inner.add(delta.inner.clone(), timestamp_ns);
|
||||
}
|
||||
|
||||
/// Check if window is complete
|
||||
#[wasm_bindgen(js_name = isComplete)]
|
||||
pub fn is_complete(&self, current_ms: f64) -> bool {
|
||||
let current_ns = (current_ms * 1_000_000.0) as u64;
|
||||
self.inner.is_complete(current_ns)
|
||||
}
|
||||
|
||||
/// Emit aggregated window result
|
||||
pub fn emit(&mut self) -> Option<JsDelta> {
|
||||
self.inner.emit().map(|r| JsDelta { inner: r.delta })
|
||||
}
|
||||
|
||||
/// Get number of entries in window
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn length(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
/// Clear the window
|
||||
pub fn clear(&mut self) {
|
||||
self.inner.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
wasm_bindgen_test_configure!(run_in_browser);
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_version() {
|
||||
assert!(!version().is_empty());
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_delta_engine_capture() {
|
||||
let engine = DeltaEngine::new(4);
|
||||
|
||||
let old = Float32Array::from(&[1.0f32, 2.0, 3.0, 4.0][..]);
|
||||
let new = Float32Array::from(&[1.5f32, 2.0, 3.5, 4.0][..]);
|
||||
|
||||
let delta = engine.capture(old, new).unwrap();
|
||||
|
||||
assert!(!delta.is_identity());
|
||||
assert_eq!(delta.dimensions(), 4);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_delta_apply() {
|
||||
let engine = DeltaEngine::new(3);
|
||||
|
||||
let old = Float32Array::from(&[1.0f32, 2.0, 3.0][..]);
|
||||
let new = Float32Array::from(&[2.0f32, 2.0, 4.0][..]);
|
||||
|
||||
let delta = engine.capture(old.clone(), new.clone()).unwrap();
|
||||
|
||||
let mut test_vec = Float32Array::from(&[1.0f32, 2.0, 3.0][..]);
|
||||
engine.apply(test_vec.clone(), &delta).unwrap();
|
||||
|
||||
// Note: can't easily verify Float32Array equality in WASM tests
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_identity_delta() {
|
||||
let engine = DeltaEngine::new(10);
|
||||
let delta = engine.identity();
|
||||
|
||||
assert!(delta.is_identity());
|
||||
assert_eq!(delta.sparsity(), 1.0);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_delta_compose() {
|
||||
let engine = DeltaEngine::new(3);
|
||||
|
||||
let d1 = engine
|
||||
.from_dense(Float32Array::from(&[1.0f32, 0.0, 0.0][..]))
|
||||
.unwrap();
|
||||
let d2 = engine
|
||||
.from_dense(Float32Array::from(&[0.0f32, 1.0, 0.0][..]))
|
||||
.unwrap();
|
||||
|
||||
let composed = d1.compose(&d2);
|
||||
assert!(!composed.is_identity());
|
||||
}
|
||||
}
|
||||
372
vendor/ruvector/crates/ruvector-delta-wasm/src/memory.rs
vendored
Normal file
372
vendor/ruvector/crates/ruvector-delta-wasm/src/memory.rs
vendored
Normal file
@@ -0,0 +1,372 @@
|
||||
//! Shared memory management for zero-copy operations
|
||||
//!
|
||||
//! Provides shared memory buffers for efficient delta operations
|
||||
//! without copying data between WASM and JavaScript.
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Maximum size for shared memory buffers (256 MB)
|
||||
const MAX_BUFFER_SIZE: usize = 256 * 1024 * 1024;
|
||||
|
||||
/// Shared memory buffer for vector operations
|
||||
#[wasm_bindgen]
|
||||
pub struct SharedBuffer {
|
||||
data: Arc<RwLock<Vec<f32>>>,
|
||||
dimensions: usize,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl SharedBuffer {
|
||||
/// Create a new shared buffer with given dimensions
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(dimensions: usize) -> Result<SharedBuffer, JsValue> {
|
||||
if dimensions == 0 {
|
||||
return Err(JsValue::from_str("Dimensions must be > 0"));
|
||||
}
|
||||
|
||||
if dimensions * 4 > MAX_BUFFER_SIZE {
|
||||
return Err(JsValue::from_str(&format!(
|
||||
"Buffer size exceeds maximum: {} > {}",
|
||||
dimensions * 4,
|
||||
MAX_BUFFER_SIZE
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(SharedBuffer {
|
||||
data: Arc::new(RwLock::new(vec![0.0; dimensions])),
|
||||
dimensions,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create from existing data
|
||||
#[wasm_bindgen(js_name = fromData)]
|
||||
pub fn from_data(data: js_sys::Float32Array) -> Result<SharedBuffer, JsValue> {
|
||||
let dimensions = data.length() as usize;
|
||||
|
||||
if dimensions == 0 {
|
||||
return Err(JsValue::from_str("Data cannot be empty"));
|
||||
}
|
||||
|
||||
if dimensions * 4 > MAX_BUFFER_SIZE {
|
||||
return Err(JsValue::from_str("Data exceeds maximum buffer size"));
|
||||
}
|
||||
|
||||
Ok(SharedBuffer {
|
||||
data: Arc::new(RwLock::new(data.to_vec())),
|
||||
dimensions,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get dimensions
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn dimensions(&self) -> usize {
|
||||
self.dimensions
|
||||
}
|
||||
|
||||
/// Get byte size
|
||||
#[wasm_bindgen(getter, js_name = byteSize)]
|
||||
pub fn byte_size(&self) -> usize {
|
||||
self.dimensions * 4
|
||||
}
|
||||
|
||||
/// Copy data to a Float32Array
|
||||
#[wasm_bindgen(js_name = toFloat32Array)]
|
||||
pub fn to_float32_array(&self) -> js_sys::Float32Array {
|
||||
let data = self.data.read();
|
||||
js_sys::Float32Array::from(&data[..])
|
||||
}
|
||||
|
||||
/// Copy data from a Float32Array
|
||||
#[wasm_bindgen(js_name = fromFloat32Array)]
|
||||
pub fn from_float32_array(&self, arr: js_sys::Float32Array) -> Result<(), JsValue> {
|
||||
if arr.length() as usize != self.dimensions {
|
||||
return Err(JsValue::from_str("Array length doesn't match dimensions"));
|
||||
}
|
||||
|
||||
let mut data = self.data.write();
|
||||
arr.copy_to(&mut data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get value at index
|
||||
pub fn get(&self, index: usize) -> Result<f32, JsValue> {
|
||||
if index >= self.dimensions {
|
||||
return Err(JsValue::from_str("Index out of bounds"));
|
||||
}
|
||||
|
||||
let data = self.data.read();
|
||||
Ok(data[index])
|
||||
}
|
||||
|
||||
/// Set value at index
|
||||
pub fn set(&self, index: usize, value: f32) -> Result<(), JsValue> {
|
||||
if index >= self.dimensions {
|
||||
return Err(JsValue::from_str("Index out of bounds"));
|
||||
}
|
||||
|
||||
let mut data = self.data.write();
|
||||
data[index] = value;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fill with a value
|
||||
pub fn fill(&self, value: f32) {
|
||||
let mut data = self.data.write();
|
||||
data.fill(value);
|
||||
}
|
||||
|
||||
/// Reset to zeros
|
||||
pub fn zero(&self) {
|
||||
self.fill(0.0);
|
||||
}
|
||||
|
||||
/// Clone the buffer
|
||||
#[wasm_bindgen(js_name = clone)]
|
||||
pub fn clone_buffer(&self) -> SharedBuffer {
|
||||
let data = self.data.read().clone();
|
||||
SharedBuffer {
|
||||
data: Arc::new(RwLock::new(data)),
|
||||
dimensions: self.dimensions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add another buffer in-place
|
||||
#[wasm_bindgen(js_name = addAssign)]
|
||||
pub fn add_assign(&self, other: &SharedBuffer) -> Result<(), JsValue> {
|
||||
if self.dimensions != other.dimensions {
|
||||
return Err(JsValue::from_str("Dimension mismatch"));
|
||||
}
|
||||
|
||||
let mut self_data = self.data.write();
|
||||
let other_data = other.data.read();
|
||||
|
||||
crate::simd::simd_add_assign(&mut self_data, &other_data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Subtract another buffer in-place
|
||||
#[wasm_bindgen(js_name = subAssign)]
|
||||
pub fn sub_assign(&self, other: &SharedBuffer) -> Result<(), JsValue> {
|
||||
if self.dimensions != other.dimensions {
|
||||
return Err(JsValue::from_str("Dimension mismatch"));
|
||||
}
|
||||
|
||||
let mut self_data = self.data.write();
|
||||
let other_data = other.data.read();
|
||||
|
||||
crate::simd::simd_sub_assign(&mut self_data, &other_data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Scale in-place
|
||||
pub fn scale(&self, factor: f32) {
|
||||
let mut data = self.data.write();
|
||||
crate::simd::simd_scale(&mut data, factor);
|
||||
}
|
||||
|
||||
/// Compute dot product with another buffer
|
||||
pub fn dot(&self, other: &SharedBuffer) -> Result<f32, JsValue> {
|
||||
if self.dimensions != other.dimensions {
|
||||
return Err(JsValue::from_str("Dimension mismatch"));
|
||||
}
|
||||
|
||||
let self_data = self.data.read();
|
||||
let other_data = other.data.read();
|
||||
|
||||
Ok(crate::simd::simd_dot(&self_data, &other_data))
|
||||
}
|
||||
|
||||
/// Compute L2 norm
|
||||
#[wasm_bindgen(js_name = l2Norm)]
|
||||
pub fn l2_norm(&self) -> f32 {
|
||||
let data = self.data.read();
|
||||
crate::simd::simd_l2_norm_squared(&data).sqrt()
|
||||
}
|
||||
|
||||
/// Count non-zero elements
|
||||
#[wasm_bindgen(js_name = countNonzero)]
|
||||
pub fn count_nonzero(&self, epsilon: f32) -> usize {
|
||||
let data = self.data.read();
|
||||
crate::simd::simd_count_nonzero(&data, epsilon)
|
||||
}
|
||||
|
||||
/// Clamp values to range
|
||||
pub fn clamp(&self, min: f32, max: f32) {
|
||||
let mut data = self.data.write();
|
||||
crate::simd::simd_clamp(&mut data, min, max);
|
||||
}
|
||||
|
||||
/// Compute element-wise absolute value
|
||||
pub fn abs(&self) {
|
||||
let mut data = self.data.write();
|
||||
crate::simd::simd_abs(&mut data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Pool of shared buffers for efficient reuse
|
||||
#[wasm_bindgen]
|
||||
pub struct BufferPool {
|
||||
buffers: Vec<SharedBuffer>,
|
||||
dimensions: usize,
|
||||
available: Vec<usize>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl BufferPool {
|
||||
/// Create a new buffer pool
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(dimensions: usize, initial_count: usize) -> Result<BufferPool, JsValue> {
|
||||
let mut buffers = Vec::with_capacity(initial_count);
|
||||
let mut available = Vec::with_capacity(initial_count);
|
||||
|
||||
for i in 0..initial_count {
|
||||
buffers.push(SharedBuffer::new(dimensions)?);
|
||||
available.push(i);
|
||||
}
|
||||
|
||||
Ok(BufferPool {
|
||||
buffers,
|
||||
dimensions,
|
||||
available,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the pool dimensions
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn dimensions(&self) -> usize {
|
||||
self.dimensions
|
||||
}
|
||||
|
||||
/// Get number of available buffers
|
||||
#[wasm_bindgen(getter, js_name = availableCount)]
|
||||
pub fn available_count(&self) -> usize {
|
||||
self.available.len()
|
||||
}
|
||||
|
||||
/// Get total number of buffers
|
||||
#[wasm_bindgen(getter, js_name = totalCount)]
|
||||
pub fn total_count(&self) -> usize {
|
||||
self.buffers.len()
|
||||
}
|
||||
|
||||
/// Acquire a buffer from the pool
|
||||
pub fn acquire(&mut self) -> Result<SharedBuffer, JsValue> {
|
||||
if let Some(idx) = self.available.pop() {
|
||||
// Clone the buffer for exclusive use
|
||||
Ok(self.buffers[idx].clone_buffer())
|
||||
} else {
|
||||
// Pool exhausted, create new buffer
|
||||
let buffer = SharedBuffer::new(self.dimensions)?;
|
||||
self.buffers.push(buffer.clone_buffer());
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Release a buffer back to the pool (just tracks availability)
|
||||
pub fn release(&mut self, _buffer: SharedBuffer) {
|
||||
// In WASM, we can't actually return ownership
|
||||
// The buffer will be dropped when JS releases it
|
||||
// This method is for tracking purposes
|
||||
}
|
||||
|
||||
/// Pre-allocate more buffers
|
||||
#[wasm_bindgen(js_name = grow)]
|
||||
pub fn grow(&mut self, count: usize) -> Result<(), JsValue> {
|
||||
let start = self.buffers.len();
|
||||
for i in 0..count {
|
||||
self.buffers.push(SharedBuffer::new(self.dimensions)?);
|
||||
self.available.push(start + i);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear the pool
|
||||
pub fn clear(&mut self) {
|
||||
self.buffers.clear();
|
||||
self.available.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Memory statistics
|
||||
#[wasm_bindgen]
|
||||
pub struct MemoryStats {
|
||||
/// Total allocated bytes
|
||||
pub total_bytes: usize,
|
||||
/// Number of buffers
|
||||
pub buffer_count: usize,
|
||||
/// Average buffer size
|
||||
pub avg_buffer_size: usize,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl MemoryStats {
|
||||
/// Create from pool
|
||||
#[wasm_bindgen(js_name = fromPool)]
|
||||
pub fn from_pool(pool: &BufferPool) -> MemoryStats {
|
||||
let total_bytes = pool.buffers.len() * pool.dimensions * 4;
|
||||
MemoryStats {
|
||||
total_bytes,
|
||||
buffer_count: pool.buffers.len(),
|
||||
avg_buffer_size: if pool.buffers.is_empty() {
|
||||
0
|
||||
} else {
|
||||
total_bytes / pool.buffers.len()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_shared_buffer_creation() {
|
||||
let buffer = SharedBuffer::new(100).unwrap();
|
||||
assert_eq!(buffer.dimensions(), 100);
|
||||
assert_eq!(buffer.byte_size(), 400);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_operations() {
|
||||
let buffer = SharedBuffer::new(4).unwrap();
|
||||
|
||||
buffer.set(0, 1.0).unwrap();
|
||||
buffer.set(1, 2.0).unwrap();
|
||||
buffer.set(2, 3.0).unwrap();
|
||||
buffer.set(3, 4.0).unwrap();
|
||||
|
||||
assert!((buffer.get(0).unwrap() - 1.0).abs() < 1e-6);
|
||||
assert!((buffer.get(3).unwrap() - 4.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_math() {
|
||||
let a = SharedBuffer::new(4).unwrap();
|
||||
let b = SharedBuffer::new(4).unwrap();
|
||||
|
||||
a.fill(1.0);
|
||||
b.fill(2.0);
|
||||
|
||||
a.add_assign(&b).unwrap();
|
||||
|
||||
assert!((a.get(0).unwrap() - 3.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_pool() {
|
||||
let mut pool = BufferPool::new(100, 5).unwrap();
|
||||
|
||||
assert_eq!(pool.available_count(), 5);
|
||||
assert_eq!(pool.total_count(), 5);
|
||||
|
||||
let _buf1 = pool.acquire().unwrap();
|
||||
let _buf2 = pool.acquire().unwrap();
|
||||
|
||||
assert_eq!(pool.available_count(), 3);
|
||||
}
|
||||
}
|
||||
364
vendor/ruvector/crates/ruvector-delta-wasm/src/simd.rs
vendored
Normal file
364
vendor/ruvector/crates/ruvector-delta-wasm/src/simd.rs
vendored
Normal file
@@ -0,0 +1,364 @@
|
||||
//! SIMD-accelerated operations for WASM
|
||||
//!
|
||||
//! Provides SIMD-optimized vector operations when wasm32-simd128 is available.
|
||||
|
||||
/// Check if SIMD is available at runtime
|
||||
pub fn simd_available() -> bool {
|
||||
#[cfg(target_feature = "simd128")]
|
||||
{
|
||||
true
|
||||
}
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// SIMD-accelerated vector addition (a += b)
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn simd_add_assign(a: &mut [f32], b: &[f32]) {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
assert_eq!(a.len(), b.len());
|
||||
|
||||
let chunks = a.len() / 4;
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let a_ptr = a.as_mut_ptr().add(offset);
|
||||
let b_ptr = b.as_ptr().add(offset);
|
||||
|
||||
let a_vec = v128_load(a_ptr as *const v128);
|
||||
let b_vec = v128_load(b_ptr as *const v128);
|
||||
let result = f32x4_add(a_vec, b_vec);
|
||||
|
||||
v128_store(a_ptr as *mut v128, result);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remainder
|
||||
for i in (chunks * 4)..a.len() {
|
||||
a[i] += b[i];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
pub fn simd_add_assign(a: &mut [f32], b: &[f32]) {
|
||||
for (av, bv) in a.iter_mut().zip(b.iter()) {
|
||||
*av += *bv;
|
||||
}
|
||||
}
|
||||
|
||||
/// SIMD-accelerated vector subtraction (a -= b)
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn simd_sub_assign(a: &mut [f32], b: &[f32]) {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
assert_eq!(a.len(), b.len());
|
||||
|
||||
let chunks = a.len() / 4;
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let a_ptr = a.as_mut_ptr().add(offset);
|
||||
let b_ptr = b.as_ptr().add(offset);
|
||||
|
||||
let a_vec = v128_load(a_ptr as *const v128);
|
||||
let b_vec = v128_load(b_ptr as *const v128);
|
||||
let result = f32x4_sub(a_vec, b_vec);
|
||||
|
||||
v128_store(a_ptr as *mut v128, result);
|
||||
}
|
||||
}
|
||||
|
||||
for i in (chunks * 4)..a.len() {
|
||||
a[i] -= b[i];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
pub fn simd_sub_assign(a: &mut [f32], b: &[f32]) {
|
||||
for (av, bv) in a.iter_mut().zip(b.iter()) {
|
||||
*av -= *bv;
|
||||
}
|
||||
}
|
||||
|
||||
/// SIMD-accelerated vector scaling (a *= scalar)
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn simd_scale(a: &mut [f32], scalar: f32) {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
let scalar_vec = f32x4_splat(scalar);
|
||||
let chunks = a.len() / 4;
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let a_ptr = a.as_mut_ptr().add(offset);
|
||||
let a_vec = v128_load(a_ptr as *const v128);
|
||||
let result = f32x4_mul(a_vec, scalar_vec);
|
||||
v128_store(a_ptr as *mut v128, result);
|
||||
}
|
||||
}
|
||||
|
||||
for i in (chunks * 4)..a.len() {
|
||||
a[i] *= scalar;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
pub fn simd_scale(a: &mut [f32], scalar: f32) {
|
||||
for v in a.iter_mut() {
|
||||
*v *= scalar;
|
||||
}
|
||||
}
|
||||
|
||||
/// SIMD-accelerated dot product
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn simd_dot(a: &[f32], b: &[f32]) -> f32 {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
assert_eq!(a.len(), b.len());
|
||||
|
||||
let chunks = a.len() / 4;
|
||||
let mut sum_vec = f32x4_splat(0.0);
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let a_vec = v128_load(a.as_ptr().add(offset) as *const v128);
|
||||
let b_vec = v128_load(b.as_ptr().add(offset) as *const v128);
|
||||
let prod = f32x4_mul(a_vec, b_vec);
|
||||
sum_vec = f32x4_add(sum_vec, prod);
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal sum
|
||||
let sum_array: [f32; 4] = unsafe { core::mem::transmute(sum_vec) };
|
||||
let mut sum = sum_array[0] + sum_array[1] + sum_array[2] + sum_array[3];
|
||||
|
||||
// Handle remainder
|
||||
for i in (chunks * 4)..a.len() {
|
||||
sum += a[i] * b[i];
|
||||
}
|
||||
|
||||
sum
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
pub fn simd_dot(a: &[f32], b: &[f32]) -> f32 {
|
||||
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
|
||||
}
|
||||
|
||||
/// SIMD-accelerated L2 norm squared
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn simd_l2_norm_squared(a: &[f32]) -> f32 {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
let chunks = a.len() / 4;
|
||||
let mut sum_vec = f32x4_splat(0.0);
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let a_vec = v128_load(a.as_ptr().add(offset) as *const v128);
|
||||
let sq = f32x4_mul(a_vec, a_vec);
|
||||
sum_vec = f32x4_add(sum_vec, sq);
|
||||
}
|
||||
}
|
||||
|
||||
let sum_array: [f32; 4] = unsafe { core::mem::transmute(sum_vec) };
|
||||
let mut sum = sum_array[0] + sum_array[1] + sum_array[2] + sum_array[3];
|
||||
|
||||
for i in (chunks * 4)..a.len() {
|
||||
sum += a[i] * a[i];
|
||||
}
|
||||
|
||||
sum
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
pub fn simd_l2_norm_squared(a: &[f32]) -> f32 {
|
||||
a.iter().map(|x| x * x).sum()
|
||||
}
|
||||
|
||||
/// SIMD-accelerated element-wise difference (result = a - b)
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn simd_diff(a: &[f32], b: &[f32], result: &mut [f32]) {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
assert_eq!(a.len(), b.len());
|
||||
assert_eq!(a.len(), result.len());
|
||||
|
||||
let chunks = a.len() / 4;
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let a_vec = v128_load(a.as_ptr().add(offset) as *const v128);
|
||||
let b_vec = v128_load(b.as_ptr().add(offset) as *const v128);
|
||||
let diff = f32x4_sub(a_vec, b_vec);
|
||||
v128_store(result.as_mut_ptr().add(offset) as *mut v128, diff);
|
||||
}
|
||||
}
|
||||
|
||||
for i in (chunks * 4)..a.len() {
|
||||
result[i] = a[i] - b[i];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
pub fn simd_diff(a: &[f32], b: &[f32], result: &mut [f32]) {
|
||||
for i in 0..a.len() {
|
||||
result[i] = a[i] - b[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// SIMD-accelerated element-wise absolute value
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn simd_abs(a: &mut [f32]) {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
let chunks = a.len() / 4;
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let a_ptr = a.as_mut_ptr().add(offset);
|
||||
let a_vec = v128_load(a_ptr as *const v128);
|
||||
let result = f32x4_abs(a_vec);
|
||||
v128_store(a_ptr as *mut v128, result);
|
||||
}
|
||||
}
|
||||
|
||||
for i in (chunks * 4)..a.len() {
|
||||
a[i] = a[i].abs();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
pub fn simd_abs(a: &mut [f32]) {
|
||||
for v in a.iter_mut() {
|
||||
*v = v.abs();
|
||||
}
|
||||
}
|
||||
|
||||
/// SIMD-accelerated clamp
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn simd_clamp(a: &mut [f32], min: f32, max: f32) {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
let min_vec = f32x4_splat(min);
|
||||
let max_vec = f32x4_splat(max);
|
||||
let chunks = a.len() / 4;
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let a_ptr = a.as_mut_ptr().add(offset);
|
||||
let a_vec = v128_load(a_ptr as *const v128);
|
||||
let clamped = f32x4_max(f32x4_min(a_vec, max_vec), min_vec);
|
||||
v128_store(a_ptr as *mut v128, clamped);
|
||||
}
|
||||
}
|
||||
|
||||
for i in (chunks * 4)..a.len() {
|
||||
a[i] = a[i].clamp(min, max);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
pub fn simd_clamp(a: &mut [f32], min: f32, max: f32) {
|
||||
for v in a.iter_mut() {
|
||||
*v = v.clamp(min, max);
|
||||
}
|
||||
}
|
||||
|
||||
/// Count non-zero elements with SIMD acceleration
|
||||
#[cfg(target_feature = "simd128")]
|
||||
pub fn simd_count_nonzero(a: &[f32], epsilon: f32) -> usize {
|
||||
use core::arch::wasm32::*;
|
||||
|
||||
let eps_vec = f32x4_splat(epsilon);
|
||||
let neg_eps_vec = f32x4_splat(-epsilon);
|
||||
let chunks = a.len() / 4;
|
||||
let mut count = 0usize;
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 4;
|
||||
unsafe {
|
||||
let a_vec = v128_load(a.as_ptr().add(offset) as *const v128);
|
||||
|
||||
// Check if |a| > epsilon
|
||||
let gt_eps = f32x4_gt(a_vec, eps_vec);
|
||||
let lt_neg_eps = f32x4_lt(a_vec, neg_eps_vec);
|
||||
let nonzero = v128_or(gt_eps, lt_neg_eps);
|
||||
|
||||
// Convert to bitmask and count
|
||||
let mask = i32x4_bitmask(nonzero) as u8;
|
||||
count += mask.count_ones() as usize;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remainder
|
||||
for i in (chunks * 4)..a.len() {
|
||||
if a[i].abs() > epsilon {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
#[cfg(not(target_feature = "simd128"))]
|
||||
pub fn simd_count_nonzero(a: &[f32], epsilon: f32) -> usize {
|
||||
a.iter().filter(|v| v.abs() > epsilon).count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add_assign() {
|
||||
let mut a = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
|
||||
let b = vec![1.0f32, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
|
||||
|
||||
simd_add_assign(&mut a, &b);
|
||||
|
||||
assert!((a[0] - 2.0).abs() < 1e-6);
|
||||
assert!((a[7] - 9.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dot() {
|
||||
let a = vec![1.0f32, 2.0, 3.0, 4.0];
|
||||
let b = vec![1.0f32, 1.0, 1.0, 1.0];
|
||||
|
||||
let result = simd_dot(&a, &b);
|
||||
assert!((result - 10.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_l2_norm_squared() {
|
||||
let a = vec![3.0f32, 4.0];
|
||||
let result = simd_l2_norm_squared(&a);
|
||||
assert!((result - 25.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scale() {
|
||||
let mut a = vec![1.0f32, 2.0, 3.0, 4.0];
|
||||
simd_scale(&mut a, 2.0);
|
||||
|
||||
assert!((a[0] - 2.0).abs() < 1e-6);
|
||||
assert!((a[3] - 8.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_nonzero() {
|
||||
let a = vec![1.0f32, 0.0, 2.0, 0.0, 3.0, 0.0, 4.0, 0.0];
|
||||
let count = simd_count_nonzero(&a, 1e-7);
|
||||
assert_eq!(count, 4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user