|
| 1 | +//! Edge-codec flavor comparison — measure ALL flavors, validate/invalidate. |
| 2 | +//! |
| 3 | +//! For each data regime, encodes the same vectors with the three edge-codec |
| 4 | +//! flavors and reports the full reliability suite (Pearson r, Spearman ρ, |
| 5 | +//! ICC(2,1), Cronbach α) on DISTANCE PRESERVATION (true pairwise L2 vs |
| 6 | +//! reconstructed pairwise L2 — the metric that matters for nearest-neighbour |
| 7 | +//! order), plus per-vector reconstruction rel-L2 and cosine. |
| 8 | +//! |
| 9 | +//! CoarseOnly 1 B/vec palette index (the EdgeBlock byte as-is) |
| 10 | +//! CoarseResidue 1 + D/2 palette + value-slab signed-4-bit residue |
| 11 | +//! Pq32x4 16 B 32 subquantizers × 4-bit (the edge block as PQ) |
| 12 | +//! |
| 13 | +//! The point: a class/schema picks the flavor by its fidelity/byte tradeoff, and |
| 14 | +//! these are the numbers that justify the pick. The deterministic part (nearest |
| 15 | +//! centroid) is also shown running on the AMX `matmul_i8_to_i32` tile path, |
| 16 | +//! bit-checked against the scalar assignment. |
| 17 | +//! |
| 18 | +//! RUSTFLAGS="-C target-cpu=native" cargo run --release --example edge_codec_compare |
| 19 | +
|
| 20 | +use std::time::Instant; |
| 21 | + |
| 22 | +use ndarray::hpc::edge_codec::{reconstruct_coarse, CoarseResidueCodec, Codebook, ProductQuantizer}; |
| 23 | +use ndarray::hpc::reliability::FidelityReport; |
| 24 | +use ndarray::simd::{amx_available, matmul_i8_to_i32}; |
| 25 | +use ndarray::{ArrayView2, ArrayViewMut2}; |
| 26 | + |
| 27 | +fn splitmix(s: &mut u64) -> f32 { |
| 28 | + *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15); |
| 29 | + let mut z = *s; |
| 30 | + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); |
| 31 | + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); |
| 32 | + z ^= z >> 31; |
| 33 | + (z >> 40) as f32 / (1u32 << 24) as f32 * 2.0 - 1.0 // [-1, 1) |
| 34 | +} |
| 35 | + |
| 36 | +/// Clustered data: each vector = a random centroid + noise (the regime where a |
| 37 | +/// coarse code is meaningful and the residue captures the within-cell offset). |
| 38 | +fn gen_blobs(n: usize, dim: usize, k: usize, noise: f32, seed: u64) -> Vec<f32> { |
| 39 | + let mut s = seed; |
| 40 | + let centers: Vec<f32> = (0..k * dim).map(|_| splitmix(&mut s)).collect(); |
| 41 | + let mut data = vec![0.0f32; n * dim]; |
| 42 | + for i in 0..n { |
| 43 | + let c = (splitmix(&mut s).abs() * k as f32) as usize % k; |
| 44 | + for d in 0..dim { |
| 45 | + data[i * dim + d] = centers[c * dim + d] + noise * splitmix(&mut s); |
| 46 | + } |
| 47 | + } |
| 48 | + data |
| 49 | +} |
| 50 | + |
| 51 | +/// Continuous high-dimensional data (no cluster structure): the regime where a |
| 52 | +/// coarse codebook can't tile the space and product quantization pulls ahead. |
| 53 | +fn gen_continuous(n: usize, dim: usize, seed: u64) -> Vec<f32> { |
| 54 | + let mut s = seed; |
| 55 | + (0..n * dim).map(|_| splitmix(&mut s)).collect() |
| 56 | +} |
| 57 | + |
| 58 | +fn l2(a: &[f32], b: &[f32]) -> f64 { |
| 59 | + a.iter() |
| 60 | + .zip(b) |
| 61 | + .map(|(x, y)| ((x - y) as f64).powi(2)) |
| 62 | + .sum::<f64>() |
| 63 | + .sqrt() |
| 64 | +} |
| 65 | + |
| 66 | +fn cosine(a: &[f32], b: &[f32]) -> f64 { |
| 67 | + let mut dot = 0.0; |
| 68 | + let mut na = 0.0; |
| 69 | + let mut nb = 0.0; |
| 70 | + for (x, y) in a.iter().zip(b) { |
| 71 | + dot += (*x as f64) * (*y as f64); |
| 72 | + na += (*x as f64).powi(2); |
| 73 | + nb += (*y as f64).powi(2); |
| 74 | + } |
| 75 | + if na < 1e-24 || nb < 1e-24 { |
| 76 | + 0.0 |
| 77 | + } else { |
| 78 | + dot / (na.sqrt() * nb.sqrt()) |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +/// Deterministic candidate pairs (i, j) for the distance-preservation metric. |
| 83 | +fn sample_pairs(n: usize, m: usize, seed: u64) -> Vec<(usize, usize)> { |
| 84 | + let mut s = seed; |
| 85 | + let mut next = || { |
| 86 | + s = s.wrapping_add(0x9E37_79B9_7F4A_7C15); |
| 87 | + let mut z = s; |
| 88 | + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); |
| 89 | + z ^= z >> 31; |
| 90 | + z |
| 91 | + }; |
| 92 | + (0..m) |
| 93 | + .map(|_| { |
| 94 | + let i = (next() as usize) % n; |
| 95 | + let mut j = (next() as usize) % n; |
| 96 | + if j == i { |
| 97 | + j = (j + 1) % n; |
| 98 | + } |
| 99 | + (i, j) |
| 100 | + }) |
| 101 | + .collect() |
| 102 | +} |
| 103 | + |
| 104 | +/// One flavor's measured row: byte cost + reconstruction + distance fidelity. |
| 105 | +fn report_flavor( |
| 106 | + name: &str, bytes_per_vec: f64, data: &[f32], recon: &[f32], n: usize, dim: usize, pairs: &[(usize, usize)], |
| 107 | +) { |
| 108 | + // Per-vector reconstruction quality. |
| 109 | + let mut rel_num = 0.0; |
| 110 | + let mut rel_den = 0.0; |
| 111 | + let mut cos_sum = 0.0; |
| 112 | + for i in 0..n { |
| 113 | + let v = &data[i * dim..(i + 1) * dim]; |
| 114 | + let r = &recon[i * dim..(i + 1) * dim]; |
| 115 | + rel_num += l2(v, r); |
| 116 | + rel_den += v.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt(); |
| 117 | + cos_sum += cosine(v, r); |
| 118 | + } |
| 119 | + let recon_rel = rel_num / rel_den.max(1e-12); |
| 120 | + let recon_cos = cos_sum / n as f64; |
| 121 | + |
| 122 | + // Distance preservation: true vs reconstructed pairwise L2. |
| 123 | + let true_d: Vec<f64> = pairs |
| 124 | + .iter() |
| 125 | + .map(|&(i, j)| l2(&data[i * dim..(i + 1) * dim], &data[j * dim..(j + 1) * dim])) |
| 126 | + .collect(); |
| 127 | + let rec_d: Vec<f64> = pairs |
| 128 | + .iter() |
| 129 | + .map(|&(i, j)| l2(&recon[i * dim..(i + 1) * dim], &recon[j * dim..(j + 1) * dim])) |
| 130 | + .collect(); |
| 131 | + let f = FidelityReport::compute(&true_d, &rec_d); |
| 132 | + |
| 133 | + println!( |
| 134 | + " {name:<14} {bytes_per_vec:>6.1} B | recon rel-L2 {recon_rel:.4} cos {recon_cos:.4} | dist: r {:.4} ρ {:.4} ICC {:.4} α {:.4}", |
| 135 | + f.pearson, f.spearman, f.icc, f.cronbach |
| 136 | + ); |
| 137 | +} |
| 138 | + |
| 139 | +/// AMX vs scalar assignment agreement + throughput (the deterministic part). |
| 140 | +fn amx_assign_demo(data: &[f32], cb: &Codebook, n: usize, dim: usize) { |
| 141 | + if !amx_available() { |
| 142 | + println!(" (AMX unavailable — deterministic assign runs scalar)"); |
| 143 | + return; |
| 144 | + } |
| 145 | + let k = cb.k; |
| 146 | + let q = |x: &[f32]| -> (Vec<i8>, f32) { |
| 147 | + let amax = x.iter().fold(0.0f32, |a, &v| a.max(v.abs())).max(1e-12); |
| 148 | + let sc = 127.0 / amax; |
| 149 | + ( |
| 150 | + x.iter() |
| 151 | + .map(|&v| (v * sc).round().clamp(-127.0, 127.0) as i8) |
| 152 | + .collect(), |
| 153 | + sc, |
| 154 | + ) |
| 155 | + }; |
| 156 | + let (v_i8, _) = q(data); |
| 157 | + let (cb_i8, _) = q(&cb.centroids); |
| 158 | + let mut cbt = vec![0i8; dim * k]; // D×K transpose |
| 159 | + for c in 0..k { |
| 160 | + for d in 0..dim { |
| 161 | + cbt[d * k + c] = cb_i8[c * dim + d]; |
| 162 | + } |
| 163 | + } |
| 164 | + let mut g = vec![0i32; n * k]; |
| 165 | + let t0 = Instant::now(); |
| 166 | + matmul_i8_to_i32( |
| 167 | + ArrayView2::from_shape((n, dim), &v_i8[..]).unwrap(), |
| 168 | + ArrayView2::from_shape((dim, k), &cbt[..]).unwrap(), |
| 169 | + ArrayViewMut2::from_shape((n, k), &mut g[..]).unwrap(), |
| 170 | + ) |
| 171 | + .unwrap(); |
| 172 | + let ns = t0.elapsed().as_nanos() as f64; |
| 173 | + let cnorm: Vec<i32> = (0..k) |
| 174 | + .map(|c| (0..dim).map(|d| (cb_i8[c * dim + d] as i32).pow(2)).sum()) |
| 175 | + .collect(); |
| 176 | + let mut agree = 0usize; |
| 177 | + for i in 0..n { |
| 178 | + let mut best = i32::MIN; |
| 179 | + let mut bj = 0u32; |
| 180 | + for j in 0..k { |
| 181 | + let score = 2 * g[i * k + j] - cnorm[j]; |
| 182 | + if score > best { |
| 183 | + best = score; |
| 184 | + bj = j as u32; |
| 185 | + } |
| 186 | + } |
| 187 | + if bj == cb.assign(&data[i * dim..(i + 1) * dim]) { |
| 188 | + agree += 1; |
| 189 | + } |
| 190 | + } |
| 191 | + let macs = (n * k * dim) as f64; |
| 192 | + println!( |
| 193 | + " AMX assign: {:.0} ns ({:.1} GMAC/s), agrees with scalar on {:.1}% of vectors", |
| 194 | + ns, |
| 195 | + macs / ns, |
| 196 | + 100.0 * agree as f64 / n as f64 |
| 197 | + ); |
| 198 | +} |
| 199 | + |
| 200 | +fn run(label: &str, data: &[f32], n: usize, dim: usize, k: usize) { |
| 201 | + println!("\n== {label} (N={n} D={dim} K={k}) =="); |
| 202 | + let pairs = sample_pairs(n, 4096, 0xF00D); |
| 203 | + |
| 204 | + // Flavor 1: coarse only. |
| 205 | + let cb = Codebook::train(data, n, dim, k, 12, 1); |
| 206 | + let mut recon_coarse = vec![0.0f32; n * dim]; |
| 207 | + for i in 0..n { |
| 208 | + let idx = cb.assign(&data[i * dim..(i + 1) * dim]); |
| 209 | + recon_coarse[i * dim..(i + 1) * dim].copy_from_slice(&reconstruct_coarse(&cb, idx)); |
| 210 | + } |
| 211 | + report_flavor("CoarseOnly", 1.0, data, &recon_coarse, n, dim, &pairs); |
| 212 | + |
| 213 | + // Flavor 2: coarse + per-dim 4-bit residue. |
| 214 | + let crc = CoarseResidueCodec::fit(data, n, dim, k, 12, 1); |
| 215 | + let mut recon_res = vec![0.0f32; n * dim]; |
| 216 | + for i in 0..n { |
| 217 | + let code = crc.encode(&data[i * dim..(i + 1) * dim]); |
| 218 | + recon_res[i * dim..(i + 1) * dim].copy_from_slice(&crc.reconstruct(&code)); |
| 219 | + } |
| 220 | + report_flavor("CoarseResidue", 1.0 + dim as f64 / 2.0, data, &recon_res, n, dim, &pairs); |
| 221 | + |
| 222 | + // Flavor 3: product quantizer 32×4-bit (16 B). |
| 223 | + if dim.is_multiple_of(32) { |
| 224 | + let pq = ProductQuantizer::fit(data, n, dim, 32, 12, 2); |
| 225 | + let mut recon_pq = vec![0.0f32; n * dim]; |
| 226 | + for i in 0..n { |
| 227 | + let code = pq.encode(&data[i * dim..(i + 1) * dim]); |
| 228 | + recon_pq[i * dim..(i + 1) * dim].copy_from_slice(&pq.reconstruct(&code)); |
| 229 | + } |
| 230 | + report_flavor("Pq32x4", 16.0, data, &recon_pq, n, dim, &pairs); |
| 231 | + } else { |
| 232 | + println!(" Pq32x4 (skipped — D={dim} not divisible by 32)"); |
| 233 | + } |
| 234 | + |
| 235 | + amx_assign_demo(data, &cb, n, dim); |
| 236 | +} |
| 237 | + |
| 238 | +fn main() { |
| 239 | + println!("== Edge-codec flavor comparison (measure all, validate/invalidate) =="); |
| 240 | + println!("amx_available() = {}", amx_available()); |
| 241 | + println!("metrics: recon rel-L2/cosine (per-vector) · dist r/ρ/ICC/α (pairwise L2 preservation)"); |
| 242 | + |
| 243 | + let (n, dim, k) = (4096, 128, 256); |
| 244 | + run("blobs σ=0.15", &gen_blobs(n, dim, k, 0.15, 0x1111), n, dim, k); |
| 245 | + run("blobs σ=0.30", &gen_blobs(n, dim, k, 0.30, 0x2222), n, dim, k); |
| 246 | + run("continuous", &gen_continuous(n, dim, 0x3333), n, dim, k); |
| 247 | +} |
0 commit comments