1use half::f16;
2use serde::{Deserialize, Serialize};
3
4use crate::{
5 bitpack::{pack_indices, unpack_indices},
6 codebook::FibCodebookV1,
7 digest::{bytes_digest, json_digest},
8 metrics,
9 profile::{FibQuantProfileV1, NormFormat},
10 receipt::FibQuantCompressionReceiptV1,
11 rotation::StoredRotation,
12 FibQuantError, Result,
13};
14
15pub const CODE_SCHEMA: &str = "fib_code_v1";
16
17pub const COMPACT_MAGIC: [u8; 3] = [b'F', b'B', b'1'];
21pub const COMPACT_VERSION: u8 = 1;
22
23pub const BATCHED_MAGIC: [u8; 3] = [b'F', b'B', b'2'];
28pub const BATCHED_VERSION: u8 = 1;
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct FibCodeV1 {
33 pub schema_version: String,
35 pub profile_digest: String,
37 pub codebook_digest: String,
39 pub rotation_digest: String,
41 pub ambient_dim: u32,
43 pub block_dim: u32,
45 pub norm_format: NormFormat,
47 pub norm_payload: Vec<u8>,
49 pub wire_index_bits: u8,
51 pub block_count: u32,
53 pub indices: Vec<u8>,
55}
56
57impl FibCodeV1 {
58 pub fn to_compact_bytes(&self) -> Vec<u8> {
82 let mut out = Vec::with_capacity(11 + self.norm_payload.len() + self.indices.len());
83 out.extend_from_slice(&COMPACT_MAGIC);
84 out.push(COMPACT_VERSION);
85 out.push(self.wire_index_bits);
86 out.extend_from_slice(&self.block_count.to_le_bytes());
87 let norm_len = self.norm_payload.len() as u16;
88 out.extend_from_slice(&norm_len.to_le_bytes());
89 out.extend_from_slice(&self.norm_payload);
90 out.extend_from_slice(&self.indices);
91 out
92 }
93
94 pub fn from_compact_bytes(bytes: &[u8], profile: &FibQuantProfileV1) -> Result<Self> {
102 if bytes.len() < 11 {
103 return Err(FibQuantError::CorruptPayload(format!(
104 "compact FibCodeV1 too short: {} bytes (need >= 11)",
105 bytes.len()
106 )));
107 }
108 if bytes[0..3] != COMPACT_MAGIC {
109 return Err(FibQuantError::CorruptPayload(format!(
110 "compact FibCodeV1 bad magic: {:?} (expected {:?})",
111 &bytes[0..3],
112 COMPACT_MAGIC
113 )));
114 }
115 if bytes[3] != COMPACT_VERSION {
116 return Err(FibQuantError::CorruptPayload(format!(
117 "compact FibCodeV1 version {} not supported (need {})",
118 bytes[3], COMPACT_VERSION
119 )));
120 }
121 let wire_index_bits = bytes[4];
122 let block_count = u32::from_le_bytes([bytes[5], bytes[6], bytes[7], bytes[8]]);
123 let norm_len = u16::from_le_bytes([bytes[9], bytes[10]]) as usize;
124 let header_len = 11;
125 if bytes.len() < header_len + norm_len {
126 return Err(FibQuantError::CorruptPayload(format!(
127 "compact FibCodeV1 truncated: norm_len={} but only {} bytes remain",
128 norm_len,
129 bytes.len() - header_len
130 )));
131 }
132 let norm_payload = bytes[header_len..header_len + norm_len].to_vec();
133 let indices = bytes[header_len + norm_len..].to_vec();
134
135 let expected_packed_len = (block_count as usize)
137 .checked_mul(wire_index_bits as usize)
138 .map(|bits| (bits + 7) / 8)
139 .ok_or_else(|| {
140 FibQuantError::ResourceLimitExceeded("packed index bits overflow".into())
141 })?;
142 if indices.len() != expected_packed_len {
143 return Err(FibQuantError::CorruptPayload(format!(
144 "compact FibCodeV1 indices: got {} bytes, expected {} (block_count={} * wire_index_bits={})",
145 indices.len(),
146 expected_packed_len,
147 block_count,
148 wire_index_bits
149 )));
150 }
151
152 let profile_digest = profile.digest()?;
159 Ok(FibCodeV1 {
160 schema_version: CODE_SCHEMA.into(),
161 profile_digest,
162 codebook_digest: String::new(),
167 rotation_digest: String::new(),
168 ambient_dim: profile.ambient_dim,
169 block_dim: profile.block_dim,
170 norm_format: profile.norm_format.clone(),
171 norm_payload,
172 wire_index_bits,
173 block_count,
174 indices,
175 })
176 }
177
178 pub fn compact_size(&self) -> usize {
180 11 + self.norm_payload.len() + self.indices.len()
181 }
182
183 pub fn encode_batch(codes: &[FibCodeV1], profile: &FibQuantProfileV1) -> Result<Vec<u8>> {
226 if codes.is_empty() {
227 return Err(FibQuantError::CorruptPayload("empty batch".into()));
228 }
229 let wire_index_bits = codes[0].wire_index_bits;
231 let block_count = codes[0].block_count;
232 let norm_format = codes[0].norm_format.clone();
233 let norm_len = codes[0].norm_payload.len();
234 let indices_len = codes[0].indices.len();
235 for (i, code) in codes.iter().enumerate() {
236 if code.wire_index_bits != wire_index_bits {
237 return Err(FibQuantError::CorruptPayload(format!(
238 "batched wire block {i} wire_index_bits {} != header {}",
239 code.wire_index_bits, wire_index_bits
240 )));
241 }
242 if code.block_count != block_count {
243 return Err(FibQuantError::CorruptPayload(format!(
244 "batched wire block {i} block_count {} != header {}",
245 code.block_count, block_count
246 )));
247 }
248 if code.norm_format != norm_format {
249 return Err(FibQuantError::CorruptPayload(format!(
250 "batched wire block {i} norm_format mismatch"
251 )));
252 }
253 if code.norm_payload.len() != norm_len {
254 return Err(FibQuantError::CorruptPayload(format!(
255 "batched wire block {i} norm_payload_len {} != header {}",
256 code.norm_payload.len(),
257 norm_len
258 )));
259 }
260 if code.indices.len() != indices_len {
261 return Err(FibQuantError::CorruptPayload(format!(
262 "batched wire block {i} indices_len {} != header {}",
263 code.indices.len(),
264 indices_len
265 )));
266 }
267 }
268 let norm_format_tag: u8 = match norm_format {
269 NormFormat::Fp16Paper => 0,
270 NormFormat::F32Reference => 1,
271 };
272 let norm_len_u16 = u16::try_from(norm_len).map_err(|_| {
273 FibQuantError::ResourceLimitExceeded(format!(
274 "batched wire norm_payload_len {norm_len} exceeds u16::MAX"
275 ))
276 })?;
277 let indices_len_u16 = u16::try_from(indices_len).map_err(|_| {
278 FibQuantError::ResourceLimitExceeded(format!(
279 "batched wire indices_len {indices_len} exceeds u16::MAX"
280 ))
281 })?;
282 let n_blocks = u32::try_from(codes.len()).map_err(|_| {
283 FibQuantError::ResourceLimitExceeded("batched wire n_blocks exceeds u32::MAX".into())
284 })?;
285 let block_payload_len = norm_len + indices_len;
286 let total_payload = block_payload_len * codes.len();
287 let mut bytes = Vec::with_capacity(19 + total_payload);
288 bytes.extend_from_slice(&BATCHED_MAGIC);
289 bytes.push(BATCHED_VERSION);
290 bytes.push(wire_index_bits);
291 bytes.push(0); bytes.extend_from_slice(&block_count.to_le_bytes());
293 bytes.extend_from_slice(&n_blocks.to_le_bytes());
294 bytes.push(norm_format_tag);
295 bytes.extend_from_slice(&norm_len_u16.to_le_bytes());
296 bytes.extend_from_slice(&indices_len_u16.to_le_bytes());
297 for code in codes {
298 bytes.extend_from_slice(&code.norm_payload);
299 bytes.extend_from_slice(&code.indices);
300 }
301 if profile.wire_index_bits != wire_index_bits {
305 return Err(FibQuantError::ProfileDigestMismatch {
306 expected: profile.digest().unwrap_or_default(),
307 actual: format!(
308 "header wire_index_bits={wire_index_bits} does not match profile {}",
309 profile.wire_index_bits
310 ),
311 });
312 }
313 if profile.block_count() != block_count {
314 return Err(FibQuantError::CorruptPayload(format!(
315 "batched wire header block_count {block_count} != profile block_count {}",
316 profile.block_count()
317 )));
318 }
319 Ok(bytes)
320 }
321
322 pub fn decode_batch(bytes: &[u8], profile: &FibQuantProfileV1) -> Result<Vec<FibCodeV1>> {
324 if bytes.len() < 19 {
325 return Err(FibQuantError::CorruptPayload(format!(
326 "batched FibCodeV1 too short: {} bytes (need >= 19)",
327 bytes.len()
328 )));
329 }
330 if bytes[0..3] != BATCHED_MAGIC {
331 return Err(FibQuantError::CorruptPayload(format!(
332 "batched FibCodeV1 bad magic: {:?} (expected {:?})",
333 &bytes[0..3],
334 BATCHED_MAGIC
335 )));
336 }
337 if bytes[3] != BATCHED_VERSION {
338 return Err(FibQuantError::CorruptPayload(format!(
339 "batched FibCodeV1 version {} not supported (need {})",
340 bytes[3], BATCHED_VERSION
341 )));
342 }
343 let wire_index_bits = bytes[4];
344 let _reserved = bytes[5];
345 let block_count = u32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]);
346 let n_blocks = u32::from_le_bytes([bytes[10], bytes[11], bytes[12], bytes[13]]) as usize;
347 let norm_format_tag = bytes[14];
348 let norm_len =
349 u16::from_le_bytes([bytes[15], bytes[16]]) as usize;
350 let indices_len =
351 u16::from_le_bytes([bytes[17], bytes[18]]) as usize;
352 if wire_index_bits != profile.wire_index_bits {
354 return Err(FibQuantError::CorruptPayload(format!(
355 "batched wire wire_index_bits {wire_index_bits} != profile {}",
356 profile.wire_index_bits
357 )));
358 }
359 if block_count != profile.block_count() {
360 return Err(FibQuantError::CorruptPayload(format!(
361 "batched wire block_count {block_count} != profile {}",
362 profile.block_count()
363 )));
364 }
365 let norm_format = match norm_format_tag {
366 0 => NormFormat::Fp16Paper,
367 1 => NormFormat::F32Reference,
368 other => {
369 return Err(FibQuantError::CorruptPayload(format!(
370 "batched wire unknown norm_format tag {other}"
371 )));
372 }
373 };
374 if norm_format != profile.norm_format {
375 return Err(FibQuantError::CorruptPayload(format!(
376 "batched wire norm_format tag {norm_format:?} != profile {:?}",
377 profile.norm_format
378 )));
379 }
380 let block_payload_len = norm_len + indices_len;
381 let expected_total = 19 + n_blocks * block_payload_len;
382 if bytes.len() < expected_total {
383 return Err(FibQuantError::CorruptPayload(format!(
384 "batched wire buffer {} bytes < expected {} for {n_blocks} blocks",
385 bytes.len(),
386 expected_total
387 )));
388 }
389 let expected_packed_len = (block_count as usize)
391 .checked_mul(wire_index_bits as usize)
392 .map(|bits| (bits + 7) / 8)
393 .ok_or_else(|| {
394 FibQuantError::ResourceLimitExceeded("packed index bits overflow".into())
395 })?;
396 if indices_len != expected_packed_len {
397 return Err(FibQuantError::CorruptPayload(format!(
398 "batched wire indices_len {indices_len} != expected {expected_packed_len} (block_count={block_count} * wire_index_bits={wire_index_bits})"
399 )));
400 }
401 let profile_digest = profile.digest()?;
402 let mut codes = Vec::with_capacity(n_blocks);
403 let mut cursor = 19;
404 for _ in 0..n_blocks {
405 let norm_payload = bytes[cursor..cursor + norm_len].to_vec();
406 cursor += norm_len;
407 let indices = bytes[cursor..cursor + indices_len].to_vec();
408 cursor += indices_len;
409 codes.push(FibCodeV1 {
410 schema_version: CODE_SCHEMA.into(),
411 profile_digest: profile_digest.clone(),
412 codebook_digest: String::new(),
415 rotation_digest: String::new(),
416 ambient_dim: profile.ambient_dim,
417 block_dim: profile.block_dim,
418 norm_format: norm_format.clone(),
419 norm_payload,
420 wire_index_bits,
421 block_count,
422 indices,
423 });
424 }
425 Ok(codes)
426 }
427}
428
429#[derive(Debug, Clone)]
431pub struct FibQuantizer {
432 profile: FibQuantProfileV1,
433 codebook: FibCodebookV1,
434 rotation: StoredRotation,
435}
436
437impl FibQuantizer {
438 pub fn new(profile: FibQuantProfileV1) -> Result<Self> {
440 let codebook = FibCodebookV1::build(profile)?;
441 Self::from_codebook(codebook)
442 }
443
444 pub fn from_codebook(codebook: FibCodebookV1) -> Result<Self> {
446 codebook.validate()?;
447 let profile = codebook.profile.clone();
448 let rotation = StoredRotation::new(profile.ambient_dim as usize, profile.rotation_seed)?;
449 Ok(Self {
450 profile,
451 codebook,
452 rotation,
453 })
454 }
455
456 pub fn profile(&self) -> &FibQuantProfileV1 {
458 &self.profile
459 }
460
461 pub fn codebook(&self) -> &FibCodebookV1 {
463 &self.codebook
464 }
465
466 pub fn encode(&self, x: &[f32]) -> Result<FibCodeV1> {
468 let d = self.profile.ambient_dim as usize;
469 let k = self.profile.block_dim as usize;
470 if x.len() != d {
471 return Err(FibQuantError::CorruptPayload(format!(
472 "input dimension {}, expected {d}",
473 x.len()
474 )));
475 }
476 check_finite(x)?;
477 let norm = l2_norm(x);
478 if norm == 0.0 {
479 return Err(FibQuantError::ZeroNorm);
480 }
481 let normalized: Vec<f64> = x.iter().map(|value| f64::from(*value) / norm).collect();
484 let rotated_f64 = self.rotation.apply(&normalized)?;
485 let rotated_f32: Vec<f32> = rotated_f64.iter().map(|&v| v as f32).collect();
486 let block_count = self.profile.block_count() as usize;
487 let mut indices = Vec::with_capacity(block_count);
488 for block in rotated_f32.chunks_exact(k) {
489 indices.push(gpu_backend::nearest_codeword_f32(block, &self.codebook.codewords, k) as u32);
490 }
491 Ok(FibCodeV1 {
492 schema_version: CODE_SCHEMA.into(),
493 profile_digest: self.profile.digest()?,
494 codebook_digest: self.codebook.codebook_digest.clone(),
495 rotation_digest: self.rotation.digest()?,
496 ambient_dim: self.profile.ambient_dim,
497 block_dim: self.profile.block_dim,
498 norm_format: self.profile.norm_format.clone(),
499 norm_payload: encode_norm(norm, &self.profile.norm_format)?,
500 wire_index_bits: self.profile.wire_index_bits,
501 block_count: self.profile.block_count(),
502 indices: pack_indices(&indices, self.profile.wire_index_bits)?,
503 })
504 }
505
506 pub fn decode(&self, code: &FibCodeV1) -> Result<Vec<f32>> {
508 self.validate_code_header(code)?;
509 let k = self.profile.block_dim as usize;
510 let block_count = self.profile.block_count() as usize;
511 let unpacked = unpack_indices(&code.indices, block_count, self.profile.wire_index_bits)?;
512 let mut rotated = Vec::with_capacity(self.profile.ambient_dim as usize);
513 for index in unpacked {
514 if index >= self.profile.codebook_size {
515 return Err(FibQuantError::IndexOutOfRange {
516 index,
517 codebook_size: self.profile.codebook_size,
518 });
519 }
520 rotated.extend(self.codebook.codeword(index as usize)?);
521 }
522 let expected_rotated_len = block_count.checked_mul(k).ok_or_else(|| {
523 FibQuantError::ResourceLimitExceeded("decoded rotated vector length overflow".into())
524 })?;
525 if rotated.len() != expected_rotated_len {
526 return Err(FibQuantError::CorruptPayload(
527 "decoded rotated vector length mismatch".into(),
528 ));
529 }
530 let norm = decode_norm(&code.norm_payload, &code.norm_format)?;
531 let reconstructed = self.rotation.apply_inverse(&rotated)?;
532 let out: Vec<f32> = reconstructed
533 .into_iter()
534 .map(|value| (value * norm) as f32)
535 .collect();
536 check_finite(&out)?;
537 Ok(out)
538 }
539
540 pub fn encode_with_receipt(
542 &self,
543 x: &[f32],
544 ) -> Result<(FibCodeV1, FibQuantCompressionReceiptV1)> {
545 let code = self.encode(x)?;
546 let source_vector_digest = source_vector_digest(x)?;
547 let mut receipt = FibQuantCompressionReceiptV1::new(
548 &self.profile,
549 code.profile_digest.clone(),
550 code.codebook_digest.clone(),
551 code.rotation_digest.clone(),
552 source_vector_digest,
553 encoded_digest(&code)?,
554 );
555 let decoded = self.decode(&code)?;
556 receipt.mse = Some(metrics::mse(x, &decoded)?);
557 receipt.cosine_similarity = Some(metrics::cosine_similarity(x, &decoded)?);
558 Ok((code, receipt))
559 }
560
561 pub fn reconstruction_mse(&self, x: &[f32]) -> Result<f64> {
563 let code = self.encode(x)?;
564 let decoded = self.decode(&code)?;
565 metrics::mse(x, &decoded)
566 }
567
568 pub fn cosine_similarity(&self, x: &[f32]) -> Result<f64> {
570 let code = self.encode(x)?;
571 let decoded = self.decode(&code)?;
572 metrics::cosine_similarity(x, &decoded)
573 }
574
575 pub fn encode_batch(&self, vectors: &[&[f32]]) -> Result<Vec<FibCodeV1>> {
580 let d = self.profile.ambient_dim as usize;
581 let k = self.profile.block_dim as usize;
582 let n = vectors.len();
583 if n == 0 {
584 return Ok(vec![]);
585 }
586
587 if n < 4 {
589 return vectors.iter().map(|v| self.encode(v)).collect();
590 }
591
592 let mut flat = Vec::with_capacity(n * d);
594 let mut norms_f64 = Vec::with_capacity(n);
595 for v in vectors {
596 if v.len() != d {
597 return Err(FibQuantError::CorruptPayload(format!(
598 "input dimension {}, expected {d}",
599 v.len()
600 )));
601 }
602 check_finite(v)?;
603 let norm = l2_norm(v);
604 if norm == 0.0 {
605 return Err(FibQuantError::ZeroNorm);
606 }
607 norms_f64.push(norm);
608 for &x in *v {
609 flat.push((x as f64 / norm) as f32);
610 }
611 }
612
613 #[cfg(feature = "gpu")]
615 {
616 if let Some(_ctx) = gpu_backend::GpuContext::init() {
617 if n >= gpu_backend::GpuContext::GPU_MIN_BATCH_SIZE
618 && d >= gpu_backend::GpuContext::GPU_MIN_DIM
619 {
620 gpu_backend::hadamard_batch(&mut flat, n, d, self.profile.rotation_seed)
621 .map_err(|e| {
622 FibQuantError::NumericalFailure(format!("gpu hadamard: {}", e))
623 })?;
624
625 #[cfg(feature = "gpu_codebook_lookup")]
636 {
637 let block_count = self.profile.block_count() as usize;
638 if let Ok(indices) = gpu_backend::codebook_lookup_batch(
639 &flat,
640 &self.codebook.codewords,
641 n,
642 d,
643 k,
644 ) {
645 if indices.len() == n * block_count {
646 return self.finish_batch_encode_with_indices(
647 &flat, &norms_f64, &indices, n, d, k,
648 );
649 }
650 }
652 }
653
654 return self.finish_batch_encode(&flat, &norms_f64, n, d, k);
656 }
657 }
658 }
659
660 let mut rotated_flat = Vec::with_capacity(n * d);
662 for chunk in flat.chunks_exact(d) {
663 let f64_chunk: Vec<f64> = chunk.iter().map(|&v| v as f64).collect();
664 let rot = self.rotation.apply(&f64_chunk)?;
665 rotated_flat.extend(rot.iter().map(|&v| v as f32));
666 }
667
668 self.finish_batch_encode(&rotated_flat, &norms_f64, n, d, k)
669 }
670
671 fn finish_batch_encode(
672 &self,
673 rotated: &[f32],
674 norms: &[f64],
675 n: usize,
676 d: usize,
677 k: usize,
678 ) -> Result<Vec<FibCodeV1>> {
679 let profile_digest = self.profile.digest()?;
683 let codebook_digest = self.codebook.codebook_digest.clone();
684 let rotation_digest = self.rotation.digest()?;
685 let profile = &self.profile;
686 let codewords_f32: &[f32] = &self.codebook.codewords;
687
688 let per_vec = |vec_idx: usize| -> Result<FibCodeV1> {
692 let start = vec_idx * d;
693 let chunk = &rotated[start..start + d];
694 let mut indices = Vec::with_capacity(profile.block_count() as usize);
695 for block in chunk.chunks_exact(k) {
696 indices.push(gpu_backend::nearest_codeword_f32(block, codewords_f32, k) as u32);
697 }
698 Ok(FibCodeV1 {
699 schema_version: CODE_SCHEMA.into(),
700 profile_digest: profile_digest.clone(),
701 codebook_digest: codebook_digest.clone(),
702 rotation_digest: rotation_digest.clone(),
703 ambient_dim: profile.ambient_dim,
704 block_dim: profile.block_dim,
705 norm_format: profile.norm_format.clone(),
706 norm_payload: encode_norm(norms[vec_idx], &profile.norm_format)?,
707 wire_index_bits: profile.wire_index_bits,
708 block_count: profile.block_count(),
709 indices: pack_indices(&indices, profile.wire_index_bits)?,
710 })
711 };
712
713 #[cfg(feature = "parallel")]
717 {
718 const RAYON_MIN_N: usize = 16;
719 if n >= RAYON_MIN_N {
720 use rayon::prelude::*;
721 return (0..n).into_par_iter().map(per_vec).collect();
722 }
723 }
724
725 let mut codes = Vec::with_capacity(n);
726 for vec_idx in 0..n {
727 codes.push(per_vec(vec_idx)?);
728 }
729 Ok(codes)
730 }
731
732 #[cfg(all(feature = "gpu", feature = "gpu_codebook_lookup"))]
736 fn finish_batch_encode_with_indices(
737 &self,
738 _rotated: &[f32], norms: &[f64],
740 indices: &[u32],
741 n: usize,
742 _d: usize,
743 _k: usize,
744 ) -> Result<Vec<FibCodeV1>> {
745 let block_count = self.profile.block_count() as usize;
746 if indices.len() != n * block_count {
747 return Err(FibQuantError::CorruptPayload(format!(
748 "indices length {} != n * block_count {}",
749 indices.len(),
750 n * block_count
751 )));
752 }
753
754 let mut codes = Vec::with_capacity(n);
755 for vec_idx in 0..n {
756 let start = vec_idx * block_count;
757 let end = start + block_count;
758 let vec_indices: Vec<u32> = indices[start..end].to_vec();
759
760 codes.push(FibCodeV1 {
761 schema_version: CODE_SCHEMA.into(),
762 profile_digest: self.profile.digest()?,
763 codebook_digest: self.codebook.codebook_digest.clone(),
764 rotation_digest: self.rotation.digest()?,
765 ambient_dim: self.profile.ambient_dim,
766 block_dim: self.profile.block_dim,
767 norm_format: self.profile.norm_format.clone(),
768 norm_payload: encode_norm(norms[vec_idx], &self.profile.norm_format)?,
769 wire_index_bits: self.profile.wire_index_bits,
770 block_count: self.profile.block_count(),
771 indices: pack_indices(&vec_indices, self.profile.wire_index_bits)?,
772 });
773 }
774 Ok(codes)
775 }
776
777 pub fn decode_batch(&self, codes: &[FibCodeV1]) -> Result<Vec<Vec<f32>>> {
779 codes.iter().map(|c| self.decode(c)).collect()
780 }
781
782 pub fn decode_batch_fast(&self, codes: &[FibCodeV1]) -> Result<Vec<Vec<f32>>> {
799 if codes.is_empty() {
800 return Ok(Vec::new());
801 }
802 let d = self.profile.ambient_dim as usize;
803 let k = self.profile.block_dim as usize;
804 let codebook_size = self.profile.codebook_size as usize;
805 let codewords = &self.codebook.codewords;
806 let mut out = Vec::with_capacity(codes.len());
807 for code in codes {
808 self.validate_code_header(code)?;
809 let block_count = self.profile.block_count() as usize;
810 let unpacked = unpack_indices(&code.indices, block_count, self.profile.wire_index_bits)?;
811 let expected_len = block_count.checked_mul(k).ok_or_else(|| {
812 FibQuantError::ResourceLimitExceeded("decoded rotated vector length overflow".into())
813 })?;
814 let mut rotated_f32: Vec<f32> = Vec::with_capacity(expected_len);
816 for &index in &unpacked {
817 let idx = index as usize;
818 if idx >= codebook_size {
819 return Err(FibQuantError::IndexOutOfRange {
820 index,
821 codebook_size: codebook_size as u32,
822 });
823 }
824 let base = idx * k;
825 rotated_f32.extend_from_slice(&codewords[base..base + k]);
827 }
828 debug_assert_eq!(rotated_f32.len(), expected_len);
829 let norm = decode_norm(&code.norm_payload, &code.norm_format)?;
830 let reconstructed = self.rotation.apply_inverse_f32(&rotated_f32)?;
835 let scaled: Vec<f32> = reconstructed
836 .into_iter()
837 .map(|value| (value * norm as f32))
838 .collect();
839 check_finite(&scaled)?;
840 out.push(scaled);
841 }
842 Ok(out)
843 }
844
845 pub fn is_gpu_accelerated(&self) -> bool {
854 #[cfg(feature = "gpu")]
855 {
856 gpu_backend::GpuContext::is_available()
857 }
858 #[cfg(not(feature = "gpu"))]
859 {
860 false
861 }
862 }
863
864 pub fn is_gpu_accelerated_for(&self, n: usize, d: usize) -> bool {
877 #[cfg(feature = "gpu")]
878 {
879 if !gpu_backend::GpuContext::is_available() {
880 return false;
881 }
882 n >= gpu_backend::GpuContext::GPU_MIN_BATCH_SIZE
883 && d >= gpu_backend::GpuContext::GPU_MIN_DIM
884 && (self.profile.codebook_size as usize) <= 32
885 }
886 #[cfg(not(feature = "gpu"))]
887 {
888 let _ = (n, d);
889 false
890 }
891 }
892
893 pub fn gpu_steps_for(&self, n: usize, d: usize) -> GpuStepReport {
901 let device_available = {
902 #[cfg(feature = "gpu")]
903 {
904 gpu_backend::GpuContext::is_available()
905 }
906 #[cfg(not(feature = "gpu"))]
907 {
908 false
909 }
910 };
911 const MIN_BATCH: usize = 16;
914 const MIN_DIM: usize = 64;
915 let clears_thresholds = n >= MIN_BATCH && d >= MIN_DIM;
916 let codebook_fits = (self.profile.codebook_size as usize) <= 32;
917 GpuStepReport {
918 hadamard: device_available && clears_thresholds,
919 codebook_lookup: device_available && clears_thresholds && codebook_fits,
920 }
921 }
922
923 fn validate_code_header(&self, code: &FibCodeV1) -> Result<()> {
926 if code.schema_version != CODE_SCHEMA {
927 return Err(FibQuantError::CorruptPayload(format!(
928 "code schema_version {}, expected {CODE_SCHEMA}",
929 code.schema_version
930 )));
931 }
932 let expected_profile = self.profile.digest()?;
933 if code.profile_digest != expected_profile {
934 return Err(FibQuantError::ProfileDigestMismatch {
935 expected: expected_profile,
936 actual: code.profile_digest.clone(),
937 });
938 }
939 if !code.codebook_digest.is_empty()
946 && code.codebook_digest != self.codebook.codebook_digest
947 {
948 return Err(FibQuantError::CodebookDigestMismatch {
949 expected: self.codebook.codebook_digest.clone(),
950 actual: code.codebook_digest.clone(),
951 });
952 }
953 let expected_rotation = self.rotation.digest()?;
954 if !code.rotation_digest.is_empty()
955 && (code.rotation_digest != expected_rotation
956 || code.rotation_digest != self.codebook.rotation_digest)
957 {
958 return Err(FibQuantError::RotationDigestMismatch {
959 expected: expected_rotation,
960 actual: code.rotation_digest.clone(),
961 });
962 }
963 if code.ambient_dim != self.profile.ambient_dim
964 || code.block_dim != self.profile.block_dim
965 || code.block_count != self.profile.block_count()
966 || code.wire_index_bits != self.profile.wire_index_bits
967 || code.norm_format != self.profile.norm_format
968 {
969 return Err(FibQuantError::CorruptPayload(
970 "encoded header does not match profile".into(),
971 ));
972 }
973 Ok(())
974 }
975}
976
977pub fn encoded_digest(code: &FibCodeV1) -> Result<String> {
979 json_digest(CODE_SCHEMA, code)
980}
981
982fn source_vector_digest(x: &[f32]) -> Result<String> {
983 check_finite(x)?;
984 let mut bytes = Vec::with_capacity(32 + std::mem::size_of_val(x));
985 bytes.extend_from_slice(b"fib_quant_source_vector_v1");
986 bytes.push(0);
987 bytes.extend_from_slice(&(x.len() as u64).to_le_bytes());
988 for value in x {
989 bytes.extend_from_slice(&value.to_le_bytes());
990 }
991 Ok(bytes_digest(&bytes))
992}
993
994fn encode_norm(norm: f64, format: &NormFormat) -> Result<Vec<u8>> {
995 if !norm.is_finite() || norm <= 0.0 {
996 return Err(FibQuantError::CorruptPayload(
997 "norm must be finite and positive".into(),
998 ));
999 }
1000 match format {
1001 NormFormat::Fp16Paper => {
1002 let narrowed = f16::from_f32(norm as f32);
1003 if !narrowed.is_finite() || narrowed <= f16::ZERO {
1004 return Err(FibQuantError::CorruptPayload(
1005 "norm cannot be represented as finite positive fp16".into(),
1006 ));
1007 }
1008 Ok(narrowed.to_le_bytes().to_vec())
1009 }
1010 NormFormat::F32Reference => {
1011 let narrowed = norm as f32;
1012 if !narrowed.is_finite() || narrowed <= 0.0 {
1013 return Err(FibQuantError::CorruptPayload(
1014 "norm cannot be represented as finite positive f32".into(),
1015 ));
1016 }
1017 Ok(narrowed.to_le_bytes().to_vec())
1018 }
1019 }
1020}
1021
1022fn decode_norm(bytes: &[u8], format: &NormFormat) -> Result<f64> {
1023 match format {
1024 NormFormat::Fp16Paper => {
1025 let bytes: [u8; 2] = bytes
1026 .try_into()
1027 .map_err(|_| FibQuantError::CorruptPayload("fp16 norm length".into()))?;
1028 let value = f16::from_le_bytes(bytes).to_f32() as f64;
1029 if value.is_finite() && value > 0.0 {
1030 Ok(value)
1031 } else {
1032 Err(FibQuantError::CorruptPayload("invalid fp16 norm".into()))
1033 }
1034 }
1035 NormFormat::F32Reference => {
1036 let bytes: [u8; 4] = bytes
1037 .try_into()
1038 .map_err(|_| FibQuantError::CorruptPayload("f32 norm length".into()))?;
1039 let value = f32::from_le_bytes(bytes) as f64;
1040 if value.is_finite() && value > 0.0 {
1041 Ok(value)
1042 } else {
1043 Err(FibQuantError::CorruptPayload("invalid f32 norm".into()))
1044 }
1045 }
1046 }
1047}
1048
1049fn l2_norm(x: &[f32]) -> f64 {
1050 x.iter()
1051 .map(|value| {
1052 let value = f64::from(*value);
1053 value * value
1054 })
1055 .sum::<f64>()
1056 .sqrt()
1057}
1058
1059fn check_finite(x: &[f32]) -> Result<()> {
1060 if let Some((idx, _)) = x.iter().enumerate().find(|(_, value)| !value.is_finite()) {
1061 return Err(FibQuantError::NonFiniteInput(idx));
1062 }
1063 Ok(())
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069
1070 #[test]
1071 fn f32_norm_overflow_rejects_before_payload_emit() {
1072 let err = encode_norm(f64::MAX, &NormFormat::F32Reference).unwrap_err();
1073 assert!(matches!(err, FibQuantError::CorruptPayload(message) if message.contains("f32")));
1074 }
1075
1076 #[test]
1077 fn f32_norm_underflow_rejects_before_payload_emit() {
1078 let err = encode_norm(
1079 f64::from(f32::from_bits(1)) / 2.0,
1080 &NormFormat::F32Reference,
1081 )
1082 .unwrap_err();
1083 assert!(matches!(err, FibQuantError::CorruptPayload(message) if message.contains("f32")));
1084 }
1085
1086 fn build_test_quantizer() -> (FibQuantProfileV1, FibQuantizer) {
1091 let profile = FibQuantProfileV1::paper_default(64, 4, 32, 42).unwrap();
1092 let quantizer = FibQuantizer::new(profile.clone()).unwrap();
1093 (profile, quantizer)
1094 }
1095
1096 #[test]
1097 fn batched_wire_roundtrip_matches_single() {
1098 let (profile, quantizer) = build_test_quantizer();
1099 let vectors: Vec<Vec<f32>> = (0..16)
1100 .map(|i| (0..64).map(|j| ((i * 64 + j) as f32 * 0.013).sin()).collect())
1101 .collect();
1102 let codes: Vec<_> = vectors
1103 .iter()
1104 .map(|v| quantizer.encode(v).unwrap())
1105 .collect();
1106 let single_bytes: Vec<Vec<u8>> = codes
1108 .iter()
1109 .map(|c| c.to_compact_bytes())
1110 .collect();
1111 let single_total: usize = single_bytes.iter().map(|b| b.len()).sum();
1112 let batched_bytes = FibCodeV1::encode_batch(&codes, &profile).unwrap();
1114 assert!(
1116 batched_bytes.len() < single_total,
1117 "batched {} >= single total {}",
1118 batched_bytes.len(),
1119 single_total
1120 );
1121 let expected = 19 + 16 * 12;
1126 assert_eq!(batched_bytes.len(), expected);
1127 let decoded = FibCodeV1::decode_batch(&batched_bytes, &profile).unwrap();
1129 assert_eq!(decoded.len(), codes.len());
1130 for (i, (orig, back)) in codes.iter().zip(decoded.iter()).enumerate() {
1131 assert_eq!(orig.norm_payload, back.norm_payload, "norm mismatch at vec {i}");
1132 assert_eq!(orig.indices, back.indices, "indices mismatch at vec {i}");
1133 assert_eq!(orig.wire_index_bits, back.wire_index_bits);
1134 assert_eq!(orig.block_count, back.block_count);
1135 assert_eq!(orig.norm_format, back.norm_format);
1136 }
1137 }
1138
1139 #[test]
1140 fn batched_wire_rejects_wrong_magic() {
1141 let (profile, _quantizer) = build_test_quantizer();
1142 let mut bytes = vec![0u8; 64];
1143 bytes[0..3].copy_from_slice(b"XXX");
1144 let r = FibCodeV1::decode_batch(&bytes, &profile);
1145 assert!(r.is_err());
1146 }
1147
1148 #[test]
1149 fn batched_wire_rejects_buffer_too_short() {
1150 let (profile, _quantizer) = build_test_quantizer();
1151 let mut bytes = b"FB2".to_vec();
1153 bytes.extend_from_slice(&[1u8, 0, 0, 0, 0, 0, 0]);
1154 let r = FibCodeV1::decode_batch(&bytes, &profile);
1155 assert!(r.is_err());
1156 }
1157
1158 #[test]
1159 fn batched_wire_preserves_f32_reconstruction() {
1160 let (profile, quantizer) = build_test_quantizer();
1164 let vectors: Vec<Vec<f32>> = (0..32)
1165 .map(|i| {
1166 (0..64)
1167 .map(|j| {
1168 let x = (i * 64 + j) as f32 * 0.013;
1169 x.sin() + x.cos() * 0.5
1170 })
1171 .collect()
1172 })
1173 .collect();
1174 let codes_fb1: Vec<_> = vectors
1176 .iter()
1177 .map(|v| quantizer.encode(v).unwrap())
1178 .collect();
1179 let decoded_fb1: Vec<Vec<f32>> = codes_fb1
1181 .iter()
1182 .map(|c| quantizer.decode(c).unwrap())
1183 .collect();
1184 let codes_fb2 = FibCodeV1::encode_batch(&codes_fb1, &profile).unwrap();
1186 let codes_back = FibCodeV1::decode_batch(&codes_fb2, &profile).unwrap();
1188 let decoded_fb2: Vec<Vec<f32>> = codes_back
1189 .iter()
1190 .map(|c| quantizer.decode(c).unwrap())
1191 .collect();
1192 assert_eq!(decoded_fb1.len(), decoded_fb2.len());
1194 for (i, (fb1, fb2)) in decoded_fb1.iter().zip(decoded_fb2.iter()).enumerate() {
1195 assert_eq!(fb1.len(), fb2.len());
1196 for (j, (&a, &b)) in fb1.iter().zip(fb2.iter()).enumerate() {
1197 assert_eq!(
1198 a.to_bits(),
1199 b.to_bits(),
1200 "f32 mismatch at vec {i} dim {j}: fb1={a} fb2={b}"
1201 );
1202 }
1203 }
1204 }
1205}
1206
1207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1209pub struct GpuStepReport {
1210 pub hadamard: bool,
1212 pub codebook_lookup: bool,
1214}