1use serde::{Deserialize, Serialize};
2
3use crate::{FibQuantError, FibQuantizer, Result};
4
5use super::{
6 block::{KvBlockEncodingV1, KvEncodedBlockV1},
7 layout::KvCacheLayoutV1,
8 page::KvEncodedPageV1,
9 profile::{KvAxisPolicyV1, KvCompressionProfileV1, KvFallbackModeV1},
10 receipt::{
11 kv_tensor_digest, now_unix_seconds, KvCompressionReceiptV1, KvDecodeReceiptV1,
12 KvOperationKindV1, KV_RECEIPT_SCHEMA,
13 },
14 shape::KvTensorShapeV1,
15};
16
17#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19pub struct KvEncodedTensorV1 {
20 pub shape: KvTensorShapeV1,
22 pub layout: KvCacheLayoutV1,
24 pub profile: KvCompressionProfileV1,
26 pub pages: Vec<KvEncodedPageV1>,
28 pub receipt: KvCompressionReceiptV1,
30}
31
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct KvDecodedTensorV1 {
35 pub values: Vec<f32>,
37 pub receipt: KvDecodeReceiptV1,
39}
40
41pub fn encode_kv_tensor(
43 shape: KvTensorShapeV1,
44 layout: KvCacheLayoutV1,
45 profile: KvCompressionProfileV1,
46 values: &[f32],
47) -> Result<KvEncodedTensorV1> {
48 shape.validate()?;
49 layout.validate_for_shape(&shape)?;
50 profile.validate_for_shape(&shape)?;
51 if values.len() != shape.element_count()? {
52 return Err(FibQuantError::CorruptPayload(format!(
53 "kv input has {} values, expected {}",
54 values.len(),
55 shape.element_count()?
56 )));
57 }
58 if values.iter().any(|value| !value.is_finite()) {
59 return Err(FibQuantError::CorruptPayload(
60 "kv input contains non-finite value".into(),
61 ));
62 }
63
64 let quantizer = build_quantizer(&profile)?;
65 let source_digest = kv_tensor_digest(values)?;
66 let profile_digest = profile.digest(&shape)?;
67 let mut pages = Vec::new();
68 let mut compressed_blocks = 0u32;
69 let mut raw_fallback_blocks = 0u32;
70 let mut fallback_reasons = Vec::new();
71 let page_count = profile.page_geometry.page_count(&shape)?;
72
73 for page_id in 0..page_count {
74 let token_start = page_id * profile.page_geometry.tokens_per_page;
75 let token_end = (token_start + profile.page_geometry.tokens_per_page).min(shape.tokens);
76 let token_count = token_end - token_start;
77 let mut blocks = Vec::new();
78 for batch in 0..shape.batch {
79 for layer in 0..shape.layers {
80 for head in 0..shape.kv_heads {
81 for token in token_start..token_end {
82 let block_id = blocks.len() as u32;
83 let vector = vector_slice(values, &shape, batch, layer, head, token)?;
84 let protected = profile
85 .protected_policy
86 .is_protected(&shape, layer, head, token);
87 let block = if protected {
88 raw_block(
89 block_id,
90 batch,
91 layer,
92 head,
93 token,
94 vector,
95 profile.page_geometry.encoded_block_bytes,
96 "protected_region",
97 )
98 } else {
99 encode_vector_block(
100 &quantizer, &profile, block_id, batch, layer, head, token, vector,
101 )?
102 };
103 if block.raw_fallback {
104 raw_fallback_blocks += 1;
105 if !fallback_reasons.contains(&block.reason) {
106 fallback_reasons.push(block.reason.clone());
107 }
108 } else {
109 compressed_blocks += 1;
110 }
111 blocks.push(block);
112 }
113 }
114 }
115 }
116 pages.push(KvEncodedPageV1::new(
117 page_id,
118 token_start,
119 token_count,
120 source_digest.clone(),
121 profile_digest.clone(),
122 &shape,
123 profile.page_geometry.clone(),
124 blocks,
125 )?);
126 }
127
128 let page_digests = pages.iter().map(|page| page.page_digest.clone()).collect();
129 let receipt = KvCompressionReceiptV1 {
130 schema_version: KV_RECEIPT_SCHEMA.into(),
131 operation_kind: KvOperationKindV1::Compress,
132 source_digest,
133 profile_digest,
134 shape_digest: shape.digest()?,
135 page_digests,
136 codebook_digest: profile.codebook_digest.clone(),
137 rotation_digest: profile.rotation_digest.clone(),
138 encoded_pages: pages.len() as u32,
139 compressed_blocks,
140 raw_fallback_blocks,
141 fallback_reasons,
142 recorded_unix_seconds: now_unix_seconds(),
143 };
144 Ok(KvEncodedTensorV1 {
145 shape,
146 layout,
147 profile,
148 pages,
149 receipt,
150 })
151}
152
153pub fn decode_kv_pages(encoded: &KvEncodedTensorV1) -> Result<KvDecodedTensorV1> {
155 encoded.shape.validate()?;
156 encoded.layout.validate_for_shape(&encoded.shape)?;
157 encoded.profile.validate_for_shape(&encoded.shape)?;
158 encoded.receipt.validate()?;
159 let profile_digest = encoded.profile.digest(&encoded.shape)?;
160 if encoded.receipt.profile_digest != profile_digest {
161 return Err(FibQuantError::ProfileDigestMismatch {
162 expected: profile_digest,
163 actual: encoded.receipt.profile_digest.clone(),
164 });
165 }
166 let quantizer = build_quantizer(&encoded.profile)?;
167 let mut values = vec![0.0; encoded.shape.element_count()?];
168 let mut page_digests = Vec::with_capacity(encoded.pages.len());
169 let mut raw_fallback_blocks = 0u32;
170 for page in &encoded.pages {
171 page.validate(&encoded.shape)?;
172 if page.profile_digest != encoded.receipt.profile_digest {
173 return Err(FibQuantError::ProfileDigestMismatch {
174 expected: encoded.receipt.profile_digest.clone(),
175 actual: page.profile_digest.clone(),
176 });
177 }
178 page_digests.push(page.page_digest.clone());
179 for block in &page.encoded_blocks {
180 if block.batch >= encoded.shape.batch
181 || block.layer >= encoded.shape.layers
182 || block.kv_head >= encoded.shape.kv_heads
183 || block.token >= encoded.shape.tokens
184 {
185 return Err(FibQuantError::CorruptPayload(
186 "kv block index outside shape".into(),
187 ));
188 }
189 let decoded = match &block.encoding {
190 KvBlockEncodingV1::RawF32 { values } => {
191 raw_fallback_blocks += 1;
192 values.clone()
193 }
194 KvBlockEncodingV1::FibQuant { code } => quantizer.decode(code)?,
195 };
196 if decoded.len() != encoded.shape.head_dim as usize {
197 return Err(FibQuantError::CorruptPayload(
198 "decoded kv vector head_dim mismatch".into(),
199 ));
200 }
201 let out = vector_slice_mut(
202 &mut values,
203 &encoded.shape,
204 block.batch,
205 block.layer,
206 block.kv_head,
207 block.token,
208 )?;
209 out.copy_from_slice(&decoded);
210 }
211 }
212 let decoded_digest = kv_tensor_digest(&values)?;
213 Ok(KvDecodedTensorV1 {
214 values,
215 receipt: KvDecodeReceiptV1 {
216 schema_version: KV_RECEIPT_SCHEMA.into(),
217 operation_kind: KvOperationKindV1::Decode,
218 decoded_digest,
219 profile_digest: encoded.receipt.profile_digest.clone(),
220 shape_digest: encoded.shape.digest()?,
221 page_digests,
222 codebook_digest: encoded.profile.codebook_digest.clone(),
223 rotation_digest: encoded.profile.rotation_digest.clone(),
224 decoded_pages: encoded.pages.len() as u32,
225 raw_fallback_blocks,
226 recorded_unix_seconds: now_unix_seconds(),
227 },
228 })
229}
230
231fn build_quantizer(profile: &KvCompressionProfileV1) -> Result<FibQuantizer> {
232 let quantizer = FibQuantizer::new(profile.fib_profile.clone())?;
233 if quantizer.codebook().codebook_digest != profile.codebook_digest {
234 return Err(FibQuantError::CodebookDigestMismatch {
235 expected: quantizer.codebook().codebook_digest.clone(),
236 actual: profile.codebook_digest.clone(),
237 });
238 }
239 Ok(quantizer)
240}
241
242#[allow(clippy::too_many_arguments)]
243fn encode_vector_block(
244 quantizer: &FibQuantizer,
245 profile: &KvCompressionProfileV1,
246 block_id: u32,
247 batch: u32,
248 layer: u32,
249 head: u32,
250 token: u32,
251 vector: &[f32],
252) -> Result<KvEncodedBlockV1> {
253 match profile.axis_policy {
254 KvAxisPolicyV1::Raw => Ok(raw_block(
255 block_id,
256 batch,
257 layer,
258 head,
259 token,
260 vector,
261 profile.page_geometry.encoded_block_bytes,
262 "raw_axis_policy",
263 )),
264 KvAxisPolicyV1::PerToken => match quantizer.encode(vector) {
265 Ok(code) => Ok(KvEncodedBlockV1::fib_quant(
266 block_id,
267 batch,
268 layer,
269 head,
270 token,
271 code,
272 profile.page_geometry.encoded_block_bytes,
273 "fib_quant_per_token",
274 )),
275 Err(err) if profile.fallback_policy.mode == KvFallbackModeV1::KeepRaw => Ok(raw_block(
276 block_id,
277 batch,
278 layer,
279 head,
280 token,
281 vector,
282 profile.page_geometry.encoded_block_bytes,
283 format!("encode_fallback:{err}"),
284 )),
285 Err(err) => Err(err),
286 },
287 KvAxisPolicyV1::PerChannel | KvAxisPolicyV1::RoleAwareKiviStyle => {
288 if profile.fallback_policy.mode == KvFallbackModeV1::KeepRaw {
289 Ok(raw_block(
290 block_id,
291 batch,
292 layer,
293 head,
294 token,
295 vector,
296 profile.page_geometry.encoded_block_bytes,
297 "unsupported_axis_raw_fallback",
298 ))
299 } else {
300 Err(FibQuantError::DependencyUnsupported(
301 "CPU reference codec supports per-token FibQuant compression only".into(),
302 ))
303 }
304 }
305 }
306}
307
308#[allow(clippy::too_many_arguments)]
309fn raw_block(
310 block_id: u32,
311 batch: u32,
312 layer: u32,
313 head: u32,
314 token: u32,
315 vector: &[f32],
316 fixed_size_bytes: u32,
317 reason: impl Into<String>,
318) -> KvEncodedBlockV1 {
319 KvEncodedBlockV1::raw(
320 block_id,
321 batch,
322 layer,
323 head,
324 token,
325 vector.to_vec(),
326 fixed_size_bytes,
327 reason,
328 )
329}
330
331pub fn decode_kv_slice(
340 encoded: &KvEncodedTensorV1,
341 layer: u32,
342 head: u32,
343 token_start: u32,
344 token_end: u32,
345) -> Result<Vec<f32>> {
346 encoded.shape.validate()?;
348 encoded.layout.validate_for_shape(&encoded.shape)?;
349 encoded.profile.validate_for_shape(&encoded.shape)?;
350 encoded.receipt.validate()?;
351 let profile_digest = encoded.profile.digest(&encoded.shape)?;
352 if encoded.receipt.profile_digest != profile_digest {
353 return Err(FibQuantError::ProfileDigestMismatch {
354 expected: profile_digest,
355 actual: encoded.receipt.profile_digest.clone(),
356 });
357 }
358
359 if layer >= encoded.shape.layers {
361 return Err(FibQuantError::CorruptPayload(format!(
362 "decode_kv_slice: layer {layer} >= shape.layers {}",
363 encoded.shape.layers
364 )));
365 }
366 if head >= encoded.shape.kv_heads {
367 return Err(FibQuantError::CorruptPayload(format!(
368 "decode_kv_slice: head {head} >= shape.kv_heads {}",
369 encoded.shape.kv_heads
370 )));
371 }
372 if token_start >= token_end {
373 return Err(FibQuantError::CorruptPayload(format!(
374 "decode_kv_slice: token_start {token_start} >= token_end {token_end}"
375 )));
376 }
377 if token_start >= encoded.shape.tokens {
378 return Err(FibQuantError::CorruptPayload(format!(
379 "decode_kv_slice: token_start {token_start} >= shape.tokens {}",
380 encoded.shape.tokens
381 )));
382 }
383 if token_end > encoded.shape.tokens {
384 return Err(FibQuantError::CorruptPayload(format!(
385 "decode_kv_slice: token_end {token_end} > shape.tokens {}",
386 encoded.shape.tokens
387 )));
388 }
389
390 let quantizer = build_quantizer(&encoded.profile)?;
391 let head_dim = encoded.shape.head_dim as usize;
392
393 let mut decoded_map: std::collections::BTreeMap<u32, Vec<f32>> =
396 std::collections::BTreeMap::new();
397
398 for page in &encoded.pages {
399 let page_token_end = page.token_start + page.token_count;
400
401 if page.token_start >= token_end || page_token_end <= token_start {
403 continue;
404 }
405
406 page.validate(&encoded.shape)?;
408 if page.profile_digest != encoded.receipt.profile_digest {
409 return Err(FibQuantError::ProfileDigestMismatch {
410 expected: encoded.receipt.profile_digest.clone(),
411 actual: page.profile_digest.clone(),
412 });
413 }
414
415 for block in &page.encoded_blocks {
416 if block.batch != 0 {
418 continue;
419 }
420 if block.layer != layer || block.kv_head != head {
421 continue;
422 }
423 if block.token < token_start || block.token >= token_end {
424 continue;
425 }
426
427 if block.batch >= encoded.shape.batch
429 || block.layer >= encoded.shape.layers
430 || block.kv_head >= encoded.shape.kv_heads
431 || block.token >= encoded.shape.tokens
432 {
433 return Err(FibQuantError::CorruptPayload(
434 "kv block index outside shape".into(),
435 ));
436 }
437
438 let decoded = match &block.encoding {
439 KvBlockEncodingV1::RawF32 { values } => values.clone(),
440 KvBlockEncodingV1::FibQuant { code } => quantizer.decode(code)?,
441 };
442
443 if decoded.len() != head_dim {
444 return Err(FibQuantError::CorruptPayload(
445 "decoded kv vector head_dim mismatch".into(),
446 ));
447 }
448
449 decoded_map.insert(block.token, decoded);
450 }
451 }
452
453 let token_count = (token_end - token_start) as usize;
455 let mut result = Vec::with_capacity(token_count * head_dim);
456 for token in token_start..token_end {
457 match decoded_map.remove(&token) {
458 Some(vec) => result.extend_from_slice(&vec),
459 None => {
460 return Err(FibQuantError::CorruptPayload(format!(
461 "decode_kv_slice: missing block for token {token} (layer {layer}, head {head})"
462 )));
463 }
464 }
465 }
466
467 Ok(result)
468}
469
470fn vector_offset(
471 shape: &KvTensorShapeV1,
472 batch: u32,
473 layer: u32,
474 head: u32,
475 token: u32,
476) -> Result<usize> {
477 if batch >= shape.batch
478 || layer >= shape.layers
479 || head >= shape.kv_heads
480 || token >= shape.tokens
481 {
482 return Err(FibQuantError::CorruptPayload(
483 "kv vector index outside shape".into(),
484 ));
485 }
486 let vectors_before = (((batch as usize * shape.layers as usize + layer as usize)
487 * shape.kv_heads as usize
488 + head as usize)
489 * shape.tokens as usize)
490 + token as usize;
491 vectors_before
492 .checked_mul(shape.head_dim as usize)
493 .ok_or_else(|| FibQuantError::ResourceLimitExceeded("kv vector offset overflow".into()))
494}
495
496fn vector_slice<'a>(
497 values: &'a [f32],
498 shape: &KvTensorShapeV1,
499 batch: u32,
500 layer: u32,
501 head: u32,
502 token: u32,
503) -> Result<&'a [f32]> {
504 let start = vector_offset(shape, batch, layer, head, token)?;
505 let end = start + shape.head_dim as usize;
506 values
507 .get(start..end)
508 .ok_or_else(|| FibQuantError::CorruptPayload("kv vector slice out of bounds".into()))
509}
510
511fn vector_slice_mut<'a>(
512 values: &'a mut [f32],
513 shape: &KvTensorShapeV1,
514 batch: u32,
515 layer: u32,
516 head: u32,
517 token: u32,
518) -> Result<&'a mut [f32]> {
519 let start = vector_offset(shape, batch, layer, head, token)?;
520 let end = start + shape.head_dim as usize;
521 values
522 .get_mut(start..end)
523 .ok_or_else(|| FibQuantError::CorruptPayload("kv vector slice out of bounds".into()))
524}
525
526#[cfg(test)]
531mod tests {
532 use super::*;
533 use crate::profile::FibQuantProfileV1;
534
535 use super::super::layout::{KvCacheLayoutV1, KvPageGeometryV1};
536 use super::super::profile::KvAxisPolicyV1;
537 use super::super::shape::{KvAttentionKind, KvDType, KvRole, KvRopeState, KvTensorShapeV1};
538
539 fn build_test_tensor() -> KvEncodedTensorV1 {
545 let shape = KvTensorShapeV1::new(
546 KvRole::Key,
547 KvAttentionKind::Mha,
548 1, 2, 2, 2, 6, 8, KvDType::F32,
555 KvRopeState::PreRope,
556 );
557 let layout = KvCacheLayoutV1::canonical(&shape).expect("canonical layout");
558 let fib_profile =
559 FibQuantProfileV1::paper_default(8, 4, 32, 42).expect("build fib profile");
560 let quantizer = FibQuantizer::new(fib_profile.clone()).expect("build quantizer");
561 let page_geometry = KvPageGeometryV1::new(3, 8, 64); let profile = KvCompressionProfileV1::from_parts(
563 "test-profile",
564 &shape,
565 fib_profile,
566 quantizer.codebook().codebook_digest.clone(),
567 KvAxisPolicyV1::PerToken,
568 page_geometry,
569 )
570 .expect("build kv profile");
571
572 let total = shape.element_count().expect("element count");
574 let values: Vec<f32> = (0..total).map(|i| (i as f32) * 0.1).collect();
575
576 encode_kv_tensor(shape, layout, profile, &values).expect("encode tensor")
577 }
578
579 fn full_decode_slice(
582 full: &[f32],
583 shape: &KvTensorShapeV1,
584 layer: u32,
585 head: u32,
586 token_start: u32,
587 token_end: u32,
588 ) -> Vec<f32> {
589 let head_dim = shape.head_dim as usize;
590 let mut result = Vec::with_capacity((token_end - token_start) as usize * head_dim);
591 for token in token_start..token_end {
592 let offset = vector_offset(shape, 0, layer, head, token).expect("offset");
593 result.extend_from_slice(&full[offset..offset + head_dim]);
594 }
595 result
596 }
597
598 #[test]
599 fn slice_matches_full_decode() {
600 let encoded = build_test_tensor();
601 let full = decode_kv_pages(&encoded).expect("full decode");
602
603 let slice = decode_kv_slice(&encoded, 1, 1, 0, 6).expect("slice decode");
605 let expected = full_decode_slice(&full.values, &encoded.shape, 1, 1, 0, 6);
606 assert_eq!(slice.len(), expected.len());
607 for (i, (a, b)) in slice.iter().zip(expected.iter()).enumerate() {
611 assert_eq!(a, b, "mismatch at index {i}: slice={a}, full={b}");
612 }
613 }
614
615 #[test]
616 fn slice_single_token() {
617 let encoded = build_test_tensor();
618 let full = decode_kv_pages(&encoded).expect("full decode");
619
620 let slice = decode_kv_slice(&encoded, 0, 0, 2, 3).expect("single-token slice");
621 assert_eq!(slice.len(), encoded.shape.head_dim as usize);
622
623 let expected = full_decode_slice(&full.values, &encoded.shape, 0, 0, 2, 3);
624 assert_eq!(slice, expected);
625 }
626
627 #[test]
628 fn slice_spans_page_boundary() {
629 let encoded = build_test_tensor();
630 let full = decode_kv_pages(&encoded).expect("full decode");
633
634 let slice = decode_kv_slice(&encoded, 0, 1, 2, 4).expect("boundary-spanning slice");
635 assert_eq!(slice.len(), 2 * encoded.shape.head_dim as usize);
636
637 let expected = full_decode_slice(&full.values, &encoded.shape, 0, 1, 2, 4);
638 assert_eq!(slice, expected);
639 }
640
641 #[test]
642 fn slice_only_visits_overlapping_pages() {
643 let encoded = build_test_tensor();
644 let slice = decode_kv_slice(&encoded, 0, 0, 0, 2).expect("partial slice");
646 assert_eq!(slice.len(), 2 * encoded.shape.head_dim as usize);
647
648 let full = decode_kv_pages(&encoded).expect("full decode");
649 let expected = full_decode_slice(&full.values, &encoded.shape, 0, 0, 0, 2);
650 assert_eq!(slice, expected);
651 }
652
653 #[test]
654 fn slice_invalid_layer_returns_error() {
655 let encoded = build_test_tensor();
656 let err = decode_kv_slice(&encoded, 99, 0, 0, 1).unwrap_err();
657 assert!(matches!(err, FibQuantError::CorruptPayload(ref msg)
658 if msg.contains("layer")));
659 }
660
661 #[test]
662 fn slice_invalid_head_returns_error() {
663 let encoded = build_test_tensor();
664 let err = decode_kv_slice(&encoded, 0, 99, 0, 1).unwrap_err();
665 assert!(matches!(err, FibQuantError::CorruptPayload(ref msg)
666 if msg.contains("head")));
667 }
668
669 #[test]
670 fn slice_invalid_token_range_returns_error() {
671 let encoded = build_test_tensor();
672 let err = decode_kv_slice(&encoded, 0, 0, 3, 3).unwrap_err();
674 assert!(matches!(err, FibQuantError::CorruptPayload(ref msg)
675 if msg.contains("token_start")));
676
677 let err = decode_kv_slice(&encoded, 0, 0, 99, 100).unwrap_err();
679 assert!(matches!(err, FibQuantError::CorruptPayload(ref msg)
680 if msg.contains("token_start")));
681
682 let err = decode_kv_slice(&encoded, 0, 0, 0, 100).unwrap_err();
684 assert!(matches!(err, FibQuantError::CorruptPayload(ref msg)
685 if msg.contains("token_end")));
686 }
687}