1use serde::{Deserialize, Serialize};
23
24#[non_exhaustive]
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum Temperature {
30 Hot,
32 Warm,
34 Cold,
36}
37
38impl std::fmt::Display for Temperature {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 Temperature::Hot => write!(f, "hot"),
42 Temperature::Warm => write!(f, "warm"),
43 Temperature::Cold => write!(f, "cold"),
44 }
45 }
46}
47
48pub fn f32_to_fp16(value: f32) -> u16 {
55 let bits = value.to_bits();
56 let sign = (bits >> 16) & 0x8000;
57 let exponent = ((bits >> 23) & 0xFF) as i32;
58 let mantissa = bits & 0x007F_FFFF;
59
60 if exponent == 0 {
61 return sign as u16;
63 }
64
65 if exponent == 0xFF {
66 return (sign | 0x7C00) as u16;
68 }
69
70 let new_exp = exponent - 127 + 15;
71
72 if new_exp >= 31 {
73 return (sign | 0x7C00) as u16;
75 }
76
77 if new_exp <= 0 {
78 return sign as u16;
80 }
81
82 let new_mantissa = mantissa >> 13;
83 (sign | ((new_exp as u32) << 10) | new_mantissa) as u16
84}
85
86pub fn fp16_to_f32(half: u16) -> f32 {
88 let sign = ((half as u32) & 0x8000) << 16;
89 let exponent = ((half >> 10) & 0x1F) as u32;
90 let mantissa = (half & 0x03FF) as u32;
91
92 if exponent == 0 {
93 if mantissa == 0 {
94 return f32::from_bits(sign);
96 }
97 let mut e = 1i32;
99 let mut m = mantissa;
100 while (m & 0x0400) == 0 {
101 m <<= 1;
102 e -= 1;
103 }
104 m &= 0x03FF;
105 let f32_exp = ((127 - 15 + e) as u32) << 23;
106 let f32_mantissa = m << 13;
107 return f32::from_bits(sign | f32_exp | f32_mantissa);
108 }
109
110 if exponent == 31 {
111 return f32::from_bits(sign | 0x7F80_0000 | (mantissa << 13));
113 }
114
115 let f32_exp = (exponent + 127 - 15) << 23;
116 let f32_mantissa = mantissa << 13;
117 f32::from_bits(sign | f32_exp | f32_mantissa)
118}
119
120const PQ_SUBVECTORS: usize = 8;
124
125const PQ_CODEBOOK_SIZE: usize = 256;
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct PqVector {
132 pub codes: Vec<u8>,
134 pub original_dim: usize,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct PqCodebook {
145 pub centroids: Vec<Vec<Vec<f32>>>,
148 pub num_subvectors: usize,
150 pub subvector_dim: usize,
152}
153
154impl PqCodebook {
155 pub fn build(vectors: &[Vec<f32>], dim: usize) -> Self {
161 let num_sub = PQ_SUBVECTORS.min(dim);
162 let sub_dim = dim / num_sub;
163 let remainder = dim % num_sub;
164
165 let mut centroids = Vec::with_capacity(num_sub);
166
167 for s in 0..num_sub {
168 let start = s * sub_dim + s.min(remainder);
169 let actual_sub_dim = sub_dim + if s < remainder { 1 } else { 0 };
170
171 let mut mins = vec![f32::MAX; actual_sub_dim];
173 let mut maxs = vec![f32::MIN; actual_sub_dim];
174
175 for vec in vectors {
176 for d in 0..actual_sub_dim {
177 if start + d < vec.len() {
178 let v = vec[start + d];
179 if v < mins[d] {
180 mins[d] = v;
181 }
182 if v > maxs[d] {
183 maxs[d] = v;
184 }
185 }
186 }
187 }
188
189 let mut sub_centroids = Vec::with_capacity(PQ_CODEBOOK_SIZE);
191 for c in 0..PQ_CODEBOOK_SIZE {
192 let t = c as f32 / (PQ_CODEBOOK_SIZE - 1).max(1) as f32;
193 let centroid: Vec<f32> = (0..actual_sub_dim)
194 .map(|d| mins[d] + t * (maxs[d] - mins[d]))
195 .collect();
196 sub_centroids.push(centroid);
197 }
198 centroids.push(sub_centroids);
199 }
200
201 PqCodebook {
202 centroids,
203 num_subvectors: num_sub,
204 subvector_dim: sub_dim,
205 }
206 }
207
208 pub fn encode(&self, vector: &[f32]) -> PqVector {
210 let dim = vector.len();
211 let remainder = dim % self.num_subvectors;
212 let mut codes = Vec::with_capacity(self.num_subvectors);
213
214 for s in 0..self.num_subvectors {
215 let start = s * self.subvector_dim + s.min(remainder);
216 let actual_sub_dim = self.subvector_dim
217 + if s < remainder { 1 } else { 0 };
218 let end = (start + actual_sub_dim).min(dim);
219
220 let sub = &vector[start..end];
221
222 let mut best_code: u8 = 0;
224 let mut best_dist = f32::MAX;
225
226 for (c, centroid) in self.centroids[s].iter().enumerate() {
227 let dist: f32 = sub
228 .iter()
229 .zip(centroid.iter())
230 .map(|(a, b)| (a - b) * (a - b))
231 .sum();
232 if dist < best_dist {
233 best_dist = dist;
234 best_code = c as u8;
235 }
236 }
237 codes.push(best_code);
238 }
239
240 PqVector {
241 codes,
242 original_dim: dim,
243 }
244 }
245
246 pub fn decode(&self, pq: &PqVector) -> Vec<f32> {
248 let dim = pq.original_dim;
249 let mut result = Vec::with_capacity(dim);
250
251 for s in 0..self.num_subvectors {
252 let code = pq.codes[s] as usize;
253 let centroid = &self.centroids[s][code];
254 result.extend_from_slice(centroid);
255 }
256
257 result.truncate(dim);
258 result
259 }
260}
261
262#[non_exhaustive]
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub enum QuantizedVector {
268 Hot(Vec<f32>),
270 Warm(Vec<u16>),
272 Cold(PqVector),
274}
275
276impl QuantizedVector {
277 pub fn decompress(&self, codebook: Option<&PqCodebook>) -> Vec<f32> {
279 match self {
280 QuantizedVector::Hot(v) => v.clone(),
281 QuantizedVector::Warm(fp16s) => {
282 fp16s.iter().map(|&h| fp16_to_f32(h)).collect()
283 }
284 QuantizedVector::Cold(pq) => {
285 if let Some(cb) = codebook {
286 cb.decode(pq)
287 } else {
288 vec![0.0; pq.original_dim]
290 }
291 }
292 }
293 }
294
295 pub fn temperature(&self) -> Temperature {
297 match self {
298 QuantizedVector::Hot(_) => Temperature::Hot,
299 QuantizedVector::Warm(_) => Temperature::Warm,
300 QuantizedVector::Cold(_) => Temperature::Cold,
301 }
302 }
303
304 pub fn compress(
306 vector: &[f32],
307 tier: Temperature,
308 codebook: Option<&PqCodebook>,
309 ) -> Self {
310 match tier {
311 Temperature::Hot => QuantizedVector::Hot(vector.to_vec()),
312 Temperature::Warm => {
313 let fp16s: Vec<u16> =
314 vector.iter().map(|&v| f32_to_fp16(v)).collect();
315 QuantizedVector::Warm(fp16s)
316 }
317 Temperature::Cold => {
318 if let Some(cb) = codebook {
319 QuantizedVector::Cold(cb.encode(vector))
320 } else {
321 let fp16s: Vec<u16> =
323 vector.iter().map(|&v| f32_to_fp16(v)).collect();
324 QuantizedVector::Warm(fp16s)
325 }
326 }
327 }
328 }
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct AccessTracker {
336 pub access_count: u64,
338 pub last_access: u64,
340 pub temperature: Temperature,
342}
343
344impl AccessTracker {
345 pub fn new() -> Self {
347 Self {
348 access_count: 0,
349 last_access: now_secs(),
350 temperature: Temperature::Hot,
351 }
352 }
353
354 pub fn record_access(&mut self) {
356 self.access_count += 1;
357 self.last_access = now_secs();
358 }
359
360 pub fn recommended_tier(&self) -> Temperature {
366 let now = now_secs();
367 let age_secs = now.saturating_sub(self.last_access);
368
369 if age_secs < 3600 || self.access_count > 10 {
370 Temperature::Hot
371 } else if age_secs < 86400 || self.access_count > 3 {
372 Temperature::Warm
373 } else {
374 Temperature::Cold
375 }
376 }
377}
378
379impl Default for AccessTracker {
380 fn default() -> Self {
381 Self::new()
382 }
383}
384
385fn now_secs() -> u64 {
386 std::time::SystemTime::now()
387 .duration_since(std::time::UNIX_EPOCH)
388 .unwrap_or_default()
389 .as_secs()
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 #[test]
399 fn fp16_roundtrip_zero() {
400 let h = f32_to_fp16(0.0);
401 let v = fp16_to_f32(h);
402 assert!((v - 0.0).abs() < f32::EPSILON);
403 }
404
405 #[test]
406 fn fp16_roundtrip_one() {
407 let h = f32_to_fp16(1.0);
408 let v = fp16_to_f32(h);
409 assert!((v - 1.0).abs() < 0.001);
410 }
411
412 #[test]
413 fn fp16_roundtrip_negative() {
414 let h = f32_to_fp16(-0.5);
415 let v = fp16_to_f32(h);
416 assert!((v - (-0.5)).abs() < 0.001);
417 }
418
419 #[test]
420 fn fp16_roundtrip_small_value() {
421 let h = f32_to_fp16(0.1);
422 let v = fp16_to_f32(h);
423 assert!((v - 0.1).abs() < 0.01);
424 }
425
426 #[test]
427 fn fp16_overflow_becomes_inf() {
428 let h = f32_to_fp16(100_000.0);
429 let v = fp16_to_f32(h);
430 assert!(v.is_infinite());
431 }
432
433 #[test]
436 fn hot_decompress_is_identity() {
437 let v = vec![1.0, 2.0, 3.0];
438 let qv = QuantizedVector::Hot(v.clone());
439 assert_eq!(qv.decompress(None), v);
440 assert_eq!(qv.temperature(), Temperature::Hot);
441 }
442
443 #[test]
444 fn warm_decompress_approximate() {
445 let v = vec![0.5, -0.3, 0.7];
446 let qv = QuantizedVector::compress(&v, Temperature::Warm, None);
447 let decompressed = qv.decompress(None);
448
449 assert_eq!(qv.temperature(), Temperature::Warm);
450 assert_eq!(decompressed.len(), v.len());
451
452 for (orig, dec) in v.iter().zip(decompressed.iter()) {
453 assert!(
454 (orig - dec).abs() < 0.01,
455 "fp16 roundtrip error too large: {orig} vs {dec}"
456 );
457 }
458 }
459
460 #[test]
461 fn cold_with_codebook_roundtrip() {
462 let vectors = vec![
463 vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
464 vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
465 vec![0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
466 ];
467 let codebook = PqCodebook::build(&vectors, 8);
468
469 let qv = QuantizedVector::compress(
470 &vectors[0],
471 Temperature::Cold,
472 Some(&codebook),
473 );
474 assert_eq!(qv.temperature(), Temperature::Cold);
475
476 let decompressed = qv.decompress(Some(&codebook));
477 assert_eq!(decompressed.len(), 8);
478
479 assert!(decompressed[0] > decompressed[1]);
481 }
482
483 #[test]
484 fn cold_without_codebook_fallback() {
485 let v = vec![1.0, 2.0, 3.0];
486 let qv = QuantizedVector::compress(&v, Temperature::Cold, None);
487 assert_eq!(qv.temperature(), Temperature::Warm);
489 }
490
491 #[test]
494 fn new_tracker_is_hot() {
495 let tracker = AccessTracker::new();
496 assert_eq!(tracker.temperature, Temperature::Hot);
497 assert_eq!(tracker.access_count, 0);
498 }
499
500 #[test]
501 fn record_access_increments_count() {
502 let mut tracker = AccessTracker::new();
503 tracker.record_access();
504 tracker.record_access();
505 assert_eq!(tracker.access_count, 2);
506 }
507
508 #[test]
509 fn recent_access_stays_hot() {
510 let tracker = AccessTracker {
511 access_count: 1,
512 last_access: now_secs(),
513 temperature: Temperature::Hot,
514 };
515 assert_eq!(tracker.recommended_tier(), Temperature::Hot);
516 }
517
518 #[test]
519 fn many_accesses_stays_hot() {
520 let tracker = AccessTracker {
521 access_count: 15,
522 last_access: now_secs() - 7200, temperature: Temperature::Hot,
524 };
525 assert_eq!(tracker.recommended_tier(), Temperature::Hot);
526 }
527
528 #[test]
529 fn moderate_access_becomes_warm() {
530 let tracker = AccessTracker {
531 access_count: 5,
532 last_access: now_secs() - 7200, temperature: Temperature::Hot,
534 };
535 assert_eq!(tracker.recommended_tier(), Temperature::Warm);
536 }
537
538 #[test]
539 fn old_low_access_becomes_cold() {
540 let tracker = AccessTracker {
541 access_count: 1,
542 last_access: now_secs() - 200_000, temperature: Temperature::Hot,
544 };
545 assert_eq!(tracker.recommended_tier(), Temperature::Cold);
546 }
547
548 #[test]
551 fn temperature_display() {
552 assert_eq!(format!("{}", Temperature::Hot), "hot");
553 assert_eq!(format!("{}", Temperature::Warm), "warm");
554 assert_eq!(format!("{}", Temperature::Cold), "cold");
555 }
556
557 #[test]
560 fn codebook_build_creates_centroids() {
561 let vectors = vec![
562 vec![1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
563 vec![0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
564 ];
565 let codebook = PqCodebook::build(&vectors, 8);
566 assert_eq!(codebook.num_subvectors, PQ_SUBVECTORS);
567 assert_eq!(codebook.centroids.len(), PQ_SUBVECTORS);
568 for sub in &codebook.centroids {
569 assert_eq!(sub.len(), PQ_CODEBOOK_SIZE);
570 }
571 }
572
573 #[test]
574 fn codebook_encode_produces_correct_code_count() {
575 let vectors = vec![vec![1.0; 8], vec![0.0; 8]];
576 let codebook = PqCodebook::build(&vectors, 8);
577 let pq = codebook.encode(&[0.5; 8]);
578 assert_eq!(pq.codes.len(), PQ_SUBVECTORS);
579 assert_eq!(pq.original_dim, 8);
580 }
581
582 #[test]
583 fn codebook_decode_correct_length() {
584 let vectors = vec![vec![1.0; 8], vec![0.0; 8]];
585 let codebook = PqCodebook::build(&vectors, 8);
586 let pq = codebook.encode(&[0.5; 8]);
587 let decoded = codebook.decode(&pq);
588 assert_eq!(decoded.len(), 8);
589 }
590
591 #[test]
592 fn default_access_tracker() {
593 let tracker = AccessTracker::default();
594 assert_eq!(tracker.access_count, 0);
595 }
596}