nodedb_codec/vector_quant/
opq.rs1use nalgebra::{DMatrix, SVD};
40
41use crate::vector_quant::codec::{AdcLut, VectorCodec};
42use crate::vector_quant::layout::{QuantHeader, QuantMode, UnifiedQuantizedVector};
43use crate::vector_quant::opq_kmeans::l2_sq;
44use crate::vector_quant::opq_kmeans::lloyd;
45
46pub struct OpqCodec {
53 pub dim: usize,
54 pub m: usize,
56 pub k: usize,
58 pub sub_dim: usize,
59 rotation: Vec<f32>,
61 codebooks: Vec<Vec<Vec<f32>>>,
63}
64
65impl OpqCodec {
66 pub fn train(
75 vectors: &[&[f32]],
76 dim: usize,
77 m: usize,
78 k: usize,
79 opq_iters: usize,
80 kmeans_iters: usize,
81 ) -> Self {
82 assert!(!vectors.is_empty(), "training set must be non-empty");
83 assert!(dim > 0 && m > 0 && k > 0, "dim/m/k must be positive");
84 assert!(
85 dim.is_multiple_of(m),
86 "dim ({dim}) must be divisible by m ({m})"
87 );
88 let sub_dim = dim / m;
89 let seed = dim as u64 ^ ((m as u64) << 16) ^ ((k as u64) << 32);
90
91 let mut rotation = identity(dim);
92 let mut codebooks: Vec<Vec<Vec<f32>>> = Vec::new();
93
94 let iters = opq_iters.max(1);
95
96 for iter in 0..iters {
97 let rotated: Vec<Vec<f32>> =
99 vectors.iter().map(|v| matvec(&rotation, v, dim)).collect();
100 codebooks = train_codebooks(&rotated, m, k, sub_dim, kmeans_iters, seed ^ iter as u64);
101
102 if iter + 1 < iters {
113 let n = vectors.len();
114 let x_mat = DMatrix::from_fn(dim, n, |row, col| vectors[col][row]);
117 let y_mat = {
118 let recon: Vec<Vec<f32>> = rotated
119 .iter()
120 .map(|rv| {
121 let codes = pq_encode(rv, &codebooks, m, sub_dim);
122 dequantize_codes(&codes, &codebooks)
123 })
124 .collect();
125 DMatrix::from_fn(dim, n, |row, col| recon[col][row])
126 };
127
128 let m_mat = &x_mat * y_mat.transpose();
130
131 let has_nan = m_mat.iter().any(|x| x.is_nan());
134 if !has_nan {
135 let svd = SVD::new(m_mat, true, true);
136 if let (Some(u), Some(v_t)) = (svd.u, svd.v_t) {
137 let r_new = v_t.transpose() * u.transpose();
139 let mut buf = Vec::with_capacity(dim * dim);
141 for i in 0..dim {
142 for j in 0..dim {
143 buf.push(r_new[(i, j)]);
144 }
145 }
146 rotation = buf;
147 }
148 }
149 }
150 }
151
152 Self {
153 dim,
154 m,
155 k,
156 sub_dim,
157 rotation,
158 codebooks,
159 }
160 }
161
162 pub fn apply_rotation(&self, v: &[f32]) -> Vec<f32> {
164 matvec(&self.rotation, v, self.dim)
165 }
166
167 fn encode_inner(&self, v: &[f32]) -> (Vec<u8>, UnifiedQuantizedVector) {
168 let rotated = self.apply_rotation(v);
169 let codes = pq_encode(&rotated, &self.codebooks, self.m, self.sub_dim);
170 let uqv = make_uqv(&codes, self.dim as u16);
171 (codes, uqv)
172 }
173
174 fn dequantize(&self, codes: &[u8]) -> Vec<f32> {
175 dequantize_codes(codes, &self.codebooks)
176 }
177}
178
179fn identity(dim: usize) -> Vec<f32> {
183 let mut mat = vec![0.0f32; dim * dim];
184 for i in 0..dim {
185 mat[i * dim + i] = 1.0;
186 }
187 mat
188}
189
190fn dequantize_codes(codes: &[u8], codebooks: &[Vec<Vec<f32>>]) -> Vec<f32> {
192 let mut out = Vec::with_capacity(codebooks.len() * codebooks[0][0].len());
193 for (s, &c) in codes.iter().enumerate() {
194 out.extend_from_slice(&codebooks[s][c as usize]);
195 }
196 out
197}
198
199#[inline]
201fn matvec(r: &[f32], v: &[f32], dim: usize) -> Vec<f32> {
202 let mut out = vec![0.0f32; dim];
203 for i in 0..dim {
204 let row = &r[i * dim..(i + 1) * dim];
205 out[i] = row.iter().zip(v.iter()).map(|(a, b)| a * b).sum();
206 }
207 out
208}
209
210fn pq_encode(v: &[f32], codebooks: &[Vec<Vec<f32>>], m: usize, sub_dim: usize) -> Vec<u8> {
211 let mut codes = Vec::with_capacity(m);
212 for (s, codebook) in codebooks.iter().enumerate().take(m) {
213 let offset = s * sub_dim;
214 let sub = &v[offset..offset + sub_dim];
215 let best = codebook
216 .iter()
217 .enumerate()
218 .min_by(|(_, a), (_, b)| {
219 l2_sq(sub, a)
220 .partial_cmp(&l2_sq(sub, b))
221 .unwrap_or(std::cmp::Ordering::Equal)
222 })
223 .map(|(i, _)| i)
224 .unwrap_or(0);
225 codes.push(best as u8);
226 }
227 codes
228}
229
230fn train_codebooks(
231 rotated: &[Vec<f32>],
232 m: usize,
233 k: usize,
234 sub_dim: usize,
235 kmeans_iters: usize,
236 seed: u64,
237) -> Vec<Vec<Vec<f32>>> {
238 let mut codebooks = Vec::with_capacity(m);
239 for s in 0..m {
240 let offset = s * sub_dim;
241 let sub_vecs: Vec<Vec<f32>> = rotated
242 .iter()
243 .map(|v| v[offset..offset + sub_dim].to_vec())
244 .collect();
245 let centroids = lloyd(
246 &sub_vecs,
247 sub_dim,
248 k,
249 kmeans_iters,
250 seed ^ (s as u64 * 0x1234567),
251 );
252 codebooks.push(centroids);
253 }
254 codebooks
255}
256
257fn make_uqv(codes: &[u8], dim: u16) -> UnifiedQuantizedVector {
258 let header = QuantHeader {
259 quant_mode: QuantMode::Pq as u16,
260 dim,
261 global_scale: 1.0,
262 residual_norm: 0.0,
263 dot_quantized: 0.0,
264 outlier_bitmask: 0,
265 reserved: [0; 8],
266 };
267 UnifiedQuantizedVector::new(header, codes, &[])
268 .expect("make_uqv: layout construction must not fail for valid inputs")
269}
270
271pub struct OpqQuantized {
275 codes: Vec<u8>,
276 uqv: UnifiedQuantizedVector,
277}
278
279impl AsRef<UnifiedQuantizedVector> for OpqQuantized {
280 fn as_ref(&self) -> &UnifiedQuantizedVector {
281 &self.uqv
282 }
283}
284
285pub struct OpqQuery {
287 pub distance_table: Vec<f32>,
288 #[allow(dead_code)]
289 rotated: Vec<f32>,
290}
291
292impl VectorCodec for OpqCodec {
295 type Quantized = OpqQuantized;
296 type Query = OpqQuery;
297
298 fn encode(&self, v: &[f32]) -> Self::Quantized {
299 let (codes, uqv) = self.encode_inner(v);
300 OpqQuantized { codes, uqv }
301 }
302
303 fn prepare_query(&self, q: &[f32]) -> Self::Query {
305 let rotated = self.apply_rotation(q);
306 let mut table = vec![0.0f32; self.m * self.k];
307 for s in 0..self.m {
308 let offset = s * self.sub_dim;
309 let sub_q = &rotated[offset..offset + self.sub_dim];
310 for c in 0..self.k {
311 table[s * self.k + c] = l2_sq(sub_q, &self.codebooks[s][c]);
312 }
313 }
314 OpqQuery {
315 distance_table: table,
316 rotated,
317 }
318 }
319
320 fn adc_lut(&self, q: &Self::Query) -> Option<AdcLut> {
321 let mut lut = AdcLut::new(self.m as u16, self.k as u16);
322 lut.table.copy_from_slice(&q.distance_table);
323 Some(lut)
324 }
325
326 fn fast_symmetric_distance(&self, q: &Self::Quantized, v: &Self::Quantized) -> f32 {
328 let qv = self.dequantize(&q.codes);
329 let vv = self.dequantize(&v.codes);
330 l2_sq(&qv, &vv)
331 }
332
333 fn exact_asymmetric_distance(&self, q: &Self::Query, v: &Self::Quantized) -> f32 {
335 v.codes
336 .iter()
337 .enumerate()
338 .map(|(s, &code)| q.distance_table[s * self.k + code as usize])
339 .sum()
340 }
341}
342
343#[cfg(test)]
346mod tests {
347 use super::*;
348
349 fn tiny_dataset() -> Vec<Vec<f32>> {
350 (0..10)
351 .map(|i| {
352 let base = i as f32 * 2.0;
353 vec![
354 base,
355 base + 0.1,
356 base - 0.1,
357 base + 0.2,
358 base * 0.5,
359 base * 0.5 + 0.1,
360 base * 0.5 - 0.1,
361 base * 0.5 + 0.05,
362 ]
363 })
364 .collect()
365 }
366
367 fn train_tiny() -> OpqCodec {
368 let vecs = tiny_dataset();
369 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
370 OpqCodec::train(&refs, 8, 2, 4, 10, 30)
371 }
372
373 #[test]
374 fn encode_produces_m_bytes() {
375 let codec = train_tiny();
376 let vecs = tiny_dataset();
377 for v in &vecs {
378 let q = codec.encode(v);
379 assert_eq!(q.codes.len(), codec.m);
380 }
381 }
382
383 #[test]
384 fn distance_is_non_negative() {
385 let codec = train_tiny();
386 let vecs = tiny_dataset();
387 for v in &vecs {
388 let qv = codec.encode(v);
389 let qq = codec.prepare_query(v);
390 let asym = codec.exact_asymmetric_distance(&qq, &qv);
391 let sym = codec.fast_symmetric_distance(&qv, &qv);
392 assert!(
393 asym >= 0.0,
394 "asymmetric distance must be non-negative, got {asym}"
395 );
396 assert!(
397 sym >= 0.0,
398 "symmetric distance must be non-negative, got {sym}"
399 );
400 }
401 }
402
403 #[test]
404 fn top1_recall_on_training_set() {
405 let vecs = tiny_dataset();
406 let codec = train_tiny();
407 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
408 let encoded: Vec<_> = refs.iter().map(|v| codec.encode(v)).collect();
409
410 let mut correct = 0usize;
411 for (i, v) in refs.iter().enumerate() {
412 let query = codec.prepare_query(v);
413 let best = encoded
414 .iter()
415 .enumerate()
416 .min_by(|(_, a), (_, b)| {
417 codec
418 .exact_asymmetric_distance(&query, a)
419 .partial_cmp(&codec.exact_asymmetric_distance(&query, b))
420 .unwrap_or(std::cmp::Ordering::Equal)
421 })
422 .map(|(idx, _)| idx)
423 .unwrap_or(usize::MAX);
424 if best == i {
425 correct += 1;
426 }
427 }
428 let recall = correct as f64 / vecs.len() as f64;
429 assert!(
434 recall >= 0.70,
435 "top-1 recall on training set too low: {correct}/{} = {recall:.2}",
436 vecs.len()
437 );
438 }
439
440 #[test]
441 fn more_iterations_reduce_reconstruction_error() {
442 let vecs = tiny_dataset();
443 let refs: Vec<&[f32]> = vecs.iter().map(|v| v.as_slice()).collect();
444
445 let codec_1 = OpqCodec::train(&refs, 8, 2, 4, 1, 10);
446 let codec_5 = OpqCodec::train(&refs, 8, 2, 4, 5, 10);
447
448 let mean_recon_error = |codec: &OpqCodec| -> f32 {
449 refs.iter()
450 .map(|v| {
451 let rotated = codec.apply_rotation(v);
452 let codes = pq_encode(&rotated, &codec.codebooks, codec.m, codec.sub_dim);
453 let recon = dequantize_codes(&codes, &codec.codebooks);
454 l2_sq(&rotated, &recon)
455 })
456 .sum::<f32>()
457 / refs.len() as f32
458 };
459
460 let err_1 = mean_recon_error(&codec_1);
461 let err_5 = mean_recon_error(&codec_5);
462
463 assert!(
464 err_5 <= err_1 * 1.05,
465 "5-iter OPQ (err={err_5:.4}) should have ≤ reconstruction error than 1-iter (err={err_1:.4})"
466 );
467 }
468}