Skip to main content

fib_quant/
compat.rs

1//! quant-codec-core trait implementations for fib-quant.
2//!
3//! This module implements the `VectorCodec`, `CodecProfile`, and
4//! `KvCacheCodec` traits from `quant-codec-core` so that fib-quant
5//! can be used through the shared codec trait boundary by `poly-kv`,
6//! `quant-governor`, and `scr-runtime-compression`.
7//!
8//! This is an additive compatibility layer behind the `compat` feature
9//! gate. It does not change any existing fib-quant APIs.
10
11use quant_codec_core::{
12    CodecId, CodecProfile, CodecProfileDigest, EvalReport, KvCacheCodec, KvSliceRequest,
13    KvTensorShape, QuantCodecError, VectorCodec,
14};
15
16use crate::{
17    codec::FibCodeV1,
18    kv::{
19        codec::{encode_kv_tensor, KvEncodedTensorV1},
20        layout::KvCacheLayoutV1,
21        shape::KvTensorShapeV1,
22    },
23    metrics,
24    profile::FibQuantProfileV1,
25    FibQuantizer,
26};
27
28/// Codec ID string for fib-quant.
29pub const FIB_QUANT_CODEC_ID: &str = "fib_quant";
30
31fn map_err(e: crate::FibQuantError) -> QuantCodecError {
32    QuantCodecError::ShapeMismatch {
33        reason: e.to_string(),
34    }
35}
36
37// ---- CodecProfile impl ----
38
39impl CodecProfile for FibQuantProfileV1 {
40    fn codec_id(&self) -> CodecId {
41        CodecId::new(FIB_QUANT_CODEC_ID).expect("valid codec id")
42    }
43
44    fn codec_version(&self) -> &str {
45        &self.codebook_version
46    }
47
48    fn profile_digest(&self) -> CodecProfileDigest {
49        match self.digest() {
50            Ok(hex) => {
51                let bytes = hex_decode(&hex).unwrap_or_else(|_| vec![0u8; 32]);
52                let mut arr = [0u8; 32];
53                let len = bytes.len().min(32);
54                arr[..len].copy_from_slice(&bytes[..len]);
55                CodecProfileDigest::from_canonical_bytes(&arr)
56            }
57            Err(_) => CodecProfileDigest::from_canonical_bytes(b"fib_quant_error"),
58        }
59    }
60
61    fn fixed_rate_bits(&self) -> Option<u16> {
62        Some(self.wire_index_bits as u16)
63    }
64
65    fn block_dim(&self) -> Option<u16> {
66        Some(self.block_dim as u16)
67    }
68
69    fn is_lossy(&self) -> bool {
70        true
71    }
72}
73
74// ---- VectorCodec impl ----
75
76impl VectorCodec for FibQuantizer {
77    type EncodedBlock = FibCodeV1;
78    type Error = QuantCodecError;
79
80    fn encode_block(&self, input: &[f32]) -> Result<Self::EncodedBlock, Self::Error> {
81        self.encode(input).map_err(map_err)
82    }
83
84    fn decode_block(&self, block: &Self::EncodedBlock, out: &mut [f32]) -> Result<(), Self::Error> {
85        let decoded = self.decode(block).map_err(map_err)?;
86        let len = decoded.len().min(out.len());
87        out[..len].copy_from_slice(&decoded[..len]);
88        Ok(())
89    }
90}
91
92// ---- KvCacheCodec impl ----
93
94/// KV cache codec wrapping fib-quant's KV compression.
95pub struct FibKvCodec {
96    quantizer: FibQuantizer,
97    kv_profile: crate::kv::profile::KvCompressionProfileV1,
98}
99
100impl FibKvCodec {
101    /// Build a KV codec from a fib-quant profile and KV compression profile.
102    pub fn new(
103        fib_profile: FibQuantProfileV1,
104        kv_profile: crate::kv::profile::KvCompressionProfileV1,
105    ) -> Result<Self, QuantCodecError> {
106        let quantizer = FibQuantizer::new(fib_profile).map_err(map_err)?;
107        Ok(Self {
108            quantizer,
109            kv_profile,
110        })
111    }
112
113    /// Access the underlying quantizer.
114    pub fn quantizer(&self) -> &FibQuantizer {
115        &self.quantizer
116    }
117}
118
119impl VectorCodec for FibKvCodec {
120    type EncodedBlock = FibCodeV1;
121    type Error = QuantCodecError;
122
123    fn encode_block(&self, input: &[f32]) -> Result<Self::EncodedBlock, Self::Error> {
124        self.quantizer.encode(input).map_err(map_err)
125    }
126
127    fn decode_block(&self, block: &Self::EncodedBlock, out: &mut [f32]) -> Result<(), Self::Error> {
128        let decoded = self.quantizer.decode(block).map_err(map_err)?;
129        let len = decoded.len().min(out.len());
130        out[..len].copy_from_slice(&decoded[..len]);
131        Ok(())
132    }
133}
134
135impl KvCacheCodec for FibKvCodec {
136    type EncodedCache = KvEncodedTensorV1;
137
138    fn encode_kv_cache(
139        &self,
140        tensors: &[f32],
141        shape: KvTensorShape,
142    ) -> Result<Self::EncodedCache, Self::Error> {
143        let fib_shape = convert_to_fib_shape(&shape)?;
144        let layout = KvCacheLayoutV1::canonical(&fib_shape).map_err(map_err)?;
145        let encoded = encode_kv_tensor(fib_shape, layout, self.kv_profile.clone(), tensors)
146            .map_err(map_err)?;
147        Ok(encoded)
148    }
149
150    fn decode_slice(
151        &self,
152        cache: &Self::EncodedCache,
153        request: KvSliceRequest,
154        out: &mut [f32],
155    ) -> Result<(), Self::Error> {
156        // Random-access decode — only decodes blocks matching the
157        // requested layer, head, and token range.
158        let decoded = crate::kv::codec::decode_kv_slice(
159            cache,
160            request.layer.0,
161            request.head.map(|h| h.0).unwrap_or(0),
162            request.token_span.start as u32,
163            request.token_span.end as u32,
164        )
165        .map_err(map_err)?;
166        let len = decoded.len().min(out.len());
167        out[..len].copy_from_slice(&decoded[..len]);
168        Ok(())
169    }
170}
171
172fn convert_to_fib_shape(shape: &KvTensorShape) -> Result<KvTensorShapeV1, QuantCodecError> {
173    Ok(KvTensorShapeV1::new(
174        crate::kv::shape::KvRole::Key,
175        crate::kv::shape::KvAttentionKind::Mha,
176        1,
177        shape.layers,
178        shape.key_heads,
179        shape.key_heads, // query_heads = key_heads for Mha
180        shape.seq_len as u32,
181        shape.head_dim,
182        crate::kv::shape::KvDType::F32,
183        crate::kv::shape::KvRopeState::NotApplicable,
184    ))
185}
186
187// ---- Eval adapter ----
188
189/// Run a compression eval using the quant-codec-core EvalReport type.
190pub fn eval_compress(
191    quantizer: &FibQuantizer,
192    vectors: &[&[f32]],
193) -> Result<EvalReport, QuantCodecError> {
194    if vectors.is_empty() {
195        return Ok(EvalReport {
196            mse: None,
197            cosine_similarity: None,
198            max_abs_error: None,
199            bytes_exact: 0,
200            bytes_encoded: 0,
201            passed: true,
202            notes: vec!["empty input".to_string()],
203        });
204    }
205    let mut total_mse = 0.0f64;
206    let mut total_cos = 0.0f64;
207    let mut total_bytes_exact = 0u64;
208    let mut total_bytes_encoded = 0u64;
209    let mut max_abs_error = 0.0f64;
210    let count = vectors.len() as f64;
211
212    for v in vectors {
213        let code = quantizer.encode(v).map_err(map_err)?;
214        let decoded = quantizer.decode(&code).map_err(map_err)?;
215        let mse = metrics::mse(v, &decoded).map_err(map_err)?;
216        let cos = metrics::cosine_similarity(v, &decoded).map_err(map_err)?;
217        total_mse += mse;
218        total_cos += cos;
219        total_bytes_exact += (v.len() * 4) as u64;
220        total_bytes_encoded += code.compact_size() as u64;
221        for (a, b) in v.iter().zip(decoded.iter()) {
222            let err = (f64::from(*a) - f64::from(*b)).abs();
223            if err > max_abs_error {
224                max_abs_error = err;
225            }
226        }
227    }
228
229    Ok(EvalReport {
230        mse: Some(total_mse / count),
231        cosine_similarity: Some(total_cos / count),
232        max_abs_error: Some(max_abs_error),
233        bytes_exact: total_bytes_exact,
234        bytes_encoded: total_bytes_encoded,
235        passed: true,
236        notes: vec![format!("{} vectors", vectors.len())],
237    })
238}
239
240fn hex_decode(s: &str) -> Result<Vec<u8>, ()> {
241    if s.len() % 2 != 0 {
242        return Err(());
243    }
244    (0..s.len())
245        .step_by(2)
246        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| ()))
247        .collect()
248}