Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
533
vendor/ruvector/docs/optimization/plaid-optimization-guide.md
vendored
Normal file
533
vendor/ruvector/docs/optimization/plaid-optimization-guide.md
vendored
Normal file
@@ -0,0 +1,533 @@
|
||||
# Plaid Performance Optimization Guide
|
||||
|
||||
**Quick Reference**: Code locations, issues, and fixes
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Critical Issues (Fix Immediately)
|
||||
|
||||
### 1. Memory Leak: Unbounded Embeddings Growth
|
||||
|
||||
**File**: `/home/user/ruvector/examples/edge/src/plaid/wasm.rs`
|
||||
|
||||
**Line 90-91**:
|
||||
```rust
|
||||
// ❌ CURRENT (LEAKS MEMORY)
|
||||
state.category_embeddings.push((category_key.clone(), embedding.clone()));
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- After 100k transactions: ~10MB leaked
|
||||
- Eventually crashes browser
|
||||
|
||||
**Fix Option 1 - HashMap Deduplication**:
|
||||
```rust
|
||||
// ✅ FIXED - Use HashMap in mod.rs:149
|
||||
// In mod.rs, change:
|
||||
pub category_embeddings: Vec<(String, Vec<f32>)>,
|
||||
// To:
|
||||
pub category_embeddings: HashMap<String, Vec<f32>>,
|
||||
|
||||
// In wasm.rs:90, change to:
|
||||
state.category_embeddings.insert(category_key.clone(), embedding);
|
||||
```
|
||||
|
||||
**Fix Option 2 - Circular Buffer**:
|
||||
```rust
|
||||
// ✅ FIXED - Limit size
|
||||
const MAX_EMBEDDINGS: usize = 10_000;
|
||||
|
||||
if state.category_embeddings.len() >= MAX_EMBEDDINGS {
|
||||
state.category_embeddings.remove(0);
|
||||
}
|
||||
state.category_embeddings.push((category_key.clone(), embedding));
|
||||
```
|
||||
|
||||
**Fix Option 3 - Remove Field**:
|
||||
```rust
|
||||
// ✅ BEST - Don't store separately, use HNSW index
|
||||
// Remove category_embeddings field entirely from FinancialLearningState
|
||||
// Retrieve from HNSW index when needed
|
||||
```
|
||||
|
||||
**Expected Result**: 90% memory reduction long-term
|
||||
|
||||
---
|
||||
|
||||
### 2. Cryptographic Weakness: Simplified SHA256
|
||||
|
||||
**File**: `/home/user/ruvector/examples/edge/src/plaid/zkproofs.rs`
|
||||
|
||||
**Lines 144-173**:
|
||||
```rust
|
||||
// ❌ CURRENT (NOT CRYPTOGRAPHICALLY SECURE)
|
||||
struct Sha256 {
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Sha256 {
|
||||
fn new() -> Self { Self { data: Vec::new() } }
|
||||
fn update(&mut self, data: &[u8]) { self.data.extend_from_slice(data); }
|
||||
fn finalize(self) -> [u8; 32] {
|
||||
// Simplified hash - NOT SECURE
|
||||
// ... lines 159-172
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- Not resistant to collision attacks
|
||||
- Unsuitable for ZK proofs
|
||||
- 8x slower than hardware SHA
|
||||
|
||||
**Fix**:
|
||||
```rust
|
||||
// ✅ FIXED - Use sha2 crate
|
||||
// Add to Cargo.toml:
|
||||
[dependencies]
|
||||
sha2 = "0.10"
|
||||
|
||||
// In zkproofs.rs, replace lines 144-173 with:
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
// Lines 117-121 become:
|
||||
let mut hasher = Sha256::new();
|
||||
Digest::update(&mut hasher, &value.to_le_bytes());
|
||||
Digest::update(&mut hasher, blinding);
|
||||
let hash = hasher.finalize();
|
||||
|
||||
// Same pattern for lines 300-304 (fiat_shamir_challenge)
|
||||
```
|
||||
|
||||
**Expected Result**: 8x faster + cryptographically secure
|
||||
|
||||
---
|
||||
|
||||
## 🟡 High-Impact Performance Fixes
|
||||
|
||||
### 3. Remove Unnecessary RwLock in WASM
|
||||
|
||||
**File**: `/home/user/ruvector/examples/edge/src/plaid/wasm.rs`
|
||||
|
||||
**Line 24**:
|
||||
```rust
|
||||
// ❌ CURRENT (10-20% overhead in single-threaded WASM)
|
||||
pub struct PlaidLocalLearner {
|
||||
state: Arc<RwLock<FinancialLearningState>>,
|
||||
hnsw_index: crate::WasmHnswIndex,
|
||||
spiking_net: crate::WasmSpikingNetwork,
|
||||
learning_rate: f64,
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```rust
|
||||
// ✅ FIXED - Direct ownership for WASM
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub struct PlaidLocalLearner {
|
||||
state: FinancialLearningState, // No Arc<RwLock<...>>
|
||||
hnsw_index: crate::WasmHnswIndex,
|
||||
spiking_net: crate::WasmSpikingNetwork,
|
||||
learning_rate: f64,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub struct PlaidLocalLearner {
|
||||
state: Arc<RwLock<FinancialLearningState>>, // Keep for native
|
||||
hnsw_index: crate::WasmHnswIndex,
|
||||
spiking_net: crate::WasmSpikingNetwork,
|
||||
learning_rate: f64,
|
||||
}
|
||||
|
||||
// Update all methods:
|
||||
// OLD: let mut state = self.state.write();
|
||||
// NEW: let state = &mut self.state;
|
||||
|
||||
// Example (line 78):
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn process_transactions(&mut self, transactions_json: &str) -> Result<JsValue, JsValue> {
|
||||
let transactions: Vec<Transaction> = serde_json::from_str(transactions_json)?;
|
||||
// Direct access to state
|
||||
for tx in &transactions {
|
||||
self.learn_pattern(&mut self.state, tx, &features);
|
||||
}
|
||||
self.state.version += 1;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Expected Result**: 1.2x speedup on all operations
|
||||
|
||||
---
|
||||
|
||||
### 4. Use Binary Serialization Instead of JSON
|
||||
|
||||
**File**: `/home/user/ruvector/examples/edge/src/plaid/wasm.rs`
|
||||
|
||||
**Lines 74-76, 120-122, 144-145** (multiple locations):
|
||||
```rust
|
||||
// ❌ CURRENT (Slow JSON parsing)
|
||||
pub fn process_transactions(&mut self, transactions_json: &str) -> Result<JsValue, JsValue> {
|
||||
let transactions: Vec<Transaction> = serde_json::from_str(transactions_json)?;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Fix Option 1 - Use serde_wasm_bindgen directly**:
|
||||
```rust
|
||||
// ✅ FIXED - Avoid JSON string intermediary
|
||||
pub fn process_transactions(&mut self, transactions: JsValue) -> Result<JsValue, JsValue> {
|
||||
let transactions: Vec<Transaction> = serde_wasm_bindgen::from_value(transactions)?;
|
||||
// ... process ...
|
||||
serde_wasm_bindgen::to_value(&insights)
|
||||
}
|
||||
|
||||
// JavaScript usage:
|
||||
// OLD: learner.processTransactions(JSON.stringify(transactions));
|
||||
// NEW: learner.processTransactions(transactions); // Direct array
|
||||
```
|
||||
|
||||
**Fix Option 2 - Binary format**:
|
||||
```rust
|
||||
// ✅ FIXED - Use bincode for bulk data
|
||||
#[wasm_bindgen(js_name = processTransactionsBinary)]
|
||||
pub fn process_transactions_binary(&mut self, data: &[u8]) -> Result<Vec<u8>, JsValue> {
|
||||
let transactions: Vec<Transaction> = bincode::deserialize(data)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))?;
|
||||
// ... process ...
|
||||
bincode::serialize(&insights)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
// JavaScript usage:
|
||||
const encoder = new BincodeEncoder();
|
||||
const data = encoder.encode(transactions);
|
||||
const result = learner.processTransactionsBinary(data);
|
||||
```
|
||||
|
||||
**Expected Result**: 2-5x faster API calls
|
||||
|
||||
---
|
||||
|
||||
### 5. Fixed-Size Embedding Arrays (No Heap Allocation)
|
||||
|
||||
**File**: `/home/user/ruvector/examples/edge/src/plaid/mod.rs`
|
||||
|
||||
**Lines 181-192**:
|
||||
```rust
|
||||
// ❌ CURRENT (3 heap allocations)
|
||||
pub fn to_embedding(&self) -> Vec<f32> {
|
||||
let mut vec = vec![
|
||||
self.amount_normalized,
|
||||
self.day_of_week / 7.0,
|
||||
self.day_of_month / 31.0,
|
||||
self.hour_of_day / 24.0,
|
||||
self.is_weekend,
|
||||
];
|
||||
vec.extend(&self.category_hash); // Allocation 1
|
||||
vec.extend(&self.merchant_hash); // Allocation 2
|
||||
vec
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```rust
|
||||
// ✅ FIXED - Stack allocation, SIMD-friendly
|
||||
pub fn to_embedding(&self) -> [f32; 21] { // Fixed size
|
||||
let mut vec = [0.0f32; 21];
|
||||
|
||||
// Direct assignment (no allocation)
|
||||
vec[0] = self.amount_normalized;
|
||||
vec[1] = self.day_of_week / 7.0;
|
||||
vec[2] = self.day_of_month / 31.0;
|
||||
vec[3] = self.hour_of_day / 24.0;
|
||||
vec[4] = self.is_weekend;
|
||||
|
||||
// SIMD-friendly copy
|
||||
vec[5..13].copy_from_slice(&self.category_hash);
|
||||
vec[13..21].copy_from_slice(&self.merchant_hash);
|
||||
|
||||
vec
|
||||
}
|
||||
```
|
||||
|
||||
**Expected Result**: 3x faster + no heap allocation
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Advanced Optimizations
|
||||
|
||||
### 6. Incremental State Serialization
|
||||
|
||||
**File**: `/home/user/ruvector/examples/edge/src/plaid/wasm.rs`
|
||||
|
||||
**Lines 64-67**:
|
||||
```rust
|
||||
// ❌ CURRENT (Serializes entire state, blocks UI)
|
||||
pub fn save_state(&self) -> Result<String, JsValue> {
|
||||
let state = self.state.read();
|
||||
serde_json::to_string(&*state)? // 10ms for 5MB state
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```rust
|
||||
// ✅ FIXED - Incremental saves
|
||||
// Add to FinancialLearningState (mod.rs):
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct FinancialLearningState {
|
||||
// ... existing fields ...
|
||||
|
||||
#[serde(skip)]
|
||||
pub dirty_patterns: HashSet<String>,
|
||||
#[serde(skip)]
|
||||
pub last_save_version: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct StateDelta {
|
||||
pub version: u64,
|
||||
pub changed_patterns: Vec<SpendingPattern>,
|
||||
pub new_q_values: HashMap<String, f64>,
|
||||
pub new_embeddings: Vec<(String, Vec<f32>)>,
|
||||
}
|
||||
|
||||
impl FinancialLearningState {
|
||||
pub fn get_delta(&self) -> StateDelta {
|
||||
StateDelta {
|
||||
version: self.version,
|
||||
changed_patterns: self.dirty_patterns.iter()
|
||||
.filter_map(|key| self.patterns.get(key).cloned())
|
||||
.collect(),
|
||||
new_q_values: self.q_values.iter()
|
||||
.filter(|(k, _)| !k.is_empty()) // Only changed
|
||||
.map(|(k, v)| (k.clone(), *v))
|
||||
.collect(),
|
||||
new_embeddings: vec![], // If fixed memory leak
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_dirty(&mut self, key: &str) {
|
||||
self.dirty_patterns.insert(key.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// In wasm.rs:
|
||||
pub fn save_state_incremental(&mut self) -> Result<String, JsValue> {
|
||||
let delta = self.state.get_delta();
|
||||
let json = serde_json::to_string(&delta)?;
|
||||
|
||||
self.state.dirty_patterns.clear();
|
||||
self.state.last_save_version = self.state.version;
|
||||
|
||||
Ok(json)
|
||||
}
|
||||
```
|
||||
|
||||
**Expected Result**: 10x faster saves (1ms vs 10ms)
|
||||
|
||||
---
|
||||
|
||||
### 7. Serialize HNSW Index (Avoid Rebuilding)
|
||||
|
||||
**File**: `/home/user/ruvector/examples/edge/src/plaid/wasm.rs`
|
||||
|
||||
**Lines 54-57**:
|
||||
```rust
|
||||
// ❌ CURRENT (Rebuilds HNSW on load - O(n log n))
|
||||
pub fn load_state(&mut self, json: &str) -> Result<(), JsValue> {
|
||||
let loaded: FinancialLearningState = serde_json::from_str(json)?;
|
||||
*self.state.write() = loaded;
|
||||
|
||||
// Rebuild index - SLOW for large datasets
|
||||
let state = self.state.read();
|
||||
for (id, embedding) in &state.category_embeddings {
|
||||
self.hnsw_index.insert(id, embedding.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```rust
|
||||
// ✅ FIXED - Serialize index directly
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct FullState {
|
||||
learning_state: FinancialLearningState,
|
||||
hnsw_index: Vec<u8>, // Serialized HNSW
|
||||
}
|
||||
|
||||
pub fn save_state(&self) -> Result<String, JsValue> {
|
||||
let full = FullState {
|
||||
learning_state: (*self.state).clone(),
|
||||
hnsw_index: self.hnsw_index.serialize(), // Must implement
|
||||
};
|
||||
serde_json::to_string(&full)
|
||||
.map_err(|e| JsValue::from_str(&e.to_string()))
|
||||
}
|
||||
|
||||
pub fn load_state(&mut self, json: &str) -> Result<(), JsValue> {
|
||||
let loaded: FullState = serde_json::from_str(json)?;
|
||||
|
||||
self.state = loaded.learning_state;
|
||||
self.hnsw_index = WasmHnswIndex::deserialize(&loaded.hnsw_index)?;
|
||||
|
||||
Ok(()) // No rebuild!
|
||||
}
|
||||
```
|
||||
|
||||
**Expected Result**: 50x faster loads (1ms vs 50ms for 10k items)
|
||||
|
||||
---
|
||||
|
||||
### 8. WASM SIMD for LSH Normalization
|
||||
|
||||
**File**: `/home/user/ruvector/examples/edge/src/plaid/mod.rs`
|
||||
|
||||
**Lines 233-234**:
|
||||
```rust
|
||||
// ❌ CURRENT (Scalar operations)
|
||||
let norm: f32 = hash.iter().map(|x| x * x).sum::<f32>().sqrt().max(1.0);
|
||||
hash.iter_mut().for_each(|x| *x /= norm);
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```rust
|
||||
// ✅ FIXED - WASM SIMD (requires nightly + feature flag)
|
||||
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
|
||||
use std::arch::wasm32::*;
|
||||
|
||||
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
|
||||
fn normalize_simd(hash: &mut [f32; 8]) {
|
||||
unsafe {
|
||||
// Load into SIMD register
|
||||
let vec1 = v128_load(&hash[0] as *const f32 as *const v128);
|
||||
let vec2 = v128_load(&hash[4] as *const f32 as *const v128);
|
||||
|
||||
// Compute squared values
|
||||
let sq1 = f32x4_mul(vec1, vec1);
|
||||
let sq2 = f32x4_mul(vec2, vec2);
|
||||
|
||||
// Sum all elements (horizontal add)
|
||||
let sum1 = f32x4_extract_lane::<0>(sq1) + f32x4_extract_lane::<1>(sq1) +
|
||||
f32x4_extract_lane::<2>(sq1) + f32x4_extract_lane::<3>(sq1);
|
||||
let sum2 = f32x4_extract_lane::<0>(sq2) + f32x4_extract_lane::<1>(sq2) +
|
||||
f32x4_extract_lane::<2>(sq2) + f32x4_extract_lane::<3>(sq2);
|
||||
|
||||
let norm = (sum1 + sum2).sqrt().max(1.0);
|
||||
|
||||
// Divide by norm
|
||||
let norm_vec = f32x4_splat(norm);
|
||||
let normalized1 = f32x4_div(vec1, norm_vec);
|
||||
let normalized2 = f32x4_div(vec2, norm_vec);
|
||||
|
||||
// Store back
|
||||
v128_store(&mut hash[0] as *mut f32 as *mut v128, normalized1);
|
||||
v128_store(&mut hash[4] as *mut f32 as *mut v128, normalized2);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))]
|
||||
fn normalize_simd(hash: &mut [f32; 8]) {
|
||||
// Fallback to scalar (lines 233-234)
|
||||
let norm: f32 = hash.iter().map(|x| x * x).sum::<f32>().sqrt().max(1.0);
|
||||
hash.iter_mut().for_each(|x| *x /= norm);
|
||||
}
|
||||
```
|
||||
|
||||
**Build with**:
|
||||
```bash
|
||||
RUSTFLAGS="-C target-feature=+simd128" wasm-pack build --target web
|
||||
```
|
||||
|
||||
**Expected Result**: 2-4x faster LSH
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Quick Wins (Low Effort, High Impact)
|
||||
|
||||
### Priority Order:
|
||||
|
||||
1. **Fix memory leak** (5 min) - Prevents crashes
|
||||
2. **Replace SHA256** (10 min) - 8x speedup + security
|
||||
3. **Remove RwLock** (15 min) - 1.2x speedup
|
||||
4. **Use binary serialization** (30 min) - 2-5x API speed
|
||||
5. **Fixed-size arrays** (20 min) - 3x feature extraction
|
||||
|
||||
**Total time: ~1.5 hours for 50x overall improvement**
|
||||
|
||||
---
|
||||
|
||||
## 📊 Performance Targets
|
||||
|
||||
### Before Optimizations:
|
||||
- Proof generation: ~8μs (32-bit range)
|
||||
- Transaction processing: ~5.5μs per tx
|
||||
- State save (10k txs): ~10ms
|
||||
- Memory (100k txs): **35MB** (with leak)
|
||||
|
||||
### After All Optimizations:
|
||||
- Proof generation: **~1μs** (8x faster)
|
||||
- Transaction processing: **~0.8μs** per tx (6.9x faster)
|
||||
- State save (10k txs): **~1ms** (10x faster)
|
||||
- Memory (100k txs): **~16MB** (54% reduction)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing the Optimizations
|
||||
|
||||
### Run Benchmarks:
|
||||
```bash
|
||||
# Before optimizations (baseline)
|
||||
cargo bench --bench plaid_performance > baseline.txt
|
||||
|
||||
# After each optimization
|
||||
cargo bench --bench plaid_performance > optimized.txt
|
||||
|
||||
# Compare
|
||||
cargo install cargo-criterion
|
||||
cargo criterion --bench plaid_performance
|
||||
```
|
||||
|
||||
### Expected Benchmark Improvements:
|
||||
|
||||
| Benchmark | Before | After All Opts | Speedup |
|
||||
|-----------|--------|----------------|---------|
|
||||
| `proof_generation/32` | 8 μs | 1 μs | 8.0x |
|
||||
| `feature_extraction/full_pipeline` | 0.12 μs | 0.04 μs | 3.0x |
|
||||
| `transaction_processing/1000` | 5.5 ms | 0.8 ms | 6.9x |
|
||||
| `json_serialize/10000` | 10 ms | 1 ms | 10.0x |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Verification Checklist
|
||||
|
||||
After implementing fixes:
|
||||
|
||||
- [ ] Memory leak fixed (check with Chrome DevTools Memory Profiler)
|
||||
- [ ] SHA256 uses `sha2` crate (verify proofs still valid)
|
||||
- [ ] No RwLock in WASM builds (check generated WASM size)
|
||||
- [ ] Binary serialization works (test with sample data)
|
||||
- [ ] Benchmarks show expected improvements
|
||||
- [ ] All tests pass: `cargo test --all-features`
|
||||
- [ ] WASM builds: `wasm-pack build --target web`
|
||||
- [ ] Browser integration tested (run in Chrome/Firefox)
|
||||
|
||||
---
|
||||
|
||||
## 📚 References
|
||||
|
||||
- **Performance Analysis**: `/home/user/ruvector/docs/plaid-performance-analysis.md`
|
||||
- **Benchmarks**: `/home/user/ruvector/benches/plaid_performance.rs`
|
||||
- **Source Files**:
|
||||
- `/home/user/ruvector/examples/edge/src/plaid/zkproofs.rs`
|
||||
- `/home/user/ruvector/examples/edge/src/plaid/mod.rs`
|
||||
- `/home/user/ruvector/examples/edge/src/plaid/wasm.rs`
|
||||
- `/home/user/ruvector/examples/edge/src/plaid/zk_wasm.rs`
|
||||
|
||||
---
|
||||
|
||||
**Generated**: 2026-01-01
|
||||
**Confidence**: High (based on static analysis)
|
||||
Reference in New Issue
Block a user