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 let k = self.profile.block_dim as usize;
424 if x.len() != d {
425 return Err(FibQuantError::CorruptPayload(format!(
426 "input dimension {}, expected {d}",
427 x.len()
428 )));
429 }
430 check_finite(x)?;
431 let norm = l2_norm(x);
432 if norm == 0.0 {
433 return Err(FibQuantError::ZeroNorm);
434 }
435 let normalized: Vec<f64> = x.iter().map(|value| f64::from(*value) / norm).collect();
438 let rotated_f64 = self.rotation.apply(&normalized)?;
439 let rotated_f32: Vec<f32> = rotated_f64.iter().map(|&v| v as f32).collect();
440 let block_count = self.profile.block_count() as usize;
441 let mut indices = Vec::with_capacity(block_count);
442 for block in rotated_f32.chunks_exact(k) {
443 indices
444 .push(gpu_backend::nearest_codeword_f32(block, &self.codebook.codewords, k) as u32);
445 }
446 Ok(FibCodeV1 {
447 schema_version: CODE_SCHEMA.into(),
448 profile_digest: self.profile.digest()?,
449 codebook_digest: self.codebook.codebook_digest.clone(),
450 rotation_digest: self.rotation.digest()?,
451 ambient_dim: self.profile.ambient_dim,
452 block_dim: self.profile.block_dim,
453 norm_format: self.profile.norm_format.clone(),
454 norm_payload: encode_norm(norm, &self.profile.norm_format)?,
455 wire_index_bits: self.profile.wire_index_bits,
456 block_count: self.profile.block_count(),
457 indices: pack_indices(&indices, self.profile.wire_index_bits)?,
458 })
459 }
460
461 pub fn decode(&self, code: &FibCodeV1) -> Result<Vec<f32>> {
463 self.validate_code_header(code)?;
464 let k = self.profile.block_dim as usize;
465 let block_count = self.profile.block_count() as usize;
466 let codebook_size = self.profile.codebook_size as usize;
467 let codewords = &self.codebook.codewords;
468 let unpacked = unpack_indices(&code.indices, block_count, self.profile.wire_index_bits)?;
469 let expected_len = block_count.checked_mul(k).ok_or_else(|| {
470 FibQuantError::ResourceLimitExceeded("decoded rotated vector length overflow".into())
471 })?;
472 let mut rotated_f32: Vec<f32> = Vec::with_capacity(expected_len);
474 for &index in &unpacked {
475 let idx = index as usize;
476 if idx >= codebook_size {
477 return Err(FibQuantError::IndexOutOfRange {
478 index,
479 codebook_size: codebook_size as u32,
480 });
481 }
482 let base = idx * k;
483 rotated_f32.extend_from_slice(&codewords[base..base + k]);
484 }
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 mut indices = Vec::with_capacity(profile.block_count() as usize);
665 for block in chunk.chunks_exact(k) {
666 indices.push(gpu_backend::nearest_codeword_f32(block, codewords_f32, k) as u32);
667 }
668 Ok(FibCodeV1 {
669 schema_version: CODE_SCHEMA.into(),
670 profile_digest: profile_digest.clone(),
671 codebook_digest: codebook_digest.clone(),
672 rotation_digest: rotation_digest.clone(),
673 ambient_dim: profile.ambient_dim,
674 block_dim: profile.block_dim,
675 norm_format: profile.norm_format.clone(),
676 norm_payload: encode_norm(norms[vec_idx], &profile.norm_format)?,
677 wire_index_bits: profile.wire_index_bits,
678 block_count: profile.block_count(),
679 indices: pack_indices(&indices, profile.wire_index_bits)?,
680 })
681 };
682
683 #[cfg(feature = "parallel")]
687 {
688 const RAYON_MIN_N: usize = 16;
689 if n >= RAYON_MIN_N {
690 use rayon::prelude::*;
691 return (0..n).into_par_iter().map(per_vec).collect();
692 }
693 }
694
695 let mut codes = Vec::with_capacity(n);
696 for vec_idx in 0..n {
697 codes.push(per_vec(vec_idx)?);
698 }
699 Ok(codes)
700 }
701
702 #[cfg(all(feature = "gpu", feature = "gpu_codebook_lookup"))]
706 fn finish_batch_encode_with_indices(
707 &self,
708 _rotated: &[f32], norms: &[f64],
710 indices: &[u32],
711 n: usize,
712 _d: usize,
713 _k: usize,
714 ) -> Result<Vec<FibCodeV1>> {
715 let block_count = self.profile.block_count() as usize;
716 if indices.len() != n * block_count {
717 return Err(FibQuantError::CorruptPayload(format!(
718 "indices length {} != n * block_count {}",
719 indices.len(),
720 n * block_count
721 )));
722 }
723
724 let mut codes = Vec::with_capacity(n);
725 for (vec_idx, &norm) in norms.iter().enumerate().take(n) {
726 let start = vec_idx * block_count;
727 let end = start + block_count;
728 let vec_indices: Vec<u32> = indices[start..end].to_vec();
729
730 codes.push(FibCodeV1 {
731 schema_version: CODE_SCHEMA.into(),
732 profile_digest: self.profile.digest()?,
733 codebook_digest: self.codebook.codebook_digest.clone(),
734 rotation_digest: self.rotation.digest()?,
735 ambient_dim: self.profile.ambient_dim,
736 block_dim: self.profile.block_dim,
737 norm_format: self.profile.norm_format.clone(),
738 norm_payload: encode_norm(norm, &self.profile.norm_format)?,
739 wire_index_bits: self.profile.wire_index_bits,
740 block_count: self.profile.block_count(),
741 indices: pack_indices(&vec_indices, self.profile.wire_index_bits)?,
742 });
743 }
744 Ok(codes)
745 }
746
747 pub fn decode_batch(&self, codes: &[FibCodeV1]) -> Result<Vec<Vec<f32>>> {
749 codes.iter().map(|c| self.decode(c)).collect()
750 }
751
752 pub fn decode_batch_fast(&self, codes: &[FibCodeV1]) -> Result<Vec<Vec<f32>>> {
769 if codes.is_empty() {
770 return Ok(Vec::new());
771 }
772 let k = self.profile.block_dim as usize;
773 let codebook_size = self.profile.codebook_size as usize;
774 let codewords = &self.codebook.codewords;
775 let matrix_f32 = self.rotation.matrix_f32();
780 let mut out = Vec::with_capacity(codes.len());
781 for code in codes {
782 self.validate_code_header(code)?;
783 let block_count = self.profile.block_count() as usize;
784 let unpacked =
785 unpack_indices(&code.indices, block_count, self.profile.wire_index_bits)?;
786 let expected_len = block_count.checked_mul(k).ok_or_else(|| {
787 FibQuantError::ResourceLimitExceeded(
788 "decoded rotated vector length overflow".into(),
789 )
790 })?;
791 let mut rotated_f32: Vec<f32> = Vec::with_capacity(expected_len);
793 for &index in &unpacked {
794 let idx = index as usize;
795 if idx >= codebook_size {
796 return Err(FibQuantError::IndexOutOfRange {
797 index,
798 codebook_size: codebook_size as u32,
799 });
800 }
801 let base = idx * k;
802 rotated_f32.extend_from_slice(&codewords[base..base + k]);
804 }
805 debug_assert_eq!(rotated_f32.len(), expected_len);
806 let norm = decode_norm(&code.norm_payload, &code.norm_format)?;
807 let reconstructed = self
809 .rotation
810 .apply_inverse_f32_with_matrix(&rotated_f32, &matrix_f32)?;
811 let scaled: Vec<f32> = reconstructed
812 .into_iter()
813 .map(|value| value * norm as f32)
814 .collect();
815 check_finite(&scaled)?;
816 out.push(scaled);
817 }
818 Ok(out)
819 }
820
821 pub fn encode_layers(&self, layer_vectors: &[&[&[f32]]]) -> Result<Vec<Vec<FibCodeV1>>> {
833 if layer_vectors.is_empty() {
834 return Ok(Vec::new());
835 }
836 let profile_digest = self.profile_digest.clone();
839 let codebook_digest = self.codebook.codebook_digest.clone();
840 let rotation_digest = self.rotation_digest.clone();
841 let d = self.profile.ambient_dim as usize;
842 let k = self.profile.block_dim as usize;
843 let profile = &self.profile;
844 let codewords_f32: &[f32] = &self.codebook.codewords;
845
846 let mut out = Vec::with_capacity(layer_vectors.len());
847 for layer in layer_vectors {
848 let n = layer.len();
849 if n == 0 {
850 out.push(Vec::new());
851 continue;
852 }
853 let mut codes = Vec::with_capacity(n);
854 for &vec_slice in *layer {
855 if vec_slice.len() != d {
856 return Err(FibQuantError::CorruptPayload(format!(
857 "input dimension {}, expected {d}",
858 vec_slice.len()
859 )));
860 }
861 check_finite(vec_slice)?;
862 let norm = l2_norm(vec_slice);
863 if norm == 0.0 {
864 return Err(FibQuantError::ZeroNorm);
865 }
866 let normalized: Vec<f64> = vec_slice.iter().map(|v| f64::from(*v) / norm).collect();
867 let rotated_f64 = self.rotation.apply(&normalized)?;
868 let rotated_f32: Vec<f32> = rotated_f64.iter().map(|&v| v as f32).collect();
869 let block_count = profile.block_count() as usize;
870 let mut indices = Vec::with_capacity(block_count);
871 for block in rotated_f32.chunks_exact(k) {
872 indices.push(gpu_backend::nearest_codeword_f32(block, codewords_f32, k) as u32);
873 }
874 codes.push(FibCodeV1 {
875 schema_version: CODE_SCHEMA.into(),
876 profile_digest: profile_digest.clone(),
877 codebook_digest: codebook_digest.clone(),
878 rotation_digest: rotation_digest.clone(),
879 ambient_dim: profile.ambient_dim,
880 block_dim: profile.block_dim,
881 norm_format: profile.norm_format.clone(),
882 norm_payload: encode_norm(norm, &profile.norm_format)?,
883 wire_index_bits: profile.wire_index_bits,
884 block_count: profile.block_count(),
885 indices: pack_indices(&indices, profile.wire_index_bits)?,
886 });
887 }
888 out.push(codes);
889 }
890 Ok(out)
891 }
892
893 pub fn is_gpu_accelerated(&self) -> bool {
902 #[cfg(feature = "gpu")]
903 {
904 gpu_backend::GpuContext::is_available()
905 }
906 #[cfg(not(feature = "gpu"))]
907 {
908 false
909 }
910 }
911
912 pub fn is_gpu_accelerated_for(&self, n: usize, d: usize) -> bool {
925 #[cfg(feature = "gpu")]
926 {
927 if !gpu_backend::GpuContext::is_available() {
928 return false;
929 }
930 n >= gpu_backend::GpuContext::GPU_MIN_BATCH_SIZE
931 && d >= gpu_backend::GpuContext::GPU_MIN_DIM
932 && (self.profile.codebook_size as usize) <= 32
933 }
934 #[cfg(not(feature = "gpu"))]
935 {
936 let _ = (n, d);
937 false
938 }
939 }
940
941 pub fn gpu_steps_for(&self, n: usize, d: usize) -> GpuStepReport {
949 let device_available = {
950 #[cfg(feature = "gpu")]
951 {
952 gpu_backend::GpuContext::is_available()
953 }
954 #[cfg(not(feature = "gpu"))]
955 {
956 false
957 }
958 };
959 const MIN_BATCH: usize = 16;
962 const MIN_DIM: usize = 64;
963 let clears_thresholds = n >= MIN_BATCH && d >= MIN_DIM;
964 let codebook_fits = (self.profile.codebook_size as usize) <= 32;
965 GpuStepReport {
966 hadamard: device_available && clears_thresholds,
967 codebook_lookup: device_available && clears_thresholds && codebook_fits,
968 }
969 }
970
971 fn validate_code_header(&self, code: &FibCodeV1) -> Result<()> {
974 if code.schema_version != CODE_SCHEMA {
975 return Err(FibQuantError::CorruptPayload(format!(
976 "code schema_version {}, expected {CODE_SCHEMA}",
977 code.schema_version
978 )));
979 }
980 let expected_profile = self.profile_digest.clone();
981 if code.profile_digest != expected_profile {
982 return Err(FibQuantError::ProfileDigestMismatch {
983 expected: expected_profile,
984 actual: code.profile_digest.clone(),
985 });
986 }
987 if !code.codebook_digest.is_empty() && code.codebook_digest != self.codebook.codebook_digest
994 {
995 return Err(FibQuantError::CodebookDigestMismatch {
996 expected: self.codebook.codebook_digest.clone(),
997 actual: code.codebook_digest.clone(),
998 });
999 }
1000 let expected_rotation = self.rotation_digest.clone();
1001 if !code.rotation_digest.is_empty()
1002 && (code.rotation_digest != expected_rotation
1003 || code.rotation_digest != self.codebook.rotation_digest)
1004 {
1005 return Err(FibQuantError::RotationDigestMismatch {
1006 expected: expected_rotation,
1007 actual: code.rotation_digest.clone(),
1008 });
1009 }
1010 if code.ambient_dim != self.profile.ambient_dim
1011 || code.block_dim != self.profile.block_dim
1012 || code.block_count != self.profile.block_count()
1013 || code.wire_index_bits != self.profile.wire_index_bits
1014 || code.norm_format != self.profile.norm_format
1015 {
1016 return Err(FibQuantError::CorruptPayload(
1017 "encoded header does not match profile".into(),
1018 ));
1019 }
1020 Ok(())
1021 }
1022}
1023
1024pub fn encoded_digest(code: &FibCodeV1) -> Result<String> {
1026 json_digest(CODE_SCHEMA, code)
1027}
1028
1029fn source_vector_digest(x: &[f32]) -> Result<String> {
1030 check_finite(x)?;
1031 let mut bytes = Vec::with_capacity(32 + std::mem::size_of_val(x));
1032 bytes.extend_from_slice(b"fib_quant_source_vector_v1");
1033 bytes.push(0);
1034 bytes.extend_from_slice(&(x.len() as u64).to_le_bytes());
1035 for value in x {
1036 bytes.extend_from_slice(&value.to_le_bytes());
1037 }
1038 Ok(bytes_digest(&bytes))
1039}
1040
1041fn encode_norm(norm: f64, format: &NormFormat) -> Result<Vec<u8>> {
1042 if !norm.is_finite() || norm <= 0.0 {
1043 return Err(FibQuantError::CorruptPayload(
1044 "norm must be finite and positive".into(),
1045 ));
1046 }
1047 match format {
1048 NormFormat::Fp16Paper => {
1049 let narrowed = f16::from_f32(norm as f32);
1050 if !narrowed.is_finite() || narrowed <= f16::ZERO {
1051 return Err(FibQuantError::CorruptPayload(
1052 "norm cannot be represented as finite positive fp16".into(),
1053 ));
1054 }
1055 Ok(narrowed.to_le_bytes().to_vec())
1056 }
1057 NormFormat::F32Reference => {
1058 let narrowed = norm as f32;
1059 if !narrowed.is_finite() || narrowed <= 0.0 {
1060 return Err(FibQuantError::CorruptPayload(
1061 "norm cannot be represented as finite positive f32".into(),
1062 ));
1063 }
1064 Ok(narrowed.to_le_bytes().to_vec())
1065 }
1066 }
1067}
1068
1069fn decode_norm(bytes: &[u8], format: &NormFormat) -> Result<f64> {
1070 match format {
1071 NormFormat::Fp16Paper => {
1072 let bytes: [u8; 2] = bytes
1073 .try_into()
1074 .map_err(|_| FibQuantError::CorruptPayload("fp16 norm length".into()))?;
1075 let value = f16::from_le_bytes(bytes).to_f32() as f64;
1076 if value.is_finite() && value > 0.0 {
1077 Ok(value)
1078 } else {
1079 Err(FibQuantError::CorruptPayload("invalid fp16 norm".into()))
1080 }
1081 }
1082 NormFormat::F32Reference => {
1083 let bytes: [u8; 4] = bytes
1084 .try_into()
1085 .map_err(|_| FibQuantError::CorruptPayload("f32 norm length".into()))?;
1086 let value = f32::from_le_bytes(bytes) as f64;
1087 if value.is_finite() && value > 0.0 {
1088 Ok(value)
1089 } else {
1090 Err(FibQuantError::CorruptPayload("invalid f32 norm".into()))
1091 }
1092 }
1093 }
1094}
1095
1096fn l2_norm(x: &[f32]) -> f64 {
1097 x.iter()
1098 .map(|value| {
1099 let value = f64::from(*value);
1100 value * value
1101 })
1102 .sum::<f64>()
1103 .sqrt()
1104}
1105
1106fn check_finite(x: &[f32]) -> Result<()> {
1107 if let Some((idx, _)) = x.iter().enumerate().find(|(_, value)| !value.is_finite()) {
1108 return Err(FibQuantError::NonFiniteInput(idx));
1109 }
1110 Ok(())
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use super::*;
1116
1117 #[test]
1118 fn f32_norm_overflow_rejects_before_payload_emit() {
1119 let err = encode_norm(f64::MAX, &NormFormat::F32Reference).unwrap_err();
1120 assert!(matches!(err, FibQuantError::CorruptPayload(message) if message.contains("f32")));
1121 }
1122
1123 #[test]
1124 fn f32_norm_underflow_rejects_before_payload_emit() {
1125 let err = encode_norm(
1126 f64::from(f32::from_bits(1)) / 2.0,
1127 &NormFormat::F32Reference,
1128 )
1129 .unwrap_err();
1130 assert!(matches!(err, FibQuantError::CorruptPayload(message) if message.contains("f32")));
1131 }
1132}
1133
1134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1136pub struct GpuStepReport {
1137 pub hadamard: bool,
1139 pub codebook_lookup: bool,
1141}