hermes_core/structures/vector/quantization/
pq.rs1use serde::{Deserialize, Serialize};
9
10#[cfg(target_arch = "aarch64")]
11#[allow(unused_imports)]
12use std::arch::aarch64::*;
13
14#[cfg(all(target_arch = "x86_64", feature = "native"))]
15#[allow(unused_imports)]
16use std::arch::x86_64::*;
17
18pub const DEFAULT_NUM_CENTROIDS: usize = 256;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct PQConfig {
24 pub dim: usize,
26 pub num_subspaces: usize,
28 pub dims_per_block: usize,
30 pub num_centroids: usize,
32 pub seed: u64,
34 pub use_opq: bool,
36 pub opq_iters: usize,
38}
39
40impl PQConfig {
41 pub fn new(dim: usize) -> Self {
45 assert!(dim > 0, "PQ dimension must be non-zero");
46 let dims_per_block = (1..=8)
47 .rev()
48 .find(|candidate| dim.is_multiple_of(*candidate))
49 .unwrap_or(1);
50 Self {
51 dim,
52 num_subspaces: dim / dims_per_block,
53 dims_per_block,
54 num_centroids: DEFAULT_NUM_CENTROIDS,
55 seed: 42,
56 use_opq: true,
57 opq_iters: 4,
58 }
59 }
60
61 #[cfg(test)]
62 pub fn with_opq(mut self, enabled: bool, iters: usize) -> Self {
63 self.use_opq = enabled;
64 self.opq_iters = iters;
65 self
66 }
67
68 pub fn subspace_dim(&self) -> usize {
70 self.dims_per_block
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct PQCodebook {
79 pub config: PQConfig,
81 pub rotation_matrix: Option<Vec<f32>>,
83 pub centroids: Vec<f32>,
85 pub version: u64,
87}
88
89impl PQCodebook {
90 pub(crate) fn validate(&self) -> Result<(), String> {
91 let config = &self.config;
92 if config.dim == 0
93 || config.num_subspaces == 0
94 || config.dims_per_block == 0
95 || config.num_centroids == 0
96 || config.num_centroids > 256
97 {
98 return Err("PQ codebook has invalid zero/unbounded dimensions".to_string());
99 }
100 let covered_dim = config
101 .num_subspaces
102 .checked_mul(config.dims_per_block)
103 .ok_or_else(|| "PQ subspace dimension overflow".to_string())?;
104 if covered_dim != config.dim {
105 return Err(format!(
106 "PQ subspaces cover {covered_dim} dimensions, expected {}",
107 config.dim
108 ));
109 }
110 let expected_centroids = config
111 .num_subspaces
112 .checked_mul(config.num_centroids)
113 .and_then(|count| count.checked_mul(config.dims_per_block))
114 .ok_or_else(|| "PQ centroid size overflow".to_string())?;
115 if self.centroids.len() != expected_centroids
116 || self.centroids.iter().any(|value| !value.is_finite())
117 {
118 return Err(format!(
119 "PQ centroid table is invalid: got {}, expected {expected_centroids}",
120 self.centroids.len()
121 ));
122 }
123 if let Some(rotation) = &self.rotation_matrix {
124 let expected = config
125 .dim
126 .checked_mul(config.dim)
127 .ok_or_else(|| "PQ rotation size overflow".to_string())?;
128 if rotation.len() != expected || rotation.iter().any(|value| !value.is_finite()) {
129 return Err(format!(
130 "PQ rotation matrix is invalid: got {}, expected {expected}",
131 rotation.len()
132 ));
133 }
134 }
135 if config.use_opq != self.rotation_matrix.is_some() {
136 return Err("PQ OPQ configuration does not match its rotation matrix".to_string());
137 }
138 Ok(())
139 }
140
141 pub fn train(config: PQConfig, vectors: &[Vec<f32>], max_iters: usize) -> Self {
143 #[cfg(not(feature = "native"))]
144 let config = PQConfig {
145 use_opq: false,
146 opq_iters: 0,
147 ..config
148 };
149
150 assert!(!vectors.is_empty(), "Cannot train on empty vector set");
151 assert!(
152 vectors
153 .iter()
154 .all(|vector| vector.len() == config.dim
155 && vector.iter().all(|value| value.is_finite())),
156 "Vector dimension mismatch or non-finite training value"
157 );
158
159 let m = config.num_subspaces;
160 let k = config.num_centroids;
161 let sub_dim = config.subspace_dim();
162 let n = vectors.len();
163
164 #[cfg(feature = "native")]
166 let rotation_matrix = (config.use_opq && config.opq_iters > 0)
167 .then(|| Self::learn_opq_rotation(&config, vectors, max_iters));
168 #[cfg(not(feature = "native"))]
169 let rotation_matrix: Option<Vec<f32>> = None;
170
171 let rotated_vectors: Vec<Vec<f32>> = if let Some(ref r) = rotation_matrix {
173 vectors
174 .iter()
175 .map(|v| Self::apply_rotation(r, v, config.dim))
176 .collect()
177 } else {
178 vectors.to_vec()
179 };
180
181 let mut centroids = Vec::with_capacity(m * k * sub_dim);
183
184 for subspace_idx in 0..m {
185 let offset = subspace_idx * sub_dim;
186
187 let subdata: Vec<f32> = rotated_vectors
188 .iter()
189 .flat_map(|v| v[offset..offset + sub_dim].iter().copied())
190 .collect();
191
192 let actual_k = k.min(n);
193
194 let trained = crate::structures::vector::kmeans::train_euclidean_kmeans(
195 &subdata,
196 n,
197 sub_dim,
198 actual_k,
199 max_iters,
200 config.seed ^ (subspace_idx as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15),
201 );
202 centroids.extend(trained.centroids);
203
204 while centroids.len() < (subspace_idx + 1) * k * sub_dim {
206 let last_start = centroids.len() - sub_dim;
207 let last: Vec<f32> = centroids[last_start..].to_vec();
208 centroids.extend(last);
209 }
210 }
211
212 let version = std::time::SystemTime::now()
213 .duration_since(std::time::UNIX_EPOCH)
214 .unwrap_or_default()
215 .as_nanos() as u64;
216
217 Self {
218 config,
219 rotation_matrix,
220 centroids,
221 version,
222 }
223 }
224
225 #[cfg(feature = "native")]
227 fn learn_opq_rotation(config: &PQConfig, vectors: &[Vec<f32>], max_iters: usize) -> Vec<f32> {
228 use nalgebra::DMatrix;
229
230 let dim = config.dim;
231 let n = vectors.len();
232
233 let mut rotation = DMatrix::<f32>::identity(dim, dim);
234 let data: Vec<f32> = vectors.iter().flat_map(|v| v.iter().copied()).collect();
235 let x = DMatrix::from_row_slice(n, dim, &data);
236
237 for _iter in 0..config.opq_iters.min(max_iters) {
238 let rotated = &x * rotation.transpose();
241 let reconstructed = Self::reconstruct_pq(
242 config,
243 &rotated,
244 5,
245 config.seed ^ (_iter as u64).wrapping_mul(0x517c_c1b7_2722_0a95),
246 );
247
248 let xtx_hat = x.transpose() * &reconstructed;
249 let svd = xtx_hat.svd(true, true);
250 if let (Some(u), Some(vt)) = (svd.u, svd.v_t) {
251 rotation = vt.transpose() * u.transpose();
254 }
255 }
256
257 let mut row_major = Vec::with_capacity(dim * dim);
258 for row in 0..dim {
259 for column in 0..dim {
260 row_major.push(rotation[(row, column)]);
261 }
262 }
263 row_major
264 }
265
266 #[cfg(feature = "native")]
267 fn reconstruct_pq(
268 config: &PQConfig,
269 rotated: &nalgebra::DMatrix<f32>,
270 iterations: usize,
271 seed: u64,
272 ) -> nalgebra::DMatrix<f32> {
273 let m = config.num_subspaces;
274 let k = config.num_centroids.min(rotated.nrows());
275 let sub_dim = config.subspace_dim();
276 let n = rotated.nrows();
277 let mut reconstructed = nalgebra::DMatrix::<f32>::zeros(n, config.dim);
278
279 for subspace_idx in 0..m {
280 let mut subdata: Vec<f32> = Vec::with_capacity(n * sub_dim);
281 for row in 0..n {
282 for col in 0..sub_dim {
283 subdata.push(rotated[(row, subspace_idx * sub_dim + col)]);
284 }
285 }
286
287 let trained = crate::structures::vector::kmeans::train_euclidean_kmeans(
288 &subdata,
289 n,
290 sub_dim,
291 k,
292 iterations,
293 seed ^ (subspace_idx as u64).wrapping_mul(0x9e37_79b9_7f4a_7c15),
294 );
295 for (row, &assignment) in trained.assignments.iter().enumerate() {
296 let centroid = &trained.centroids[assignment * sub_dim..(assignment + 1) * sub_dim];
297 for (column, &value) in centroid.iter().enumerate() {
298 reconstructed[(row, subspace_idx * sub_dim + column)] = value;
299 }
300 }
301 }
302 reconstructed
303 }
304
305 fn apply_rotation(rotation: &[f32], vector: &[f32], dim: usize) -> Vec<f32> {
307 let mut result = vec![0.0f32; dim];
308 for i in 0..dim {
309 result[i] = crate::structures::simd::dot_product_f32(
310 &rotation[i * dim..(i + 1) * dim],
311 vector,
312 dim,
313 );
314 }
315 result
316 }
317
318 fn find_nearest(centroids: &[f32], vector: &[f32], sub_dim: usize) -> usize {
320 let num_centroids = centroids.len() / sub_dim;
321 let mut best_idx = 0;
322 let mut best_dist = f32::MAX;
323
324 for c in 0..num_centroids {
325 let offset = c * sub_dim;
326 let dist: f32 = vector
327 .iter()
328 .zip(¢roids[offset..offset + sub_dim])
329 .map(|(&a, &b)| (a - b) * (a - b))
330 .sum();
331
332 if dist < best_dist {
333 best_dist = dist;
334 best_idx = c;
335 }
336 }
337
338 best_idx
339 }
340
341 pub(crate) fn encode_into(
344 &self,
345 vector: &[f32],
346 centroid: Option<&[f32]>,
347 codes: &mut Vec<u8>,
348 residual: &mut Vec<f32>,
349 rotated: &mut Vec<f32>,
350 ) {
351 let m = self.config.num_subspaces;
352 let k = self.config.num_centroids;
353 let sub_dim = self.config.subspace_dim();
354
355 residual.clear();
356 residual.reserve(self.config.dim);
357 if let Some(centroid) = centroid {
358 residual.extend(
359 vector
360 .iter()
361 .zip(centroid)
362 .map(|(&value, ¢er)| value - center),
363 );
364 } else {
365 residual.extend_from_slice(vector);
366 }
367
368 let vec_to_encode = if let Some(ref r) = self.rotation_matrix {
369 rotated.clear();
370 rotated.resize(self.config.dim, 0.0);
371 for (row, output) in rotated.iter_mut().enumerate() {
372 *output = crate::structures::simd::dot_product_f32(
373 &r[row * self.config.dim..(row + 1) * self.config.dim],
374 residual,
375 self.config.dim,
376 );
377 }
378 rotated.as_slice()
379 } else {
380 residual.as_slice()
381 };
382
383 codes.reserve(m);
384
385 for subspace_idx in 0..m {
386 let vec_offset = subspace_idx * sub_dim;
387 let subvec = &vec_to_encode[vec_offset..vec_offset + sub_dim];
388
389 let centroid_base = subspace_idx * k * sub_dim;
390 let centroids_slice = &self.centroids[centroid_base..centroid_base + k * sub_dim];
391
392 let nearest = Self::find_nearest(centroids_slice, subvec, sub_dim);
393 codes.push(nearest as u8);
394 }
395 }
396
397 pub fn encode(&self, vector: &[f32], centroid: Option<&[f32]>) -> Vec<u8> {
401 let mut codes = Vec::with_capacity(self.config.num_subspaces);
402 self.encode_into(
403 vector,
404 centroid,
405 &mut codes,
406 &mut Vec::new(),
407 &mut Vec::new(),
408 );
409 codes
410 }
411
412 pub fn decode(&self, codes: &[u8]) -> Vec<f32> {
414 let m = self.config.num_subspaces;
415 let k = self.config.num_centroids;
416 let sub_dim = self.config.subspace_dim();
417
418 let mut rotated_vector = Vec::with_capacity(self.config.dim);
419
420 for (subspace_idx, &code) in codes.iter().enumerate().take(m) {
421 let centroid_base = subspace_idx * k * sub_dim;
422 let centroid_offset = centroid_base + (code as usize) * sub_dim;
423 rotated_vector
424 .extend_from_slice(&self.centroids[centroid_offset..centroid_offset + sub_dim]);
425 }
426
427 if let Some(ref r) = self.rotation_matrix {
429 Self::apply_rotation_transpose(r, &rotated_vector, self.config.dim)
430 } else {
431 rotated_vector
432 }
433 }
434
435 fn apply_rotation_transpose(rotation: &[f32], vector: &[f32], dim: usize) -> Vec<f32> {
437 let mut result = vec![0.0f32; dim];
438 for i in 0..dim {
439 for j in 0..dim {
440 result[i] += rotation[j * dim + i] * vector[j];
441 }
442 }
443 result
444 }
445
446 #[inline]
448 pub fn get_centroid(&self, subspace_idx: usize, code: u8) -> &[f32] {
449 let k = self.config.num_centroids;
450 let sub_dim = self.config.subspace_dim();
451 let offset = subspace_idx * k * sub_dim + (code as usize) * sub_dim;
452 &self.centroids[offset..offset + sub_dim]
453 }
454
455 pub fn rotate_query(&self, query: &[f32]) -> Vec<f32> {
457 if let Some(ref r) = self.rotation_matrix {
458 Self::apply_rotation(r, query, self.config.dim)
459 } else {
460 query.to_vec()
461 }
462 }
463
464 pub fn size_bytes(&self) -> usize {
466 let centroids_size = self.centroids.len() * 4;
467 let rotation_size = self
468 .rotation_matrix
469 .as_ref()
470 .map(|r| r.len() * 4)
471 .unwrap_or(0);
472 centroids_size + rotation_size + 64
473 }
474
475 #[cfg(feature = "native")]
478 pub(crate) fn visit_resident_regions(&self, visit: &mut dyn FnMut(&'static str, &[u8])) {
479 if let Some(rotation) = &self.rotation_matrix {
480 visit(
481 "PQ rotation matrix",
482 crate::structures::vector::ivf::routing::bytes_of_slice(rotation),
483 );
484 }
485 visit(
486 "PQ centroid table",
487 crate::structures::vector::ivf::routing::bytes_of_slice(&self.centroids),
488 );
489 }
490}
491
492#[derive(Debug, Clone)]
494pub struct DistanceTable {
495 pub distances: Vec<f32>,
497 pub num_subspaces: usize,
499 pub num_centroids: usize,
501}
502
503impl DistanceTable {
504 pub fn build(codebook: &PQCodebook, query: &[f32], centroid: Option<&[f32]>) -> Self {
506 let m = codebook.config.num_subspaces;
507 let k = codebook.config.num_centroids;
508 let sub_dim = codebook.config.subspace_dim();
509
510 let residual: Vec<f32> = if let Some(c) = centroid {
512 query.iter().zip(c).map(|(&v, &c)| v - c).collect()
513 } else {
514 query.to_vec()
515 };
516
517 let rotated_query = codebook.rotate_query(&residual);
519
520 let mut distances = Vec::with_capacity(m * k);
521
522 for subspace_idx in 0..m {
523 let query_offset = subspace_idx * sub_dim;
524 let query_sub = &rotated_query[query_offset..query_offset + sub_dim];
525
526 let centroid_base = subspace_idx * k * sub_dim;
527
528 for centroid_idx in 0..k {
529 let centroid_offset = centroid_base + centroid_idx * sub_dim;
530 let centroid = &codebook.centroids[centroid_offset..centroid_offset + sub_dim];
531
532 let dist: f32 = query_sub
533 .iter()
534 .zip(centroid.iter())
535 .map(|(&a, &b)| (a - b) * (a - b))
536 .sum();
537
538 distances.push(dist);
539 }
540 }
541
542 Self {
543 distances,
544 num_subspaces: m,
545 num_centroids: k,
546 }
547 }
548
549 #[inline]
551 pub fn compute_distance(&self, codes: &[u8]) -> f32 {
552 let k = self.num_centroids;
553 let mut total = 0.0f32;
554
555 for (subspace_idx, &code) in codes.iter().enumerate() {
556 let table_offset = subspace_idx * k + code as usize;
557 total += self.distances[table_offset];
558 }
559
560 total
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567 use rand::prelude::*;
568
569 #[test]
570 fn test_pq_config() {
571 let config = PQConfig::new(128);
572 assert_eq!(config.dim, 128);
573 assert_eq!(config.dims_per_block, 8);
574 assert_eq!(config.num_subspaces, 16);
575 }
576
577 #[test]
578 fn test_pq_encode_decode() {
579 let dim = 32;
580 let config = PQConfig::new(dim).with_opq(false, 0);
581
582 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
583 let vectors: Vec<Vec<f32>> = (0..100)
584 .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
585 .collect();
586
587 let codebook = PQCodebook::train(config, &vectors, 10);
588
589 let test_vec: Vec<f32> = (0..dim).map(|i| i as f32 / dim as f32).collect();
590 let code = codebook.encode(&test_vec, None);
591
592 assert_eq!(code.len(), 4); }
594
595 #[test]
596 fn test_distance_table() {
597 let dim = 16;
598 let config = PQConfig::new(dim).with_opq(false, 0);
599
600 let mut rng = rand::rngs::StdRng::seed_from_u64(123);
601 let vectors: Vec<Vec<f32>> = (0..50)
602 .map(|_| (0..dim).map(|_| rng.random::<f32>()).collect())
603 .collect();
604
605 let codebook = PQCodebook::train(config, &vectors, 5);
606
607 let query: Vec<f32> = (0..dim).map(|_| rng.random::<f32>()).collect();
608 let table = DistanceTable::build(&codebook, &query, None);
609
610 let code = codebook.encode(&vectors[0], None);
611 let dist = table.compute_distance(&code);
612
613 assert!(dist >= 0.0);
614 }
615}