1use std::time::Instant;
2
3use crate::codec::{create_codec, CompressedBlock};
4use crate::digest_compat::Digest;
5use crate::error::{PolyKvError, Result};
6use crate::manifest::PoolManifest;
7use crate::policy::{CompressionPolicy, CODEC_FIB_K4_N32};
8use crate::receipt::{now_unix, CompressedAttentionSelectionReceipt, PoolBuildReceipt};
9use crate::shape::KvTensorShape;
10
11#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13pub struct PoolLayer {
14 pub layer_index: u32,
16 pub key_blocks: Vec<CompressedBlock>,
18 pub value_blocks: Vec<CompressedBlock>,
20 pub block_digest: Digest,
22}
23
24impl PoolLayer {
25 fn compute_digest(&self) -> Result<Digest> {
27 let key_digests: Vec<&str> = self
29 .key_blocks
30 .iter()
31 .map(|b| b.payload_digest.hex())
32 .collect();
33 let value_digests: Vec<&str> = self
34 .value_blocks
35 .iter()
36 .map(|b| b.payload_digest.hex())
37 .collect();
38 let payload = serde_json::json!({
39 "layer_index": self.layer_index,
40 "key_digests": key_digests,
41 "value_digests": value_digests,
42 });
43 crate::digest_compat::compute_json(&payload)
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
49pub struct CompressedAttentionHit {
50 pub token_index: usize,
52 pub score: f32,
54 pub value: Vec<f32>,
56}
57
58#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
60pub struct CompressedAttentionSelection {
61 pub hits: Vec<CompressedAttentionHit>,
63 pub receipt: CompressedAttentionSelectionReceipt,
65}
66
67#[derive(Debug, Clone)]
73pub struct SharedKVPool {
74 pub manifest: PoolManifest,
76 pub layers: Vec<PoolLayer>,
78 pub policy: CompressionPolicy,
80}
81
82impl SharedKVPool {
83 pub fn build(
95 corpus: &[(String, Vec<f32>)],
96 shape: &KvTensorShape,
97 seed: u64,
98 ) -> Result<(Self, PoolBuildReceipt)> {
99 let start = Instant::now();
100
101 if corpus.is_empty() {
102 return Err(PolyKvError::EmptyCorpus);
103 }
104
105 shape.validate()?;
106 let policy = CompressionPolicy::default_two_tier();
107 policy.validate()?;
108
109 let num_tokens = corpus.len();
110 let num_layers = shape.num_layers as usize;
111 let num_kv_heads = shape.num_kv_heads as usize;
112 let head_dim = shape.head_dim;
113
114 let expected_len = num_layers * num_kv_heads * head_dim * 2; for (token_id, vec) in corpus {
117 if vec.len() != expected_len {
118 return Err(PolyKvError::DimensionMismatch {
119 expected: expected_len,
120 got: vec.len(),
121 });
122 }
123 if vec.iter().any(|v| !v.is_finite()) {
124 return Err(PolyKvError::CorruptPayload(format!(
125 "token {} contains non-finite values",
126 token_id
127 )));
128 }
129 }
130
131 let codec = create_codec(CODEC_FIB_K4_N32, head_dim, Some(&policy.fib_config), None)?;
138
139 let mut layers: Vec<PoolLayer> = Vec::with_capacity(num_layers);
140 let mut total_compressed_bytes: u64 = 0;
141
142 let build_layer = |layer_idx: usize| -> Result<(PoolLayer, u64)> {
146 let mut key_inputs: Vec<Vec<f32>> = Vec::with_capacity(num_tokens * num_kv_heads);
151 let mut value_inputs: Vec<Vec<f32>> = Vec::with_capacity(num_tokens * num_kv_heads);
152 for (_token_id, vec) in corpus.iter() {
153 for head_idx in 0..num_kv_heads {
154 let base_offset =
155 layer_idx * num_kv_heads * head_dim * 2 + head_idx * head_dim * 2;
156 let key_end = base_offset + head_dim;
157 let value_end = key_end + head_dim;
158 key_inputs.push(vec[base_offset..key_end].to_vec());
159 value_inputs.push(vec[key_end..value_end].to_vec());
160 }
161 }
162
163 let key_refs: Vec<&[f32]> = key_inputs.iter().map(|v| v.as_slice()).collect();
164 let value_refs: Vec<&[f32]> = value_inputs.iter().map(|v| v.as_slice()).collect();
165
166 let mut key_blocks: Vec<CompressedBlock>;
167 let mut value_blocks: Vec<CompressedBlock>;
168 if let (Some(key_payload), Some(value_payload)) = (
169 codec.encode_batch_compact(&key_refs, seed)?,
170 codec.encode_batch_compact(&value_refs, seed)?,
171 ) {
172 key_blocks = vec![CompressedBlock::new(
173 codec.codec_id(),
174 key_payload,
175 head_dim,
176 )];
177 value_blocks = vec![CompressedBlock::new(
178 codec.codec_id(),
179 value_payload,
180 head_dim,
181 )];
182 } else {
183 let encoded_keys = codec.encode_batch(&key_refs, seed)?;
184 let encoded_values = codec.encode_batch(&value_refs, seed)?;
185
186 if encoded_keys.len() != num_tokens * num_kv_heads
187 || encoded_values.len() != num_tokens * num_kv_heads
188 {
189 return Err(PolyKvError::Internal(format!(
190 "encode_batch returned {} keys / {} values, expected {} (layer {})",
191 encoded_keys.len(),
192 encoded_values.len(),
193 num_tokens * num_kv_heads,
194 layer_idx
195 )));
196 }
197
198 key_blocks = Vec::with_capacity(num_tokens * num_kv_heads);
199 value_blocks = Vec::with_capacity(num_tokens * num_kv_heads);
200 for (k_payload, v_payload) in encoded_keys.into_iter().zip(encoded_values) {
201 key_blocks.push(CompressedBlock::new(codec.codec_id(), k_payload, head_dim));
202 value_blocks.push(CompressedBlock::new(codec.codec_id(), v_payload, head_dim));
203 }
204 }
205 let layer_bytes: u64 = key_blocks
206 .iter()
207 .map(|b| b.compressed_bytes as u64)
208 .sum::<u64>()
209 + value_blocks
210 .iter()
211 .map(|b| b.compressed_bytes as u64)
212 .sum::<u64>();
213
214 let mut layer = PoolLayer {
215 layer_index: layer_idx as u32,
216 key_blocks,
217 value_blocks,
218 block_digest: Digest::from_hex_unchecked(""),
219 };
220 layer.block_digest = layer.compute_digest()?;
221 Ok((layer, layer_bytes))
222 };
223
224 let layer_results: Vec<Result<(PoolLayer, u64)>> = {
227 #[cfg(feature = "parallel_pool")]
228 {
229 use rayon::prelude::*;
230 (0..num_layers).into_par_iter().map(build_layer).collect()
231 }
232 #[cfg(not(feature = "parallel_pool"))]
233 {
234 (0..num_layers).map(build_layer).collect()
235 }
236 };
237 for r in layer_results {
238 let (layer, layer_bytes) = r?;
239 total_compressed_bytes += layer_bytes;
240 layers.push(layer);
241 }
242
243 let raw_size_bytes = shape.total_kv_bytes(num_tokens) as u64;
244 let fib_build_ms = start.elapsed().as_millis() as u64;
245 let built_at_unix = now_unix();
246
247 let layer_digests: Vec<Digest> = layers.iter().map(|l| l.block_digest.clone()).collect();
249 let pool_id = crate::digest_compat::compute_json(&layer_digests)?;
250
251 let manifest = PoolManifest::new(
252 pool_id.clone(),
253 shape.clone(),
254 policy.clone(),
255 num_tokens as u32,
256 shape.num_layers,
257 total_compressed_bytes,
258 raw_size_bytes,
259 seed,
260 built_at_unix,
261 )?;
262
263 let batch_n = num_tokens * num_kv_heads;
270 let backend = if codec.is_gpu_accelerated_for(batch_n, head_dim) {
271 "gpu"
272 } else {
273 "cpu"
274 };
275 let codebook_digest = codec
276 .codebook_digest(seed)
277 .map(Digest::from_hex_unchecked)
278 .unwrap_or_else(|| Digest::from_hex_unchecked(""));
279 let rotation_digest = codec
280 .rotation_digest(seed)
281 .map(Digest::from_hex_unchecked)
282 .unwrap_or_else(|| Digest::from_hex_unchecked(""));
283 let receipt = PoolBuildReceipt::new(
284 pool_id,
285 layer_digests,
286 codebook_digest,
287 rotation_digest,
288 num_tokens as u32,
289 fib_build_ms,
290 total_compressed_bytes,
291 raw_size_bytes,
292 policy.clone(),
293 seed,
294 built_at_unix,
295 )
296 .with_backend(backend);
297
298 Ok((
299 Self {
300 manifest,
301 layers,
302 policy,
303 },
304 receipt,
305 ))
306 }
307 pub fn materialize_shell(
321 &self,
322 agent_id: &str,
323 agent_tokens: &[(String, Vec<f32>)],
324 seed: u64,
325 ) -> Result<(
326 crate::shell::AgentShell,
327 crate::receipt::ShellMaterializeReceipt,
328 )> {
329 crate::shell::materialize_shell(self, agent_id, agent_tokens, seed)
330 }
331
332 pub fn inject_into_cache(
337 _shell: &crate::shell::AgentShell,
338 _base_cache: &mut dyn CacheTarget,
339 ) -> Result<crate::receipt::InjectionReceipt> {
340 Err(PolyKvError::Internal(
344 "inject_into_cache requires a concrete cache adapter; use inject_into_cache_with_adaptor"
345 .into(),
346 ))
347 }
348
349 pub fn decompress_layer(&self, layer_idx: usize) -> Result<DecompressedLayer> {
363 if layer_idx >= self.layers.len() {
364 return Err(PolyKvError::Internal(format!(
365 "decompress_layer: layer_idx {layer_idx} out of range (have {})",
366 self.layers.len()
367 )));
368 }
369 let layer = &self.layers[layer_idx];
370 let head_dim = self.manifest.shape.head_dim;
371 let num_heads = self.manifest.shape.num_kv_heads as usize;
372 let num_tokens = if layer.key_blocks.len() == 1 && layer.value_blocks.len() == 1 {
373 self.manifest.num_shared_tokens as usize
374 } else {
375 layer.key_blocks.len() / num_heads
376 };
377 if layer.value_blocks.len() != layer.key_blocks.len() {
378 return Err(PolyKvError::Internal(format!(
379 "layer {}: key/value block count mismatch ({} vs {})",
380 layer_idx,
381 layer.key_blocks.len(),
382 layer.value_blocks.len()
383 )));
384 }
385 if layer.key_blocks.len() != num_tokens * num_heads && layer.key_blocks.len() != 1 {
386 return Err(PolyKvError::Internal(format!(
387 "layer {}: block count {} != num_tokens * num_heads {}",
388 layer_idx,
389 layer.key_blocks.len(),
390 num_tokens * num_heads
391 )));
392 }
393 let shared_codec: crate::policy::CodecId = self.manifest.shared_codec.clone();
396 let codec = create_codec(
397 &shared_codec,
398 head_dim,
399 Some(&self.manifest.policy.fib_config),
400 Some(&self.manifest.policy.turbo_config),
401 )?;
402 let seed = self.manifest.build_seed;
403 let mut keys_per_head: Vec<Vec<f32>> =
408 vec![Vec::with_capacity(num_tokens * head_dim); num_heads];
409 let mut values_per_head: Vec<Vec<f32>> =
410 vec![Vec::with_capacity(num_tokens * head_dim); num_heads];
411 if layer.key_blocks.len() == 1 && layer.value_blocks.len() == 1 {
412 if let (Some(decoded_keys), Some(decoded_values)) = (
413 codec.decode_batch_compact(&layer.key_blocks[0].encoded_payload, seed)?,
414 codec.decode_batch_compact(&layer.value_blocks[0].encoded_payload, seed)?,
415 ) {
416 if decoded_keys.len() != num_tokens * num_heads
417 || decoded_values.len() != num_tokens * num_heads
418 {
419 return Err(PolyKvError::Internal(format!(
420 "FB2 decoded {} keys / {} values, expected {} (layer {})",
421 decoded_keys.len(),
422 decoded_values.len(),
423 num_tokens * num_heads,
424 layer_idx
425 )));
426 }
427 for token_idx in 0..num_tokens {
428 for head_idx in 0..num_heads {
429 let block_idx = token_idx * num_heads + head_idx;
430 let k_decoded = &decoded_keys[block_idx];
431 let v_decoded = &decoded_values[block_idx];
432 if k_decoded.len() != head_dim || v_decoded.len() != head_dim {
433 return Err(PolyKvError::Internal(format!(
434 "FB2 decoded vector length mismatch (layer {}, token {}, head {})",
435 layer_idx, token_idx, head_idx
436 )));
437 }
438 keys_per_head[head_idx].extend_from_slice(k_decoded);
439 values_per_head[head_idx].extend_from_slice(v_decoded);
440 }
441 }
442 } else {
443 if num_tokens != 1 || num_heads != 1 {
444 return Err(PolyKvError::Internal(format!(
445 "single non-compact block cannot decode shape tokens={} heads={} (layer {})",
446 num_tokens, num_heads, layer_idx
447 )));
448 }
449 let k_decoded = codec.decode(&layer.key_blocks[0].encoded_payload, seed)?;
450 let v_decoded = codec.decode(&layer.value_blocks[0].encoded_payload, seed)?;
451 keys_per_head[0].extend_from_slice(&k_decoded);
452 values_per_head[0].extend_from_slice(&v_decoded);
453 }
454 } else {
455 for token_idx in 0..num_tokens {
456 for head_idx in 0..num_heads {
457 let block_idx = token_idx * num_heads + head_idx;
458 let k_payload = &layer.key_blocks[block_idx].encoded_payload;
459 let v_payload = &layer.value_blocks[block_idx].encoded_payload;
460 let k_decoded = codec.decode(k_payload, seed)?;
461 let v_decoded = codec.decode(v_payload, seed)?;
462 if k_decoded.len() != head_dim {
463 return Err(PolyKvError::Internal(format!(
464 "decoded key length {} != head_dim {} (layer {}, token {}, head {})",
465 k_decoded.len(),
466 head_dim,
467 layer_idx,
468 token_idx,
469 head_idx
470 )));
471 }
472 keys_per_head[head_idx].extend_from_slice(&k_decoded);
473 values_per_head[head_idx].extend_from_slice(&v_decoded);
474 }
475 }
476 }
477 Ok(DecompressedLayer {
478 layer_index: layer_idx as u32,
479 num_tokens,
480 num_heads,
481 head_dim,
482 keys: keys_per_head,
483 values: values_per_head,
484 })
485 }
486
487 pub fn attention_topk_compressed(
494 &self,
495 layer_idx: usize,
496 head_idx: usize,
497 query: &[f32],
498 top_k: usize,
499 ) -> Result<CompressedAttentionSelection> {
500 #[cfg(not(feature = "fib"))]
501 {
502 let _ = (layer_idx, head_idx, query, top_k);
503 return Err(PolyKvError::CodecUnavailable {
504 codec: CODEC_FIB_K4_N32.into(),
505 feature: "fib".into(),
506 });
507 }
508
509 #[cfg(feature = "fib")]
510 {
511 if layer_idx >= self.layers.len() {
512 return Err(PolyKvError::LayerIndexOutOfBounds {
513 index: layer_idx as u32,
514 total: self.layers.len() as u32,
515 });
516 }
517 let head_dim = self.manifest.shape.head_dim;
518 if query.len() != head_dim {
519 return Err(PolyKvError::DimensionMismatch {
520 expected: head_dim,
521 got: query.len(),
522 });
523 }
524 if head_idx >= self.manifest.shape.num_kv_heads as usize {
525 return Err(PolyKvError::Internal(format!(
526 "head_idx {head_idx} out of range (have {})",
527 self.manifest.shape.num_kv_heads
528 )));
529 }
530 if self.manifest.shared_codec != CODEC_FIB_K4_N32 {
531 return Err(PolyKvError::InvalidPolicy(format!(
532 "compressed cold-pool attention requires shared codec {CODEC_FIB_K4_N32}, got {}",
533 self.manifest.shared_codec
534 )));
535 }
536
537 let layer = &self.layers[layer_idx];
538 if layer.key_blocks.len() != layer.value_blocks.len() {
539 return Err(PolyKvError::Internal(format!(
540 "layer {layer_idx}: key/value block count mismatch ({} vs {})",
541 layer.key_blocks.len(),
542 layer.value_blocks.len()
543 )));
544 }
545 let num_heads = self.manifest.shape.num_kv_heads as usize;
546 let num_tokens = self.manifest.num_shared_tokens as usize;
547 let expected_codes = num_tokens * num_heads;
548 let adapter = crate::codec::FibQuantAdapter::new(
549 head_dim,
550 self.manifest.policy.fib_config.k,
551 self.manifest.policy.fib_config.n,
552 self.manifest.policy.fib_config.training_samples,
553 self.manifest.policy.fib_config.lloyd_restarts,
554 self.manifest.policy.fib_config.lloyd_iterations,
555 )?;
556 let seed = self.manifest.build_seed;
557 let mut key_codes = Vec::with_capacity(expected_codes);
558 for block in &layer.key_blocks {
559 key_codes.extend(adapter.decode_codes_payload(&block.encoded_payload, seed)?);
560 }
561 let mut value_codes = Vec::with_capacity(expected_codes);
562 for block in &layer.value_blocks {
563 value_codes.extend(adapter.decode_codes_payload(&block.encoded_payload, seed)?);
564 }
565 if key_codes.len() != expected_codes || value_codes.len() != expected_codes {
566 return Err(PolyKvError::Internal(format!(
567 "layer {layer_idx}: decoded {} key codes / {} value codes, expected {expected_codes}",
568 key_codes.len(),
569 value_codes.len()
570 )));
571 }
572
573 let quantizer = adapter.build_quantizer(seed)?;
574 let scorer = fib_quant::FibScorer::new(quantizer).map_err(|e| {
575 PolyKvError::Internal(format!("fib compressed scorer construction failed: {e}"))
576 })?;
577 let prepared = scorer.prepare_query(query).map_err(|e| {
578 PolyKvError::Internal(format!("fib compressed query preparation failed: {e}"))
579 })?;
580 let mut scored = Vec::with_capacity(num_tokens);
581 for token_idx in 0..num_tokens {
582 let code_idx = token_idx * num_heads + head_idx;
583 let score = scorer
584 .score_prepared(&prepared, &key_codes[code_idx])
585 .map_err(|e| {
586 PolyKvError::Internal(format!("fib compressed score failed: {e}"))
587 })?;
588 scored.push((token_idx, code_idx, score));
589 }
590 let selected = top_k.min(scored.len());
591 if selected > 0 && selected < scored.len() {
592 scored.select_nth_unstable_by(selected - 1, |a, b| {
593 b.2.total_cmp(&a.2).then_with(|| a.0.cmp(&b.0))
594 });
595 scored.truncate(selected);
596 }
597 scored.sort_by(|a, b| b.2.total_cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
598 let mut hits = Vec::with_capacity(selected);
599 for &(token_index, code_idx, score) in scored.iter().take(selected) {
600 let value = scorer
601 .quantizer()
602 .decode(&value_codes[code_idx])
603 .map_err(|e| {
604 PolyKvError::DecompressionFailed(format!(
605 "fib selected value decode failed: {e}"
606 ))
607 })?;
608 if value.len() != head_dim {
609 return Err(PolyKvError::DimensionMismatch {
610 expected: head_dim,
611 got: value.len(),
612 });
613 }
614 hits.push(CompressedAttentionHit {
615 token_index,
616 score,
617 value,
618 });
619 }
620 let receipt = CompressedAttentionSelectionReceipt::new(
621 self.manifest.pool_id.clone(),
622 layer_idx as u32,
623 head_idx as u32,
624 num_tokens as u32,
625 hits.len() as u32,
626 num_tokens as u64,
627 hits.len() as u64,
628 false,
629 "fib_cold_pool_compressed_score_topk_value_decode",
630 self.manifest.shared_codec.clone(),
631 now_unix(),
632 );
633 receipt.validate()?;
634 Ok(CompressedAttentionSelection { hits, receipt })
635 }
636 }
637
638 #[cfg(feature = "fib")]
646 pub fn prepare_compressed_index(
647 &self,
648 layer_idx: usize,
649 head_idx: usize,
650 ) -> Result<PreparedCompressedIndex> {
651 if layer_idx >= self.layers.len() {
652 return Err(PolyKvError::LayerIndexOutOfBounds {
653 index: layer_idx as u32,
654 total: self.layers.len() as u32,
655 });
656 }
657 let head_dim = self.manifest.shape.head_dim;
658 let num_heads = self.manifest.shape.num_kv_heads as usize;
659 if head_idx >= num_heads {
660 return Err(PolyKvError::Internal(format!(
661 "head_idx {head_idx} out of range (have {num_heads})"
662 )));
663 }
664 if self.manifest.shared_codec != CODEC_FIB_K4_N32 {
665 return Err(PolyKvError::InvalidPolicy(format!(
666 "compressed cold-pool attention requires shared codec {CODEC_FIB_K4_N32}, got {}",
667 self.manifest.shared_codec
668 )));
669 }
670 let layer = &self.layers[layer_idx];
671 if layer.key_blocks.len() != layer.value_blocks.len() {
672 return Err(PolyKvError::Internal(format!(
673 "layer {layer_idx}: key/value block count mismatch ({} vs {})",
674 layer.key_blocks.len(),
675 layer.value_blocks.len()
676 )));
677 }
678 let num_tokens = self.manifest.num_shared_tokens as usize;
679 let expected_codes = num_tokens * num_heads;
680 let adapter = crate::codec::FibQuantAdapter::new(
681 head_dim,
682 self.manifest.policy.fib_config.k,
683 self.manifest.policy.fib_config.n,
684 self.manifest.policy.fib_config.training_samples,
685 self.manifest.policy.fib_config.lloyd_restarts,
686 self.manifest.policy.fib_config.lloyd_iterations,
687 )?;
688 let seed = self.manifest.build_seed;
689 let mut key_codes = Vec::with_capacity(expected_codes);
690 for block in &layer.key_blocks {
691 key_codes.extend(adapter.decode_codes_payload(&block.encoded_payload, seed)?);
692 }
693 let mut value_codes = Vec::with_capacity(expected_codes);
694 for block in &layer.value_blocks {
695 value_codes.extend(adapter.decode_codes_payload(&block.encoded_payload, seed)?);
696 }
697 if key_codes.len() != expected_codes || value_codes.len() != expected_codes {
698 return Err(PolyKvError::Internal(format!(
699 "layer {layer_idx}: decoded {} key codes / {} value codes, expected {expected_codes}",
700 key_codes.len(),
701 value_codes.len()
702 )));
703 }
704 let quantizer = adapter.build_quantizer(seed)?;
705 let scorer = fib_quant::FibScorer::new(quantizer).map_err(|e| {
706 PolyKvError::Internal(format!("fib compressed scorer construction failed: {e}"))
707 })?;
708 Ok(PreparedCompressedIndex {
709 layer_idx,
710 head_idx,
711 head_dim,
712 num_tokens,
713 num_heads,
714 key_codes,
715 value_codes,
716 scorer,
717 })
718 }
719
720 #[cfg(feature = "fib")]
726 pub fn attention_topk_compressed_prepared(
727 &self,
728 index: &PreparedCompressedIndex,
729 query: &[f32],
730 top_k: usize,
731 ) -> Result<CompressedAttentionSelection> {
732 if query.len() != index.head_dim {
733 return Err(PolyKvError::DimensionMismatch {
734 expected: index.head_dim,
735 got: query.len(),
736 });
737 }
738 let head_idx = index.head_idx;
739 let num_heads = index.num_heads;
740 let num_tokens = index.num_tokens;
741 let prepared = index.scorer.prepare_query(query).map_err(|e| {
742 PolyKvError::Internal(format!("fib compressed query preparation failed: {e}"))
743 })?;
744 let mut scored: Vec<(usize, usize, f32)> = Vec::with_capacity(num_tokens);
745 for token_idx in 0..num_tokens {
746 let code_idx = token_idx * num_heads + head_idx;
747 let score = index
748 .scorer
749 .score_prepared(&prepared, &index.key_codes[code_idx])
750 .map_err(|e| PolyKvError::Internal(format!("fib compressed score failed: {e}")))?;
751 scored.push((token_idx, code_idx, score));
752 }
753 let selected = top_k.min(scored.len());
754 if selected > 0 && selected < scored.len() {
755 scored.select_nth_unstable_by(selected - 1, |a, b| {
756 b.2.total_cmp(&a.2).then_with(|| a.0.cmp(&b.0))
757 });
758 scored.truncate(selected);
759 }
760 scored.sort_by(|a, b| b.2.total_cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
761 let mut hits = Vec::with_capacity(selected);
762 for &(token_index, code_idx, score) in scored.iter().take(selected) {
763 let value = index
764 .scorer
765 .quantizer()
766 .decode(&index.value_codes[code_idx])
767 .map_err(|e| {
768 PolyKvError::DecompressionFailed(format!(
769 "fib selected value decode failed: {e}"
770 ))
771 })?;
772 if value.len() != index.head_dim {
773 return Err(PolyKvError::DimensionMismatch {
774 expected: index.head_dim,
775 got: value.len(),
776 });
777 }
778 hits.push(CompressedAttentionHit {
779 token_index,
780 score,
781 value,
782 });
783 }
784 let receipt = CompressedAttentionSelectionReceipt::new(
785 self.manifest.pool_id.clone(),
786 index.layer_idx as u32,
787 head_idx as u32,
788 num_tokens as u32,
789 hits.len() as u32,
790 num_tokens as u64,
791 hits.len() as u64,
792 false,
793 "fib_cold_pool_prepared_compressed_score_topk_value_decode",
794 self.manifest.shared_codec.clone(),
795 now_unix(),
796 );
797 receipt.validate()?;
798 Ok(CompressedAttentionSelection { hits, receipt })
799 }
800
801 #[cfg(feature = "fib")]
806 pub fn prepare_fully_compressed_index(
807 &self,
808 layer_idx: usize,
809 head_idx: usize,
810 ) -> Result<FullyPreparedCompressedIndex> {
811 let prep = self.prepare_compressed_index(layer_idx, head_idx)?;
812 let block_count = prep.scorer.quantizer().profile().block_count() as usize;
813 let wire_bits = prep.scorer.quantizer().profile().wire_index_bits;
814 let num_entries = prep.num_tokens * prep.num_heads;
815 let mut key_indices_flat = Vec::with_capacity(num_entries * block_count);
816 let mut key_norms = Vec::with_capacity(num_entries);
817 for i in 0..num_entries {
818 let indices = fib_quant::bitpack::unpack_indices(
819 &prep.key_codes[i].indices,
820 block_count,
821 wire_bits,
822 )
823 .map_err(|e| PolyKvError::Internal(format!("fib unpack_indices failed: {e}")))?;
824 key_indices_flat.extend_from_slice(&indices);
825 let norm = fib_quant::scoring::decode_stored_norm(
827 &prep.key_codes[i],
828 prep.scorer.quantizer().profile(),
829 )
830 .map_err(|e| PolyKvError::Internal(format!("fib decode_stored_norm failed: {e}")))?;
831 key_norms.push(norm as f32);
832 }
833 Ok(FullyPreparedCompressedIndex {
834 layer_idx: prep.layer_idx,
835 head_idx: prep.head_idx,
836 head_dim: prep.head_dim,
837 num_tokens: prep.num_tokens,
838 num_heads: prep.num_heads,
839 key_indices_flat,
840 key_norms,
841 block_count,
842 value_codes: prep.value_codes,
843 scorer: prep.scorer,
844 })
845 }
846
847 #[cfg(feature = "fib")]
853 pub fn attention_topk_fully_prepared(
854 &self,
855 index: &FullyPreparedCompressedIndex,
856 query: &[f32],
857 top_k: usize,
858 ) -> Result<CompressedAttentionSelection> {
859 self.attention_topk_prefetched(index, query, top_k)
860 }
861
862 #[cfg(feature = "fib")]
868 pub fn attention_topk_prefetched(
869 &self,
870 index: &FullyPreparedCompressedIndex,
871 query: &[f32],
872 top_k: usize,
873 ) -> Result<CompressedAttentionSelection> {
874 let prefetched = index.prepare_gram_rows(query)?;
875 let mut scored = index.score_all_tokens(&prefetched)?;
876
877 let selected = top_k.min(scored.len());
878 if selected > 0 && selected < scored.len() {
879 scored.select_nth_unstable_by(selected - 1, |a, b| {
880 b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0))
881 });
882 scored.truncate(selected);
883 }
884 scored.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
885
886 let num_tokens = index.num_tokens;
887 let head_idx = index.head_idx;
888 let num_heads = index.num_heads;
889 let mut hits = Vec::with_capacity(selected);
890 for &(token_index, score) in scored.iter().take(selected) {
891 let code_idx = token_index * num_heads + head_idx;
892 let value = index
893 .scorer
894 .quantizer()
895 .decode(&index.value_codes[code_idx])
896 .map_err(|e| {
897 PolyKvError::DecompressionFailed(format!(
898 "fib selected value decode failed: {e}"
899 ))
900 })?;
901 if value.len() != index.head_dim {
902 return Err(PolyKvError::DimensionMismatch {
903 expected: index.head_dim,
904 got: value.len(),
905 });
906 }
907 hits.push(CompressedAttentionHit {
908 token_index,
909 score,
910 value,
911 });
912 }
913 let receipt = CompressedAttentionSelectionReceipt::new(
914 self.manifest.pool_id.clone(),
915 index.layer_idx as u32,
916 head_idx as u32,
917 num_tokens as u32,
918 hits.len() as u32,
919 num_tokens as u64,
920 hits.len() as u64,
921 false,
922 "fib_cold_pool_prefetched_gram_rows_topk_value_decode",
923 self.manifest.shared_codec.clone(),
924 now_unix(),
925 );
926 receipt.validate()?;
927 Ok(CompressedAttentionSelection { hits, receipt })
928 }
929
930 #[cfg(feature = "fib")]
936 pub fn attention_topk_batch_heads(
937 &self,
938 index: &FullyPreparedCompressedIndex,
939 queries: &[&[f32]],
940 top_k: usize,
941 ) -> Result<Vec<CompressedAttentionSelection>> {
942 let num_heads = index.num_heads;
943 let num_tokens = index.num_tokens;
944 let head_dim = index.head_dim;
945 if queries.len() != num_heads {
946 return Err(PolyKvError::DimensionMismatch {
947 expected: num_heads,
948 got: queries.len(),
949 });
950 }
951
952 let mut all_prefetched: Vec<PrefetchedGramRows> = Vec::with_capacity(num_heads);
954 for q in queries.iter() {
955 if q.len() != head_dim {
956 return Err(PolyKvError::DimensionMismatch {
957 expected: head_dim,
958 got: q.len(),
959 });
960 }
961 let prepared = index
965 .scorer
966 .prepare_query(q)
967 .map_err(|e| PolyKvError::Internal(format!("fib batch query prep failed: {e}")))?;
968 let n = index.scorer.quantizer().profile().codebook_size as usize;
969 let block_count = index.scorer.quantizer().profile().block_count() as usize;
970 let gram = index.scorer.gram_table();
971 let mut gram_rows = vec![0.0f32; block_count * n];
972 for (block_idx, &query_idx) in prepared.query_indices.iter().enumerate() {
973 let qi = query_idx as usize;
974 if qi >= n {
975 return Err(PolyKvError::Internal(format!(
976 "fib batch: query_idx {qi} >= {n}"
977 )));
978 }
979 let src = &gram.values()[qi * n..(qi + 1) * n];
980 gram_rows[block_idx * n..(block_idx + 1) * n].copy_from_slice(src);
981 }
982 all_prefetched.push(PrefetchedGramRows {
983 gram_rows,
984 block_count,
985 n,
986 query_norm: prepared.query_norm,
987 });
988 }
989
990 let n = all_prefetched[0].n;
992 let block_count = all_prefetched[0].block_count;
993 let mut all_scored: Vec<Vec<(usize, f32)>> =
994 vec![vec![(0usize, 0.0f32); num_tokens]; num_heads];
995
996 for token_idx in 0..num_tokens {
997 for head_idx in 0..num_heads {
998 let code_idx = token_idx * num_heads + head_idx;
999 let indices = index.key_block(code_idx);
1000 let stored_norm = index.key_norms[code_idx];
1001 let q_norm = all_prefetched[head_idx].query_norm as f32;
1002 let gram_rows = &all_prefetched[head_idx].gram_rows;
1003
1004 let mut total = 0.0f32;
1005 for (block_idx, &stored_idx) in indices.iter().enumerate().take(block_count) {
1006 let si = stored_idx as usize;
1007 if si >= n {
1008 return Err(PolyKvError::Internal(format!(
1009 "fib batch: stored_idx {si} >= {n}"
1010 )));
1011 }
1012 total += gram_rows[block_idx * n + si];
1013 }
1014 let score = total * q_norm * stored_norm;
1015 all_scored[head_idx][token_idx] = (token_idx, score);
1016 }
1017 }
1018
1019 let mut results = Vec::with_capacity(num_heads);
1021 for (head_idx, scored) in all_scored.iter_mut().enumerate() {
1022 let selected = top_k.min(scored.len());
1023 if selected > 0 && selected < scored.len() {
1024 scored.select_nth_unstable_by(selected - 1, |a, b| {
1025 b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0))
1026 });
1027 scored.truncate(selected);
1028 }
1029 scored.sort_by(|a, b| b.1.total_cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
1030
1031 let mut hits = Vec::with_capacity(selected);
1032 for &(token_index, score) in scored.iter().take(selected) {
1033 let code_idx = token_index * num_heads + head_idx;
1034 let value = index
1035 .scorer
1036 .quantizer()
1037 .decode(&index.value_codes[code_idx])
1038 .map_err(|e| {
1039 PolyKvError::DecompressionFailed(format!(
1040 "fib batch value decode failed: {e}"
1041 ))
1042 })?;
1043 if value.len() != head_dim {
1044 return Err(PolyKvError::DimensionMismatch {
1045 expected: head_dim,
1046 got: value.len(),
1047 });
1048 }
1049 hits.push(CompressedAttentionHit {
1050 token_index,
1051 score,
1052 value,
1053 });
1054 }
1055 let receipt = CompressedAttentionSelectionReceipt::new(
1056 self.manifest.pool_id.clone(),
1057 index.layer_idx as u32,
1058 head_idx as u32,
1059 num_tokens as u32,
1060 hits.len() as u32,
1061 num_tokens as u64,
1062 hits.len() as u64,
1063 false,
1064 "fib_cold_pool_batch_heads_prefetched_gram_topk_value_decode",
1065 self.manifest.shared_codec.clone(),
1066 now_unix(),
1067 );
1068 receipt.validate()?;
1069 results.push(CompressedAttentionSelection { hits, receipt });
1070 }
1071 Ok(results)
1072 }
1073
1074 pub fn search_similar_tokens(
1081 &self,
1082 layer_idx: usize,
1083 query: &[f32],
1084 top_k: usize,
1085 ) -> Result<Vec<(usize, f32)>> {
1086 let decompressed = self.decompress_layer(layer_idx)?;
1087 let keys = decompressed
1088 .keys
1089 .first()
1090 .ok_or_else(|| PolyKvError::Internal("pool has no key heads".into()))?;
1091
1092 let head_dim = decompressed.head_dim;
1093 let num_tokens = keys.len() / head_dim;
1094
1095 let mut scored: Vec<(usize, f32)> = Vec::with_capacity(num_tokens);
1096 for i in 0..num_tokens {
1097 let start = i * head_dim;
1098 let vec = &keys[start..start + head_dim];
1099 let dot: f32 = query.iter().zip(vec.iter()).map(|(a, b)| a * b).sum();
1100 scored.push((i, dot));
1101 }
1102
1103 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1104 scored.truncate(top_k.min(num_tokens));
1105 Ok(scored)
1106 }
1107
1108 pub fn save_to_path(&self, path: &std::path::Path) -> Result<()> {
1116 let json = serde_json::to_string_pretty(&PoolFileEnvelope {
1117 schema: "polykv_pool_file_v1".into(),
1118 manifest: self.manifest.clone(),
1119 layers: self.layers.clone(),
1120 policy: self.policy.clone(),
1121 })
1122 .map_err(|e| PolyKvError::Internal(format!("pool serialize: {e}")))?;
1123 std::fs::write(path, &json)?;
1124 Ok(())
1125 }
1126
1127 pub fn load_from_path(path: &std::path::Path) -> Result<Self> {
1129 let json = std::fs::read_to_string(path)?;
1130 let envelope: PoolFileEnvelope = serde_json::from_str(&json)
1131 .map_err(|e| PolyKvError::Internal(format!("pool deserialize: {e}")))?;
1132 if envelope.schema != "polykv_pool_file_v1" {
1133 return Err(PolyKvError::Internal(format!(
1134 "unknown pool file schema: {}",
1135 envelope.schema
1136 )));
1137 }
1138 Ok(Self {
1139 manifest: envelope.manifest,
1140 layers: envelope.layers,
1141 policy: envelope.policy,
1142 })
1143 }
1144}
1145
1146#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1148struct PoolFileEnvelope {
1149 schema: String,
1150 manifest: PoolManifest,
1151 layers: Vec<PoolLayer>,
1152 policy: CompressionPolicy,
1153}
1154
1155#[cfg(feature = "fib")]
1161pub struct PreparedCompressedIndex {
1162 pub layer_idx: usize,
1164 pub head_idx: usize,
1166 pub head_dim: usize,
1168 pub num_tokens: usize,
1170 pub num_heads: usize,
1172 pub key_codes: Vec<fib_quant::FibCodeV1>,
1174 pub value_codes: Vec<fib_quant::FibCodeV1>,
1176 pub scorer: fib_quant::FibScorer,
1178}
1179
1180#[cfg(feature = "fib")]
1185pub struct FullyPreparedCompressedIndex {
1186 pub layer_idx: usize,
1188 pub head_idx: usize,
1190 pub head_dim: usize,
1192 pub num_tokens: usize,
1194 pub num_heads: usize,
1196 pub key_indices_flat: Vec<u32>,
1200 pub key_norms: Vec<f32>,
1202 pub block_count: usize,
1204 pub value_codes: Vec<fib_quant::FibCodeV1>,
1206 pub scorer: fib_quant::FibScorer,
1208}
1209
1210#[cfg(feature = "fib")]
1217pub struct PrefetchedGramRows {
1218 pub gram_rows: Vec<f32>,
1221 pub block_count: usize,
1223 pub n: usize,
1225 pub query_norm: f64,
1227}
1228
1229#[cfg(feature = "fib")]
1230impl FullyPreparedCompressedIndex {
1231 #[inline]
1233 pub fn key_block(&self, code_idx: usize) -> &[u32] {
1234 let start = code_idx * self.block_count;
1235 &self.key_indices_flat[start..start + self.block_count]
1236 }
1237
1238 pub fn prepare_gram_rows(&self, query: &[f32]) -> Result<PrefetchedGramRows> {
1244 if query.len() != self.head_dim {
1245 return Err(PolyKvError::DimensionMismatch {
1246 expected: self.head_dim,
1247 got: query.len(),
1248 });
1249 }
1250 let prepared = self
1251 .scorer
1252 .prepare_query(query)
1253 .map_err(|e| PolyKvError::Internal(format!("fib query preparation failed: {e}")))?;
1254 let n = self.scorer.quantizer().profile().codebook_size as usize;
1255 let block_count = self.scorer.quantizer().profile().block_count() as usize;
1256 let gram = self.scorer.gram_table();
1257
1258 let mut gram_rows = vec![0.0f32; block_count * n];
1260 for (block_idx, &query_idx) in prepared.query_indices.iter().enumerate() {
1261 let qi = query_idx as usize;
1262 if qi >= n {
1263 return Err(PolyKvError::Internal(format!(
1264 "fib prepare_gram_rows: query_idx {qi} >= {n}"
1265 )));
1266 }
1267 let src = &gram.values()[qi * n..(qi + 1) * n];
1268 gram_rows[block_idx * n..(block_idx + 1) * n].copy_from_slice(src);
1269 }
1270
1271 Ok(PrefetchedGramRows {
1272 gram_rows,
1273 block_count,
1274 n,
1275 query_norm: prepared.query_norm,
1276 })
1277 }
1278
1279 pub fn score_all_tokens(&self, prefetched: &PrefetchedGramRows) -> Result<Vec<(usize, f32)>> {
1289 let head_idx = self.head_idx;
1290 let num_heads = self.num_heads;
1291 let num_tokens = self.num_tokens;
1292 let q_norm = prefetched.query_norm as f32;
1293 let n = prefetched.n;
1294 let block_count = prefetched.block_count;
1295 let gram_rows = &prefetched.gram_rows;
1296
1297 let mut scored: Vec<(usize, f32)> = vec![(0usize, 0.0f32); num_tokens];
1298 for token_idx in 0..num_tokens {
1299 let code_idx = token_idx * num_heads + head_idx;
1300 let indices = self.key_block(code_idx);
1301 let stored_norm = self.key_norms[code_idx];
1302
1303 let mut total = 0.0f32;
1304 for (block_idx, &stored_idx) in indices.iter().enumerate().take(block_count) {
1305 let si = stored_idx as usize;
1306 if si >= n {
1307 return Err(PolyKvError::Internal(format!(
1308 "fib score_all_tokens: stored_idx {si} >= {n}"
1309 )));
1310 }
1311 total += gram_rows[block_idx * n + si];
1312 }
1313 let score = total * q_norm * stored_norm;
1314 scored[token_idx] = (token_idx, score);
1315 }
1316 Ok(scored)
1317 }
1318}
1319
1320#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1328pub struct DecompressedLayer {
1329 pub layer_index: u32,
1331 pub num_tokens: usize,
1333 pub num_heads: usize,
1335 pub head_dim: usize,
1337 pub keys: Vec<Vec<f32>>,
1340 pub values: Vec<Vec<f32>>,
1342}
1343
1344pub trait CacheTarget: std::fmt::Debug {
1346 fn num_layers(&self) -> u32;
1348
1349 fn append_key(&mut self, layer: u32, position: u32, key: &[f32]) -> Result<()>;
1351
1352 fn append_value(&mut self, layer: u32, position: u32, value: &[f32]) -> Result<()>;
1354
1355 fn seq_len(&self) -> u32;
1357}
1358
1359#[cfg(test)]
1360mod tests {
1361 use super::*;
1362 use crate::shape::AttentionType;
1363
1364 fn make_test_shape() -> KvTensorShape {
1365 KvTensorShape {
1366 attention_type: AttentionType::MHA,
1367 num_layers: 2,
1368 num_heads: 4,
1369 num_kv_heads: 4,
1370 head_dim: 8, hidden_size: 32,
1372 }
1373 }
1374
1375 fn make_test_corpus(n: usize) -> Vec<(String, Vec<f32>)> {
1376 use rand::Rng;
1377 use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng};
1378 let mut rng = ChaCha8Rng::seed_from_u64(42);
1379 let shape = make_test_shape();
1380 let vec_len = shape.num_layers as usize * shape.num_kv_heads as usize * shape.head_dim * 2;
1381
1382 (0..n)
1383 .map(|i| {
1384 let vec: Vec<f32> = (0..vec_len).map(|_| rng.gen_range(-1.0..1.0)).collect();
1385 (format!("token_{}", i), vec)
1386 })
1387 .collect()
1388 }
1389
1390 #[test]
1391 fn test_pool_build_empty() {
1392 let shape = make_test_shape();
1393 let corpus: Vec<(String, Vec<f32>)> = vec![];
1394 let result = SharedKVPool::build(&corpus, &shape, 42);
1395 assert!(result.is_err());
1396 }
1397
1398 #[test]
1399 fn test_pool_build_basic() {
1400 let shape = make_test_shape();
1401 let corpus = make_test_corpus(4);
1402 let result = SharedKVPool::build(&corpus, &shape, 42);
1403 assert!(result.is_ok(), "build failed: {:?}", result.err());
1404
1405 let (pool, receipt) = result.unwrap();
1406 assert_eq!(pool.layers.len(), 2);
1407 assert_eq!(pool.manifest.num_shared_tokens, 4);
1408 assert_eq!(receipt.total_tokens, 4);
1409 assert!(
1410 receipt.compression_ratio > 0.0,
1411 "compression ratio: {}",
1412 receipt.compression_ratio
1413 );
1414 }
1417
1418 #[test]
1419 fn test_pool_build_deterministic() {
1420 let shape = make_test_shape();
1421 let corpus = make_test_corpus(4);
1422
1423 let (pool1, receipt1) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1424 let (pool2, receipt2) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1425
1426 assert_eq!(receipt1.pool_digest, receipt2.pool_digest);
1427 assert_eq!(receipt1.layer_digests, receipt2.layer_digests);
1428 assert_eq!(pool1.layers[0].block_digest, pool2.layers[0].block_digest);
1429 }
1430
1431 #[test]
1432 fn test_pool_build_different_seeds() {
1433 let shape = make_test_shape();
1434 let corpus = make_test_corpus(4);
1435
1436 let (_pool1, receipt1) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1437 let (_pool2, receipt2) = SharedKVPool::build(&corpus, &shape, 12345).unwrap();
1438
1439 assert_ne!(receipt1.pool_digest, receipt2.pool_digest);
1440 }
1441
1442 #[test]
1443 fn test_decompress_layer_recovers_finite_floats() {
1444 let shape = make_test_shape();
1448 let corpus = make_test_corpus(8);
1449 let (pool, _) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1450
1451 for layer_idx in 0..shape.num_layers as usize {
1452 let decompressed = pool.decompress_layer(layer_idx).unwrap();
1453 assert_eq!(decompressed.num_tokens, 8);
1454 assert_eq!(decompressed.num_heads, shape.num_kv_heads as usize);
1455 assert_eq!(decompressed.head_dim, shape.head_dim);
1456 assert_eq!(decompressed.keys.len(), shape.num_kv_heads as usize);
1457 assert_eq!(decompressed.values.len(), shape.num_kv_heads as usize);
1458 for h in 0..decompressed.num_heads {
1459 assert_eq!(decompressed.keys[h].len(), 8 * shape.head_dim);
1460 assert_eq!(decompressed.values[h].len(), 8 * shape.head_dim);
1461 assert!(decompressed.keys[h].iter().all(|v| v.is_finite()));
1462 assert!(decompressed.values[h].iter().all(|v| v.is_finite()));
1463 }
1464 }
1465 }
1466
1467 #[test]
1468 fn test_decompress_layer_is_deterministic() {
1469 let shape = make_test_shape();
1474 let corpus = make_test_corpus(6);
1475 let (pool_a, _) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1476 let (pool_b, _) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1477 for layer_idx in 0..shape.num_layers as usize {
1478 let a = pool_a.decompress_layer(layer_idx).unwrap();
1479 let b = pool_b.decompress_layer(layer_idx).unwrap();
1480 assert_eq!(
1481 a.keys, b.keys,
1482 "decompressed K tensors must be deterministic across builds (layer {})",
1483 layer_idx
1484 );
1485 assert_eq!(a.values, b.values);
1486 }
1487 }
1488
1489 #[test]
1490 fn test_mismatched_shape_rejected() {
1491 let shape = make_test_shape();
1492 let mut bad_corpus = make_test_corpus(1);
1493 bad_corpus[0].1.truncate(10);
1495 let result = SharedKVPool::build(&bad_corpus, &shape, 42);
1496 assert!(result.is_err());
1497 }
1498
1499 #[test]
1500 fn test_pool_build_writes_single_fb2_payload_per_layer_side() {
1501 let shape = make_test_shape();
1502 let corpus = make_test_corpus(32);
1503 let (pool, receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1504
1505 assert_eq!(pool.layers.len(), shape.num_layers as usize);
1506 for layer in &pool.layers {
1507 assert_eq!(
1508 layer.key_blocks.len(),
1509 1,
1510 "pool layer keys must be stored as one batched payload, not per-vector blocks"
1511 );
1512 assert_eq!(
1513 layer.value_blocks.len(),
1514 1,
1515 "pool layer values must be stored as one batched payload, not per-vector blocks"
1516 );
1517 assert_eq!(&layer.key_blocks[0].encoded_payload[0..4], b"FBWB");
1518 assert_eq!(&layer.value_blocks[0].encoded_payload[0..4], b"FBWB");
1519 }
1520 let raw_bytes = shape.total_kv_bytes(corpus.len()) as f64;
1521 let ratio = raw_bytes / receipt.pool_size_bytes as f64;
1522 assert!(
1527 ratio > 0.2,
1528 "batched pool should show some compression; ratio={ratio:.2}"
1529 );
1530 }
1531
1532 #[test]
1537 fn test_pool_build_digest_invariant_across_corpora_size() {
1538 let shape = make_test_shape();
1539
1540 let small = make_test_corpus(4);
1542 let (pool_small, receipt_small) = SharedKVPool::build(&small, &shape, 42).unwrap();
1543
1544 let large = make_test_corpus(40);
1546 let (pool_large, receipt_large) = SharedKVPool::build(&large, &shape, 42).unwrap();
1547
1548 assert!(!pool_small.layers.is_empty());
1549 assert!(!pool_large.layers.is_empty());
1550 assert!(receipt_small.backend == "cpu" || receipt_small.backend == "gpu");
1551 assert!(receipt_large.backend == "cpu" || receipt_large.backend == "gpu");
1552
1553 assert_eq!(
1558 receipt_small.backend, "cpu",
1559 "corpus under GPU batch threshold should fall through to CPU"
1560 );
1561 }
1562
1563 #[test]
1564 fn test_search_similar_tokens_returns_top_k() {
1565 let shape = make_test_shape();
1566 let corpus = make_test_corpus(32);
1567 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1568
1569 let query: Vec<f32> = (0..shape.head_dim).map(|x| x as f32 * 0.1).collect();
1571 let results = pool.search_similar_tokens(0, &query, 5).unwrap();
1572
1573 assert!(!results.is_empty(), "search should return results");
1574 assert!(results.len() <= 5, "should return at most top_k");
1575 for (idx, _) in &results {
1577 assert!(*idx < 32, "token index must be in range of corpus size");
1578 }
1579 for w in results.windows(2) {
1581 assert!(w[0].1 >= w[1].1, "scores should be descending");
1582 }
1583 }
1584
1585 #[test]
1586 fn test_prepared_compressed_index_matches_regular_attention() {
1587 let shape = make_test_shape();
1588 let corpus = make_test_corpus(16);
1589 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1590 let query: Vec<f32> = (0..shape.head_dim).map(|x| x as f32 * 0.125).collect();
1591
1592 let regular = pool
1593 .attention_topk_compressed(0, 0, &query, 5)
1594 .expect("regular compressed attention should work");
1595
1596 let index = pool
1597 .prepare_compressed_index(0, 0)
1598 .expect("prepare compressed index should work");
1599
1600 let prepared = pool
1601 .attention_topk_compressed_prepared(&index, &query, 5)
1602 .expect("prepared compressed attention should work");
1603
1604 assert_eq!(prepared.hits.len(), regular.hits.len());
1605 for (a, b) in prepared.hits.iter().zip(regular.hits.iter()) {
1606 assert_eq!(a.token_index, b.token_index);
1607 assert!((a.score - b.score).abs() < 1e-5);
1608 assert_eq!(a.value.len(), b.value.len());
1609 }
1610 assert_eq!(
1611 prepared.receipt.scoring_path,
1612 "fib_cold_pool_prepared_compressed_score_topk_value_decode"
1613 );
1614 }
1615
1616 #[test]
1617 fn test_prepared_compressed_index_rejects_wrong_query_dimension() {
1618 let shape = make_test_shape();
1619 let corpus = make_test_corpus(8);
1620 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1621 let index = pool
1622 .prepare_compressed_index(0, 0)
1623 .expect("prepare compressed index should work");
1624 let err = pool
1625 .attention_topk_compressed_prepared(&index, &[1.0, 2.0], 3)
1626 .expect_err("wrong query dimension must fail");
1627 assert!(matches!(
1628 err,
1629 PolyKvError::DimensionMismatch {
1630 expected: 8,
1631 got: 2
1632 }
1633 ));
1634 }
1635
1636 #[test]
1637 fn test_fully_prepared_compressed_index_matches_regular_attention() {
1638 let shape = make_test_shape();
1639 let corpus = make_test_corpus(16);
1640 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1641 let query: Vec<f32> = (0..shape.head_dim).map(|x| x as f32 * 0.125).collect();
1642
1643 let regular = pool
1644 .attention_topk_compressed(0, 0, &query, 5)
1645 .expect("regular compressed attention should work");
1646
1647 let fully_index = pool
1648 .prepare_fully_compressed_index(0, 0)
1649 .expect("prepare fully compressed index should work");
1650
1651 let fully_prepared = pool
1652 .attention_topk_fully_prepared(&fully_index, &query, 5)
1653 .expect("fully prepared compressed attention should work");
1654
1655 assert_eq!(fully_prepared.hits.len(), regular.hits.len());
1659 for (a, b) in fully_prepared.hits.iter().zip(regular.hits.iter()) {
1660 assert_eq!(a.token_index, b.token_index);
1661 assert_eq!(a.value.len(), b.value.len());
1662 }
1663 assert_eq!(
1664 fully_prepared.receipt.scoring_path,
1665 "fib_cold_pool_prefetched_gram_rows_topk_value_decode"
1666 );
1667 }
1668
1669 #[test]
1670 fn test_fully_prepared_compressed_index_rejects_wrong_query_dimension() {
1671 let shape = make_test_shape();
1672 let corpus = make_test_corpus(8);
1673 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1674 let fully_index = pool
1675 .prepare_fully_compressed_index(0, 0)
1676 .expect("prepare fully compressed index should work");
1677 let err = pool
1678 .attention_topk_fully_prepared(&fully_index, &[1.0, 2.0], 3)
1679 .expect_err("wrong query dimension must fail");
1680 assert!(matches!(
1681 err,
1682 PolyKvError::DimensionMismatch {
1683 expected: 8,
1684 got: 2
1685 }
1686 ));
1687 }
1688
1689 #[test]
1690 fn test_prefetched_gram_rows_matches_regular_attention() {
1691 let shape = make_test_shape();
1692 let corpus = make_test_corpus(16);
1693 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1694 let query: Vec<f32> = (0..shape.head_dim).map(|x| x as f32 * 0.125).collect();
1695
1696 let regular = pool
1697 .attention_topk_compressed(0, 0, &query, 5)
1698 .expect("regular compressed attention should work");
1699
1700 let fully_index = pool
1701 .prepare_fully_compressed_index(0, 0)
1702 .expect("prepare fully compressed index should work");
1703
1704 let prefetched = pool
1705 .attention_topk_prefetched(&fully_index, &query, 5)
1706 .expect("prefetched compressed attention should work");
1707
1708 assert_eq!(prefetched.hits.len(), regular.hits.len());
1709 for (a, b) in prefetched.hits.iter().zip(regular.hits.iter()) {
1710 assert_eq!(a.token_index, b.token_index);
1711 }
1712 assert_eq!(
1713 prefetched.receipt.scoring_path,
1714 "fib_cold_pool_prefetched_gram_rows_topk_value_decode"
1715 );
1716 }
1717
1718 #[test]
1719 fn test_batch_heads_returns_correct_count() {
1720 let shape = make_test_shape();
1721 let corpus = make_test_corpus(16);
1722 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1723
1724 let fully_index = pool
1725 .prepare_fully_compressed_index(0, 0)
1726 .expect("prepare fully compressed index should work");
1727
1728 let queries: Vec<Vec<f32>> = (0..shape.num_kv_heads as usize)
1729 .map(|h| {
1730 (0..shape.head_dim)
1731 .map(|x| x as f32 * 0.125 + h as f32 * 0.01)
1732 .collect()
1733 })
1734 .collect();
1735 let query_refs: Vec<&[f32]> = queries.iter().map(|q| q.as_slice()).collect();
1736
1737 let results = pool
1738 .attention_topk_batch_heads(&fully_index, &query_refs, 5)
1739 .expect("batch heads should work");
1740
1741 assert_eq!(results.len(), shape.num_kv_heads as usize);
1742 for r in &results {
1743 assert_eq!(r.hits.len(), 5);
1744 assert_eq!(
1745 r.receipt.scoring_path,
1746 "fib_cold_pool_batch_heads_prefetched_gram_topk_value_decode"
1747 );
1748 }
1749 }
1750
1751 #[test]
1752 fn test_compressed_attention_topk_scores_cold_pool_without_full_layer_decode() {
1753 let shape = make_test_shape();
1754 let corpus = make_test_corpus(32);
1755 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1756 let query: Vec<f32> = (0..shape.head_dim).map(|x| x as f32 * 0.1).collect();
1757
1758 let out = pool
1759 .attention_topk_compressed(0, 0, &query, 3)
1760 .expect("compressed attention selection should work over fib cold pool");
1761
1762 assert_eq!(out.hits.len(), 3);
1763 assert_eq!(
1764 out.receipt.schema_version,
1765 "compressed_attention_selection_receipt_v1"
1766 );
1767 assert_eq!(out.receipt.layer, 0);
1768 assert_eq!(out.receipt.head, 0);
1769 assert_eq!(out.receipt.candidate_count, 32);
1770 assert_eq!(out.receipt.selected_count, 3);
1771 assert_eq!(out.receipt.compressed_key_scores, 32);
1772 assert_eq!(out.receipt.decoded_value_vectors, 3);
1773 assert!(!out.receipt.full_layer_decoded);
1774 assert_eq!(
1775 out.receipt.scoring_path,
1776 "fib_cold_pool_compressed_score_topk_value_decode"
1777 );
1778 for hit in &out.hits {
1779 assert!(hit.token_index < 32);
1780 assert_eq!(hit.value.len(), shape.head_dim);
1781 assert!(hit.value.iter().all(|v| v.is_finite()));
1782 }
1783 for window in out.hits.windows(2) {
1784 assert!(window[0].score >= window[1].score);
1785 }
1786 }
1787
1788 #[test]
1789 fn test_compressed_attention_topk_rejects_wrong_query_dimension() {
1790 let shape = make_test_shape();
1791 let corpus = make_test_corpus(8);
1792 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1793
1794 let err = pool
1795 .attention_topk_compressed(0, 0, &[1.0, 2.0], 3)
1796 .expect_err("wrong query dimension must fail before scoring");
1797
1798 assert!(matches!(
1799 err,
1800 PolyKvError::DimensionMismatch {
1801 expected: 8,
1802 got: 2
1803 }
1804 ));
1805 }
1806
1807 #[test]
1808 fn test_persistence_roundtrip() {
1809 let shape = make_test_shape();
1810 let corpus = make_test_corpus(16);
1811 let (pool, _receipt) = SharedKVPool::build(&corpus, &shape, 42).unwrap();
1812
1813 let json = serde_json::to_string_pretty(&PoolFileEnvelope {
1815 schema: "polykv_pool_file_v1".into(),
1816 manifest: pool.manifest.clone(),
1817 layers: pool.layers.clone(),
1818 policy: pool.policy.clone(),
1819 })
1820 .unwrap();
1821
1822 let envelope: PoolFileEnvelope = serde_json::from_str(&json).unwrap();
1823 let loaded = SharedKVPool {
1824 manifest: envelope.manifest,
1825 layers: envelope.layers,
1826 policy: envelope.policy,
1827 };
1828
1829 assert_eq!(pool.layers.len(), loaded.layers.len());
1830 assert_eq!(
1831 pool.layers[0].key_blocks.len(),
1832 loaded.layers[0].key_blocks.len()
1833 );
1834 assert_eq!(pool.manifest.pool_id, loaded.manifest.pool_id);
1835
1836 let query: Vec<f32> = (0..shape.head_dim).map(|x| x as f32 * 0.1).collect();
1838 let orig_results = pool.search_similar_tokens(0, &query, 3).unwrap();
1839 let loaded_results = loaded.search_similar_tokens(0, &query, 3).unwrap();
1840 assert_eq!(orig_results, loaded_results);
1841 }
1842}