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 CODEC_ID: &str = "fib_quant";
20
21pub const COMPACT_MAGIC: [u8; 3] = [b'F', b'B', b'1'];
25pub const COMPACT_VERSION: u8 = 1;
26
27pub const COMPACT_V2_MAGIC: [u8; 3] = [b'F', b'B', b'2'];
32pub const COMPACT_V2_VERSION: u8 = 1;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct CompactFeatureFlags(pub u8);
39
40impl CompactFeatureFlags {
41 pub const NONE: Self = Self(0);
43 pub const SPARSE_INDICES: Self = Self(1 << 0);
45 pub const ALT_NORM_F32: Self = Self(1 << 1);
47 pub const ROTATION_SCHEMA_TAG: Self = Self(1 << 2);
49
50 pub fn contains(self, bit: Self) -> bool {
52 self.0 & bit.0 != 0
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct FibCodeV1 {
59 pub schema_version: String,
61 pub profile_digest: String,
63 pub codebook_digest: String,
65 pub rotation_digest: String,
67 pub ambient_dim: u32,
69 pub block_dim: u32,
71 pub norm_format: NormFormat,
73 pub norm_payload: Vec<u8>,
75 pub wire_index_bits: u8,
77 pub block_count: u32,
79 pub indices: Vec<u8>,
81}
82
83impl FibCodeV1 {
84 pub fn to_compact_bytes(&self) -> Vec<u8> {
108 let mut out = Vec::with_capacity(11 + self.norm_payload.len() + self.indices.len());
109 out.extend_from_slice(&COMPACT_MAGIC);
110 out.push(COMPACT_VERSION);
111 out.push(self.wire_index_bits);
112 out.extend_from_slice(&self.block_count.to_le_bytes());
113 let norm_len = self.norm_payload.len() as u16;
114 out.extend_from_slice(&norm_len.to_le_bytes());
115 out.extend_from_slice(&self.norm_payload);
116 out.extend_from_slice(&self.indices);
117 out
118 }
119
120 pub fn from_compact_bytes(bytes: &[u8], profile: &FibQuantProfileV1) -> Result<Self> {
128 if bytes.len() < 11 {
129 return Err(FibQuantError::CorruptPayload(format!(
130 "compact FibCodeV1 too short: {} bytes (need >= 11)",
131 bytes.len()
132 )));
133 }
134 if bytes[0..3] != COMPACT_MAGIC {
135 return Err(FibQuantError::CorruptPayload(format!(
136 "compact FibCodeV1 bad magic: {:?} (expected {:?})",
137 &bytes[0..3],
138 COMPACT_MAGIC
139 )));
140 }
141 if bytes[3] != COMPACT_VERSION {
142 return Err(FibQuantError::CorruptPayload(format!(
143 "compact FibCodeV1 version {} not supported (need {})",
144 bytes[3], COMPACT_VERSION
145 )));
146 }
147 let wire_index_bits = bytes[4];
148 let block_count = u32::from_le_bytes([bytes[5], bytes[6], bytes[7], bytes[8]]);
149 let norm_len = u16::from_le_bytes([bytes[9], bytes[10]]) as usize;
150 let header_len = 11;
151 if bytes.len() < header_len + norm_len {
152 return Err(FibQuantError::CorruptPayload(format!(
153 "compact FibCodeV1 truncated: norm_len={} but only {} bytes remain",
154 norm_len,
155 bytes.len() - header_len
156 )));
157 }
158 let norm_payload = bytes[header_len..header_len + norm_len].to_vec();
159 let indices = bytes[header_len + norm_len..].to_vec();
160
161 let expected_packed_len = (block_count as usize)
163 .checked_mul(wire_index_bits as usize)
164 .map(|bits| bits.div_ceil(8))
165 .ok_or_else(|| {
166 FibQuantError::ResourceLimitExceeded("packed index bits overflow".into())
167 })?;
168 if indices.len() != expected_packed_len {
169 return Err(FibQuantError::CorruptPayload(format!(
170 "compact FibCodeV1 indices: got {} bytes, expected {} (block_count={} * wire_index_bits={})",
171 indices.len(),
172 expected_packed_len,
173 block_count,
174 wire_index_bits
175 )));
176 }
177
178 let profile_digest = profile.digest()?;
185 Ok(FibCodeV1 {
186 schema_version: CODE_SCHEMA.into(),
187 profile_digest,
188 codebook_digest: String::new(),
193 rotation_digest: String::new(),
194 ambient_dim: profile.ambient_dim,
195 block_dim: profile.block_dim,
196 norm_format: profile.norm_format.clone(),
197 norm_payload,
198 wire_index_bits,
199 block_count,
200 indices,
201 })
202 }
203
204 pub fn compact_size(&self) -> usize {
206 11 + self.norm_payload.len() + self.indices.len()
207 }
208
209 pub fn to_compact_v2_bytes(&self, flags: CompactFeatureFlags) -> Vec<u8> {
225 self.to_compact_v2_bytes_with_flags(flags.0)
226 }
227
228 fn to_compact_v2_bytes_with_flags(&self, flags: u8) -> Vec<u8> {
231 let mut out = Vec::with_capacity(12 + self.norm_payload.len() + self.indices.len());
232 out.extend_from_slice(&COMPACT_V2_MAGIC);
233 out.push(COMPACT_V2_VERSION);
234 out.push(flags);
235 out.push(self.wire_index_bits);
236 out.extend_from_slice(&self.block_count.to_le_bytes());
237 let norm_len = self.norm_payload.len() as u16;
238 out.extend_from_slice(&norm_len.to_le_bytes());
239 out.extend_from_slice(&self.norm_payload);
240 out.extend_from_slice(&self.indices);
241 out
242 }
243
244 pub fn from_compact_v2_bytes(
248 bytes: &[u8],
249 profile: &FibQuantProfileV1,
250 ) -> Result<(Self, CompactFeatureFlags)> {
251 if bytes.len() < 12 {
252 return Err(FibQuantError::CorruptPayload(format!(
253 "compact v2 FibCodeV1 too short: {} bytes (need >= 12)",
254 bytes.len()
255 )));
256 }
257 if bytes[0..3] != COMPACT_V2_MAGIC {
258 return Err(FibQuantError::CorruptPayload(format!(
259 "compact v2 FibCodeV1 bad magic: {:?} (expected {:?})",
260 &bytes[0..3],
261 COMPACT_V2_MAGIC
262 )));
263 }
264 if bytes[3] != COMPACT_V2_VERSION {
265 return Err(FibQuantError::CorruptPayload(format!(
266 "compact v2 FibCodeV1 version {} not supported (need {})",
267 bytes[3], COMPACT_V2_VERSION
268 )));
269 }
270 let flags = CompactFeatureFlags(bytes[4]);
271 let wire_index_bits = bytes[5];
272 let block_count = u32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]);
273 let norm_len = u16::from_le_bytes([bytes[10], bytes[11]]) as usize;
274 let header_len = 12;
275 if bytes.len() < header_len + norm_len {
276 return Err(FibQuantError::CorruptPayload(format!(
277 "compact v2 FibCodeV1 truncated: norm_len={} but only {} bytes remain",
278 norm_len,
279 bytes.len() - header_len
280 )));
281 }
282 let norm_payload = bytes[header_len..header_len + norm_len].to_vec();
283 let indices = bytes[header_len + norm_len..].to_vec();
284
285 let expected_packed_len = (block_count as usize)
287 .checked_mul(wire_index_bits as usize)
288 .map(|bits| bits.div_ceil(8))
289 .ok_or_else(|| {
290 FibQuantError::ResourceLimitExceeded("packed index bits overflow".into())
291 })?;
292 if indices.len() != expected_packed_len {
293 return Err(FibQuantError::CorruptPayload(format!(
294 "compact v2 FibCodeV1 indices: got {} bytes, expected {} (block_count={} * wire_index_bits={})",
295 indices.len(),
296 expected_packed_len,
297 block_count,
298 wire_index_bits
299 )));
300 }
301
302 let profile_digest = profile.digest()?;
303 Ok((
304 FibCodeV1 {
305 schema_version: CODE_SCHEMA.into(),
306 profile_digest,
307 codebook_digest: String::new(),
308 rotation_digest: String::new(),
309 ambient_dim: profile.ambient_dim,
310 block_dim: profile.block_dim,
311 norm_format: profile.norm_format.clone(),
312 norm_payload,
313 wire_index_bits,
314 block_count,
315 indices,
316 },
317 flags,
318 ))
319 }
320}
321
322#[derive(Debug, Clone)]
324pub struct FibQuantizer {
325 profile: FibQuantProfileV1,
326 codebook: FibCodebookV1,
327 rotation: StoredRotation,
328 profile_digest: String,
333 rotation_digest: String,
335}
336
337impl FibQuantizer {
338 pub fn new(profile: FibQuantProfileV1) -> Result<Self> {
340 let codebook = FibCodebookV1::build(profile)?;
341 Self::from_codebook(codebook)
342 }
343
344 pub fn from_codebook(codebook: FibCodebookV1) -> Result<Self> {
346 codebook.validate()?;
347 let profile = codebook.profile.clone();
348 let rotation = StoredRotation::new(profile.ambient_dim as usize, profile.rotation_seed)?;
349 let profile_digest = profile.digest()?;
350 let rotation_digest = rotation.digest()?;
351 Ok(Self {
352 profile,
353 codebook,
354 rotation,
355 profile_digest,
356 rotation_digest,
357 })
358 }
359
360 pub fn profile(&self) -> &FibQuantProfileV1 {
362 &self.profile
363 }
364
365 pub fn codebook(&self) -> &FibCodebookV1 {
367 &self.codebook
368 }
369
370 pub fn rotation(&self) -> &StoredRotation {
374 &self.rotation
375 }
376
377 pub fn codebook_digest(&self) -> &str {
381 &self.codebook.codebook_digest
382 }
383
384 pub fn rotation_digest(&self) -> &str {
386 &self.codebook.rotation_digest
387 }
388
389 pub fn nominal_compression_ratio(&self) -> f64 {
395 let original = (self.profile.ambient_dim as usize) * std::mem::size_of::<f32>();
396 let block_count = self.profile.block_count() as usize;
399 let index_bytes = (block_count * self.profile.wire_index_bits as usize).div_ceil(8);
400 let encoded = 11 + 2 + index_bytes;
401 original as f64 / encoded as f64
402 }
403
404 pub fn default_degradation_threshold(&self) -> f64 {
409 0.03
410 }
411
412 pub fn is_high_fidelity(&self) -> bool {
415 self.profile.codebook_size >= 16 && self.profile.block_dim <= 8
418 }
419
420 pub fn encode(&self, x: &[f32]) -> Result<FibCodeV1> {
422 let d = self.profile.ambient_dim as usize;
423 if x.len() != d {
424 return Err(FibQuantError::CorruptPayload(format!(
425 "input dimension {}, expected {d}",
426 x.len()
427 )));
428 }
429 check_finite(x)?;
430 let norm = l2_norm(x);
431 if norm == 0.0 {
432 return Err(FibQuantError::ZeroNorm);
433 }
434 let normalized: Vec<f64> = x.iter().map(|value| f64::from(*value) / norm).collect();
437 let rotated_f64 = self.rotation.apply(&normalized)?;
438 let rotated_f32: Vec<f32> = rotated_f64.iter().map(|&v| v as f32).collect();
439 let _block_count = self.profile.block_count() as usize;
440 let n = self.profile.codebook_size as usize;
441 let k = self.profile.block_dim as usize;
442 let c_indices =
443 crate::ffi::c_encode_vector_block(&rotated_f32, &self.codebook.codewords, n, k);
444 let indices: Vec<u32> = c_indices.iter().map(|&i| i as u32).collect();
445 Ok(FibCodeV1 {
446 schema_version: CODE_SCHEMA.into(),
447 profile_digest: self.profile.digest()?,
448 codebook_digest: self.codebook.codebook_digest.clone(),
449 rotation_digest: self.rotation.digest()?,
450 ambient_dim: self.profile.ambient_dim,
451 block_dim: self.profile.block_dim,
452 norm_format: self.profile.norm_format.clone(),
453 norm_payload: encode_norm(norm, &self.profile.norm_format)?,
454 wire_index_bits: self.profile.wire_index_bits,
455 block_count: self.profile.block_count(),
456 indices: pack_indices(&indices, self.profile.wire_index_bits)?,
457 })
458 }
459
460 pub fn decode(&self, code: &FibCodeV1) -> Result<Vec<f32>> {
462 self.validate_code_header(code)?;
463 let k = self.profile.block_dim as usize;
464 let block_count = self.profile.block_count() as usize;
465 let codebook_size = self.profile.codebook_size as usize;
466 let codewords = &self.codebook.codewords;
467 let unpacked = unpack_indices(&code.indices, block_count, self.profile.wire_index_bits)?;
468 let expected_len = block_count.checked_mul(k).ok_or_else(|| {
469 FibQuantError::ResourceLimitExceeded("decoded rotated vector length overflow".into())
470 })?;
471 for &index in &unpacked {
473 let idx = index as usize;
474 if idx >= codebook_size {
475 return Err(FibQuantError::IndexOutOfRange {
476 index,
477 codebook_size: codebook_size as u32,
478 });
479 }
480 }
481 let u16_indices: Vec<u16> = unpacked.iter().map(|&i| i as u16).collect();
483 let rotated_f32 =
484 crate::ffi::c_decode_vector_block(&u16_indices, codewords, codebook_size, k);
485 if rotated_f32.len() != expected_len {
486 return Err(FibQuantError::CorruptPayload(
487 "decoded rotated vector length mismatch".into(),
488 ));
489 }
490 let norm = decode_norm(&code.norm_payload, &code.norm_format)?;
491 let matrix_f32 = self.rotation.matrix_f32();
493 let reconstructed = self
494 .rotation
495 .apply_inverse_f32_with_matrix(&rotated_f32, &matrix_f32)?;
496 let out: Vec<f32> = reconstructed
497 .into_iter()
498 .map(|value| value * norm as f32)
499 .collect();
500 check_finite(&out)?;
501 Ok(out)
502 }
503
504 pub fn encode_with_receipt(
506 &self,
507 x: &[f32],
508 ) -> Result<(FibCodeV1, FibQuantCompressionReceiptV1)> {
509 let code = self.encode(x)?;
510 let source_vector_digest = source_vector_digest(x)?;
511 let original_bytes = std::mem::size_of_val(x);
512 let encoded_bytes = code.compact_size();
513 let lloyd_refined = self.profile.lloyd_mode == crate::profile::LloydMode::Refine;
514 let mut receipt = FibQuantCompressionReceiptV1::new(
515 &self.profile,
516 code.profile_digest.clone(),
517 code.codebook_digest.clone(),
518 code.rotation_digest.clone(),
519 source_vector_digest,
520 encoded_digest(&code)?,
521 original_bytes,
522 encoded_bytes,
523 lloyd_refined,
524 );
525 let decoded = self.decode(&code)?;
526 receipt.mse = Some(metrics::mse(x, &decoded)?);
527 receipt.cosine_similarity = Some(metrics::cosine_similarity(x, &decoded)?);
528 Ok((code, receipt))
529 }
530
531 pub fn reconstruction_mse(&self, x: &[f32]) -> Result<f64> {
533 let code = self.encode(x)?;
534 let decoded = self.decode(&code)?;
535 metrics::mse(x, &decoded)
536 }
537
538 pub fn cosine_similarity(&self, x: &[f32]) -> Result<f64> {
540 let code = self.encode(x)?;
541 let decoded = self.decode(&code)?;
542 metrics::cosine_similarity(x, &decoded)
543 }
544
545 pub fn encode_batch(&self, vectors: &[&[f32]]) -> Result<Vec<FibCodeV1>> {
550 let d = self.profile.ambient_dim as usize;
551 let k = self.profile.block_dim as usize;
552 let n = vectors.len();
553 if n == 0 {
554 return Ok(vec![]);
555 }
556
557 if n < 4 {
559 return vectors.iter().map(|v| self.encode(v)).collect();
560 }
561
562 let mut flat = Vec::with_capacity(n * d);
564 let mut norms_f64 = Vec::with_capacity(n);
565 for v in vectors {
566 if v.len() != d {
567 return Err(FibQuantError::CorruptPayload(format!(
568 "input dimension {}, expected {d}",
569 v.len()
570 )));
571 }
572 check_finite(v)?;
573 let norm = l2_norm(v);
574 if norm == 0.0 {
575 return Err(FibQuantError::ZeroNorm);
576 }
577 norms_f64.push(norm);
578 for &x in *v {
579 flat.push((x as f64 / norm) as f32);
580 }
581 }
582
583 #[cfg(feature = "gpu")]
585 {
586 if let Some(_ctx) = gpu_backend::GpuContext::init() {
587 if n >= gpu_backend::GpuContext::GPU_MIN_BATCH_SIZE
588 && d >= gpu_backend::GpuContext::GPU_MIN_DIM
589 {
590 gpu_backend::hadamard_batch(&mut flat, n, d, self.profile.rotation_seed)
591 .map_err(|e| {
592 FibQuantError::NumericalFailure(format!("gpu hadamard: {}", e))
593 })?;
594
595 #[cfg(feature = "gpu_codebook_lookup")]
606 {
607 let block_count = self.profile.block_count() as usize;
608 if let Ok(indices) = gpu_backend::codebook_lookup_batch(
609 &flat,
610 &self.codebook.codewords,
611 n,
612 d,
613 k,
614 ) {
615 if indices.len() == n * block_count {
616 return self.finish_batch_encode_with_indices(
617 &flat, &norms_f64, &indices, n, d, k,
618 );
619 }
620 }
622 }
623
624 return self.finish_batch_encode(&flat, &norms_f64, n, d, k);
626 }
627 }
628 }
629
630 let mut rotated_flat = Vec::with_capacity(n * d);
632 for chunk in flat.chunks_exact(d) {
633 let f64_chunk: Vec<f64> = chunk.iter().map(|&v| v as f64).collect();
634 let rot = self.rotation.apply(&f64_chunk)?;
635 rotated_flat.extend(rot.iter().map(|&v| v as f32));
636 }
637
638 self.finish_batch_encode(&rotated_flat, &norms_f64, n, d, k)
639 }
640
641 fn finish_batch_encode(
642 &self,
643 rotated: &[f32],
644 norms: &[f64],
645 n: usize,
646 d: usize,
647 k: usize,
648 ) -> Result<Vec<FibCodeV1>> {
649 let profile_digest = self.profile_digest.clone();
653 let codebook_digest = self.codebook.codebook_digest.clone();
654 let rotation_digest = self.rotation_digest.clone();
655 let profile = &self.profile;
656 let codewords_f32: &[f32] = &self.codebook.codewords;
657
658 let per_vec = |vec_idx: usize| -> Result<FibCodeV1> {
662 let start = vec_idx * d;
663 let chunk = &rotated[start..start + d];
664 let c_indices = crate::ffi::c_encode_vector_block(
665 chunk,
666 codewords_f32,
667 self.profile.codebook_size as usize,
668 k,
669 );
670 let indices: Vec<u32> = c_indices.iter().map(|&i| i as u32).collect();
671 Ok(FibCodeV1 {
672 schema_version: CODE_SCHEMA.into(),
673 profile_digest: profile_digest.clone(),
674 codebook_digest: codebook_digest.clone(),
675 rotation_digest: rotation_digest.clone(),
676 ambient_dim: profile.ambient_dim,
677 block_dim: profile.block_dim,
678 norm_format: profile.norm_format.clone(),
679 norm_payload: encode_norm(norms[vec_idx], &profile.norm_format)?,
680 wire_index_bits: profile.wire_index_bits,
681 block_count: profile.block_count(),
682 indices: pack_indices(&indices, profile.wire_index_bits)?,
683 })
684 };
685
686 #[cfg(feature = "parallel")]
690 {
691 const RAYON_MIN_N: usize = 16;
692 if n >= RAYON_MIN_N {
693 use rayon::prelude::*;
694 return (0..n).into_par_iter().map(per_vec).collect();
695 }
696 }
697
698 let mut codes = Vec::with_capacity(n);
699 for vec_idx in 0..n {
700 codes.push(per_vec(vec_idx)?);
701 }
702 Ok(codes)
703 }
704
705 #[cfg(all(feature = "gpu", feature = "gpu_codebook_lookup"))]
709 fn finish_batch_encode_with_indices(
710 &self,
711 _rotated: &[f32], norms: &[f64],
713 indices: &[u32],
714 n: usize,
715 _d: usize,
716 _k: usize,
717 ) -> Result<Vec<FibCodeV1>> {
718 let block_count = self.profile.block_count() as usize;
719 if indices.len() != n * block_count {
720 return Err(FibQuantError::CorruptPayload(format!(
721 "indices length {} != n * block_count {}",
722 indices.len(),
723 n * block_count
724 )));
725 }
726
727 let mut codes = Vec::with_capacity(n);
728 for (vec_idx, &norm) in norms.iter().enumerate().take(n) {
729 let start = vec_idx * block_count;
730 let end = start + block_count;
731 let vec_indices: Vec<u32> = indices[start..end].to_vec();
732
733 codes.push(FibCodeV1 {
734 schema_version: CODE_SCHEMA.into(),
735 profile_digest: self.profile.digest()?,
736 codebook_digest: self.codebook.codebook_digest.clone(),
737 rotation_digest: self.rotation.digest()?,
738 ambient_dim: self.profile.ambient_dim,
739 block_dim: self.profile.block_dim,
740 norm_format: self.profile.norm_format.clone(),
741 norm_payload: encode_norm(norm, &self.profile.norm_format)?,
742 wire_index_bits: self.profile.wire_index_bits,
743 block_count: self.profile.block_count(),
744 indices: pack_indices(&vec_indices, self.profile.wire_index_bits)?,
745 });
746 }
747 Ok(codes)
748 }
749
750 pub fn decode_batch(&self, codes: &[FibCodeV1]) -> Result<Vec<Vec<f32>>> {
752 codes.iter().map(|c| self.decode(c)).collect()
753 }
754
755 pub fn decode_batch_fast(&self, codes: &[FibCodeV1]) -> Result<Vec<Vec<f32>>> {
772 if codes.is_empty() {
773 return Ok(Vec::new());
774 }
775 let k = self.profile.block_dim as usize;
776 let codebook_size = self.profile.codebook_size as usize;
777 let codewords = &self.codebook.codewords;
778 let matrix_f32 = self.rotation.matrix_f32();
783 let mut out = Vec::with_capacity(codes.len());
784 for code in codes {
785 self.validate_code_header(code)?;
786 let block_count = self.profile.block_count() as usize;
787 let unpacked =
788 unpack_indices(&code.indices, block_count, self.profile.wire_index_bits)?;
789 let expected_len = block_count.checked_mul(k).ok_or_else(|| {
790 FibQuantError::ResourceLimitExceeded(
791 "decoded rotated vector length overflow".into(),
792 )
793 })?;
794 let mut rotated_f32: Vec<f32> = Vec::with_capacity(expected_len);
796 for &index in &unpacked {
797 let idx = index as usize;
798 if idx >= codebook_size {
799 return Err(FibQuantError::IndexOutOfRange {
800 index,
801 codebook_size: codebook_size as u32,
802 });
803 }
804 let base = idx * k;
805 rotated_f32.extend_from_slice(&codewords[base..base + k]);
807 }
808 debug_assert_eq!(rotated_f32.len(), expected_len);
809 let norm = decode_norm(&code.norm_payload, &code.norm_format)?;
810 let reconstructed = self
812 .rotation
813 .apply_inverse_f32_with_matrix(&rotated_f32, &matrix_f32)?;
814 let scaled: Vec<f32> = reconstructed
815 .into_iter()
816 .map(|value| value * norm as f32)
817 .collect();
818 check_finite(&scaled)?;
819 out.push(scaled);
820 }
821 Ok(out)
822 }
823
824 pub fn encode_layers(&self, layer_vectors: &[&[&[f32]]]) -> Result<Vec<Vec<FibCodeV1>>> {
836 if layer_vectors.is_empty() {
837 return Ok(Vec::new());
838 }
839 let profile_digest = self.profile_digest.clone();
842 let codebook_digest = self.codebook.codebook_digest.clone();
843 let rotation_digest = self.rotation_digest.clone();
844 let d = self.profile.ambient_dim as usize;
845 let k = self.profile.block_dim as usize;
846 let profile = &self.profile;
847 let codewords_f32: &[f32] = &self.codebook.codewords;
848
849 let mut out = Vec::with_capacity(layer_vectors.len());
850 for layer in layer_vectors {
851 let n = layer.len();
852 if n == 0 {
853 out.push(Vec::new());
854 continue;
855 }
856 let mut codes = Vec::with_capacity(n);
857 for &vec_slice in *layer {
858 if vec_slice.len() != d {
859 return Err(FibQuantError::CorruptPayload(format!(
860 "input dimension {}, expected {d}",
861 vec_slice.len()
862 )));
863 }
864 check_finite(vec_slice)?;
865 let norm = l2_norm(vec_slice);
866 if norm == 0.0 {
867 return Err(FibQuantError::ZeroNorm);
868 }
869 let normalized: Vec<f64> = vec_slice.iter().map(|v| f64::from(*v) / norm).collect();
870 let rotated_f64 = self.rotation.apply(&normalized)?;
871 let rotated_f32: Vec<f32> = rotated_f64.iter().map(|&v| v as f32).collect();
872 let block_count = profile.block_count() as usize;
873 let mut indices = Vec::with_capacity(block_count);
874 for block in rotated_f32.chunks_exact(k) {
875 indices.push(gpu_backend::nearest_codeword_f32(block, codewords_f32, k) as u32);
876 }
877 codes.push(FibCodeV1 {
878 schema_version: CODE_SCHEMA.into(),
879 profile_digest: profile_digest.clone(),
880 codebook_digest: codebook_digest.clone(),
881 rotation_digest: rotation_digest.clone(),
882 ambient_dim: profile.ambient_dim,
883 block_dim: profile.block_dim,
884 norm_format: profile.norm_format.clone(),
885 norm_payload: encode_norm(norm, &profile.norm_format)?,
886 wire_index_bits: profile.wire_index_bits,
887 block_count: profile.block_count(),
888 indices: pack_indices(&indices, profile.wire_index_bits)?,
889 });
890 }
891 out.push(codes);
892 }
893 Ok(out)
894 }
895
896 pub fn is_gpu_accelerated(&self) -> bool {
905 #[cfg(feature = "gpu")]
906 {
907 gpu_backend::GpuContext::is_available()
908 }
909 #[cfg(not(feature = "gpu"))]
910 {
911 false
912 }
913 }
914
915 pub fn is_gpu_accelerated_for(&self, n: usize, d: usize) -> bool {
928 #[cfg(feature = "gpu")]
929 {
930 if !gpu_backend::GpuContext::is_available() {
931 return false;
932 }
933 n >= gpu_backend::GpuContext::GPU_MIN_BATCH_SIZE
934 && d >= gpu_backend::GpuContext::GPU_MIN_DIM
935 && (self.profile.codebook_size as usize) <= 32
936 }
937 #[cfg(not(feature = "gpu"))]
938 {
939 let _ = (n, d);
940 false
941 }
942 }
943
944 pub fn gpu_steps_for(&self, n: usize, d: usize) -> GpuStepReport {
952 let device_available = {
953 #[cfg(feature = "gpu")]
954 {
955 gpu_backend::GpuContext::is_available()
956 }
957 #[cfg(not(feature = "gpu"))]
958 {
959 false
960 }
961 };
962 const MIN_BATCH: usize = 16;
965 const MIN_DIM: usize = 64;
966 let clears_thresholds = n >= MIN_BATCH && d >= MIN_DIM;
967 let codebook_fits = (self.profile.codebook_size as usize) <= 32;
968 GpuStepReport {
969 hadamard: device_available && clears_thresholds,
970 codebook_lookup: device_available && clears_thresholds && codebook_fits,
971 }
972 }
973
974 fn validate_code_header(&self, code: &FibCodeV1) -> Result<()> {
977 if code.schema_version != CODE_SCHEMA {
978 return Err(FibQuantError::CorruptPayload(format!(
979 "code schema_version {}, expected {CODE_SCHEMA}",
980 code.schema_version
981 )));
982 }
983 let expected_profile = self.profile_digest.clone();
984 if code.profile_digest != expected_profile {
985 return Err(FibQuantError::ProfileDigestMismatch {
986 expected: expected_profile,
987 actual: code.profile_digest.clone(),
988 });
989 }
990 if !code.codebook_digest.is_empty() && code.codebook_digest != self.codebook.codebook_digest
997 {
998 return Err(FibQuantError::CodebookDigestMismatch {
999 expected: self.codebook.codebook_digest.clone(),
1000 actual: code.codebook_digest.clone(),
1001 });
1002 }
1003 let expected_rotation = self.rotation_digest.clone();
1004 if !code.rotation_digest.is_empty()
1005 && (code.rotation_digest != expected_rotation
1006 || code.rotation_digest != self.codebook.rotation_digest)
1007 {
1008 return Err(FibQuantError::RotationDigestMismatch {
1009 expected: expected_rotation,
1010 actual: code.rotation_digest.clone(),
1011 });
1012 }
1013 if code.ambient_dim != self.profile.ambient_dim
1014 || code.block_dim != self.profile.block_dim
1015 || code.block_count != self.profile.block_count()
1016 || code.wire_index_bits != self.profile.wire_index_bits
1017 || code.norm_format != self.profile.norm_format
1018 {
1019 return Err(FibQuantError::CorruptPayload(
1020 "encoded header does not match profile".into(),
1021 ));
1022 }
1023 Ok(())
1024 }
1025}
1026
1027pub fn encoded_digest(code: &FibCodeV1) -> Result<String> {
1029 json_digest(CODE_SCHEMA, code)
1030}
1031
1032fn source_vector_digest(x: &[f32]) -> Result<String> {
1033 check_finite(x)?;
1034 let mut bytes = Vec::with_capacity(32 + std::mem::size_of_val(x));
1035 bytes.extend_from_slice(b"fib_quant_source_vector_v1");
1036 bytes.push(0);
1037 bytes.extend_from_slice(&(x.len() as u64).to_le_bytes());
1038 for value in x {
1039 bytes.extend_from_slice(&value.to_le_bytes());
1040 }
1041 Ok(bytes_digest(&bytes))
1042}
1043
1044fn encode_norm(norm: f64, format: &NormFormat) -> Result<Vec<u8>> {
1045 if !norm.is_finite() || norm <= 0.0 {
1046 return Err(FibQuantError::CorruptPayload(
1047 "norm must be finite and positive".into(),
1048 ));
1049 }
1050 match format {
1051 NormFormat::Fp16Paper => {
1052 let narrowed = f16::from_f32(norm as f32);
1053 if !narrowed.is_finite() || narrowed <= f16::ZERO {
1054 return Err(FibQuantError::CorruptPayload(
1055 "norm cannot be represented as finite positive fp16".into(),
1056 ));
1057 }
1058 Ok(narrowed.to_le_bytes().to_vec())
1059 }
1060 NormFormat::F32Reference => {
1061 let narrowed = norm as f32;
1062 if !narrowed.is_finite() || narrowed <= 0.0 {
1063 return Err(FibQuantError::CorruptPayload(
1064 "norm cannot be represented as finite positive f32".into(),
1065 ));
1066 }
1067 Ok(narrowed.to_le_bytes().to_vec())
1068 }
1069 }
1070}
1071
1072fn decode_norm(bytes: &[u8], format: &NormFormat) -> Result<f64> {
1073 match format {
1074 NormFormat::Fp16Paper => {
1075 let bytes: [u8; 2] = bytes
1076 .try_into()
1077 .map_err(|_| FibQuantError::CorruptPayload("fp16 norm length".into()))?;
1078 let value = f16::from_le_bytes(bytes).to_f32() as f64;
1079 if value.is_finite() && value > 0.0 {
1080 Ok(value)
1081 } else {
1082 Err(FibQuantError::CorruptPayload("invalid fp16 norm".into()))
1083 }
1084 }
1085 NormFormat::F32Reference => {
1086 let bytes: [u8; 4] = bytes
1087 .try_into()
1088 .map_err(|_| FibQuantError::CorruptPayload("f32 norm length".into()))?;
1089 let value = f32::from_le_bytes(bytes) as f64;
1090 if value.is_finite() && value > 0.0 {
1091 Ok(value)
1092 } else {
1093 Err(FibQuantError::CorruptPayload("invalid f32 norm".into()))
1094 }
1095 }
1096 }
1097}
1098
1099fn l2_norm(x: &[f32]) -> f64 {
1100 x.iter()
1101 .map(|value| {
1102 let value = f64::from(*value);
1103 value * value
1104 })
1105 .sum::<f64>()
1106 .sqrt()
1107}
1108
1109fn check_finite(x: &[f32]) -> Result<()> {
1110 if let Some((idx, _)) = x.iter().enumerate().find(|(_, value)| !value.is_finite()) {
1111 return Err(FibQuantError::NonFiniteInput(idx));
1112 }
1113 Ok(())
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 use super::*;
1119
1120 #[test]
1121 fn f32_norm_overflow_rejects_before_payload_emit() {
1122 let err = encode_norm(f64::MAX, &NormFormat::F32Reference).unwrap_err();
1123 assert!(matches!(err, FibQuantError::CorruptPayload(message) if message.contains("f32")));
1124 }
1125
1126 #[test]
1127 fn f32_norm_underflow_rejects_before_payload_emit() {
1128 let err = encode_norm(
1129 f64::from(f32::from_bits(1)) / 2.0,
1130 &NormFormat::F32Reference,
1131 )
1132 .unwrap_err();
1133 assert!(matches!(err, FibQuantError::CorruptPayload(message) if message.contains("f32")));
1134 }
1135}
1136
1137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1139pub struct GpuStepReport {
1140 pub hadamard: bool,
1142 pub codebook_lookup: bool,
1144}