1use std::{
4 collections::{BTreeMap, BTreeSet},
5 ops::Range,
6};
7
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11use crate::attention::{AttentionPolicy, LayerSchedule};
12
13use super::{
14 CachePolicyError, CacheRankIdentity, CacheRepresentation, LayerCachePolicy, StateTensorOwner,
15 StateTensorRole,
16};
17
18pub const PROMPT_CACHE_SCHEMA_VERSION: u32 = 8;
20
21#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
23pub struct PromptCacheStateSegment {
24 id: String,
25 layers: Range<usize>,
26}
27
28impl PromptCacheStateSegment {
29 pub fn new(id: impl Into<String>, layers: Range<usize>) -> Result<Self, PromptCacheError> {
31 let id = id.into();
32 if id.trim().is_empty() {
33 return Err(PromptCacheError::Malformed(
34 "prompt-cache state segment identity must not be empty".into(),
35 ));
36 }
37 if layers.is_empty() {
38 return Err(PromptCacheError::Malformed(format!(
39 "prompt-cache state segment {id:?} has an empty range"
40 )));
41 }
42 Ok(Self { id, layers })
43 }
44
45 pub fn id(&self) -> &str {
47 &self.id
48 }
49
50 pub fn layers(&self) -> Range<usize> {
52 self.layers.clone()
53 }
54}
55
56#[derive(Debug, Clone, Eq, Hash, PartialEq)]
58pub struct PromptCacheDescriptor {
59 model_family: String,
61 effective_model_type: String,
63 checkpoint_fingerprint: String,
65 prefix_content_fingerprint: String,
67 architecture_fingerprint: String,
69 layer_count: usize,
71 global_layer_start: usize,
73 global_layer_end: usize,
75 batch_size: usize,
77 layer_layout: LayerSchedule<LayerCachePolicy>,
79 layer_prefix_offsets: Vec<i32>,
81 state_segments: Vec<PromptCacheStateSegment>,
83 sink_tokens: usize,
85 topology: PromptCacheTopology,
87 distributed_commit: Option<crate::DistributedCommitOutcome>,
89}
90
91impl PromptCacheDescriptor {
92 #[allow(clippy::too_many_arguments)]
94 pub fn new(
95 model_family: impl Into<String>,
96 effective_model_type: impl Into<String>,
97 checkpoint_fingerprint: impl Into<String>,
98 prefix_content_fingerprint: impl Into<String>,
99 architecture_fingerprint: impl Into<String>,
100 layer_count: usize,
101 global_layer_start: usize,
102 global_layer_end: usize,
103 batch_size: usize,
104 layer_layout: LayerSchedule<LayerCachePolicy>,
105 layer_prefix_offsets: Vec<i32>,
106 state_segments: Vec<PromptCacheStateSegment>,
107 sink_tokens: usize,
108 topology: PromptCacheTopology,
109 ) -> Result<Self, PromptCacheError> {
110 let descriptor = Self {
111 model_family: model_family.into(),
112 effective_model_type: effective_model_type.into(),
113 checkpoint_fingerprint: checkpoint_fingerprint.into(),
114 prefix_content_fingerprint: prefix_content_fingerprint.into(),
115 architecture_fingerprint: architecture_fingerprint.into(),
116 layer_count,
117 global_layer_start,
118 global_layer_end,
119 batch_size,
120 layer_layout,
121 layer_prefix_offsets,
122 state_segments,
123 sink_tokens,
124 topology,
125 distributed_commit: None,
126 };
127 for value in [
128 &descriptor.model_family,
129 &descriptor.effective_model_type,
130 &descriptor.checkpoint_fingerprint,
131 &descriptor.prefix_content_fingerprint,
132 &descriptor.architecture_fingerprint,
133 ] {
134 if value.trim().is_empty() {
135 return Err(PromptCacheError::Malformed(
136 "prompt-cache identity strings must be non-empty".into(),
137 ));
138 }
139 }
140 descriptor.validate()?;
141 Ok(descriptor)
142 }
143
144 pub fn model_family(&self) -> &str {
146 &self.model_family
147 }
148 pub fn effective_model_type(&self) -> &str {
150 &self.effective_model_type
151 }
152 pub fn checkpoint_fingerprint(&self) -> &str {
154 &self.checkpoint_fingerprint
155 }
156 pub fn prefix_content_fingerprint(&self) -> &str {
158 &self.prefix_content_fingerprint
159 }
160 pub fn architecture_fingerprint(&self) -> &str {
162 &self.architecture_fingerprint
163 }
164 pub const fn layer_count(&self) -> usize {
166 self.layer_count
167 }
168 pub const fn global_layer_start(&self) -> usize {
170 self.global_layer_start
171 }
172 pub const fn global_layer_end(&self) -> usize {
174 self.global_layer_end
175 }
176 pub const fn batch_size(&self) -> usize {
178 self.batch_size
179 }
180 pub const fn layer_layout(&self) -> &LayerSchedule<LayerCachePolicy> {
182 &self.layer_layout
183 }
184 pub fn layer_prefix_offsets(&self) -> &[i32] {
186 &self.layer_prefix_offsets
187 }
188 pub fn state_segments(&self) -> &[PromptCacheStateSegment] {
190 &self.state_segments
191 }
192 pub const fn sink_tokens(&self) -> usize {
194 self.sink_tokens
195 }
196 pub const fn topology(&self) -> &PromptCacheTopology {
198 &self.topology
199 }
200 pub const fn distributed_commit(&self) -> Option<crate::DistributedCommitOutcome> {
202 self.distributed_commit
203 }
204 pub const fn with_distributed_commit(
206 mut self,
207 outcome: Option<crate::DistributedCommitOutcome>,
208 ) -> Self {
209 self.distributed_commit = outcome;
210 self
211 }
212 pub fn with_topology(
214 mut self,
215 topology: PromptCacheTopology,
216 ) -> Result<Self, PromptCacheError> {
217 self.topology = topology;
218 self.validate()?;
219 Ok(self)
220 }
221 pub fn with_architecture_fingerprint(
223 mut self,
224 architecture_fingerprint: impl Into<String>,
225 ) -> Result<Self, PromptCacheError> {
226 self.architecture_fingerprint = architecture_fingerprint.into();
227 if self.architecture_fingerprint.trim().is_empty() {
228 return Err(PromptCacheError::Malformed(
229 "prompt-cache architecture fingerprint must be non-empty".into(),
230 ));
231 }
232 self.validate()?;
233 Ok(self)
234 }
235 pub fn with_layer_count(mut self, layer_count: usize) -> Result<Self, PromptCacheError> {
237 self.layer_count = layer_count;
238 self.validate()?;
239 Ok(self)
240 }
241 pub fn from_model_identity(
247 model: PromptCacheModelIdentity,
248 checkpoint_fingerprint: impl Into<String>,
249 prefix_content_fingerprint: impl Into<String>,
250 batch_size: usize,
251 ) -> Result<Self, PromptCacheError> {
252 let descriptor = Self {
253 model_family: model.model_family,
254 effective_model_type: model.effective_model_type,
255 checkpoint_fingerprint: checkpoint_fingerprint.into(),
256 prefix_content_fingerprint: prefix_content_fingerprint.into(),
257 architecture_fingerprint: model.architecture_fingerprint,
258 layer_count: model.layer_count,
259 global_layer_start: model.global_layer_start,
260 global_layer_end: model.global_layer_end,
261 batch_size,
262 layer_layout: model.layer_layout,
263 layer_prefix_offsets: model.layer_prefix_offsets,
264 state_segments: model.state_segments,
265 sink_tokens: model.sink_tokens,
266 topology: model.topology,
267 distributed_commit: None,
268 };
269 descriptor.validate()?;
270 Ok(descriptor)
271 }
272
273 pub fn validate(&self) -> Result<(), PromptCacheError> {
275 IdentityLayout {
276 layer_count: self.layer_count,
277 global_layer_start: self.global_layer_start,
278 global_layer_end: self.global_layer_end,
279 batch_size: self.batch_size,
280 layer_layout: &self.layer_layout,
281 layer_prefix_offsets: &self.layer_prefix_offsets,
282 state_segments: &self.state_segments,
283 topology: &self.topology,
284 }
285 .validate("prompt-cache descriptor")
286 }
287}
288
289#[derive(Debug, Clone, Eq, Hash, PartialEq)]
291pub struct PromptCacheModelIdentity {
292 model_family: String,
294 effective_model_type: String,
296 architecture_fingerprint: String,
298 layer_count: usize,
300 global_layer_start: usize,
302 global_layer_end: usize,
304 sink_tokens: usize,
306 topology: PromptCacheTopology,
308 layer_layout: LayerSchedule<LayerCachePolicy>,
310 layer_prefix_offsets: Vec<i32>,
312 state_segments: Vec<PromptCacheStateSegment>,
314}
315
316impl PromptCacheModelIdentity {
317 #[allow(clippy::too_many_arguments)]
319 pub fn new(
320 model_family: impl Into<String>,
321 effective_model_type: impl Into<String>,
322 architecture_fingerprint: impl Into<String>,
323 layer_count: usize,
324 global_layer_start: usize,
325 global_layer_end: usize,
326 sink_tokens: usize,
327 topology: PromptCacheTopology,
328 layer_layout: LayerSchedule<LayerCachePolicy>,
329 layer_prefix_offsets: Vec<i32>,
330 state_segments: Vec<PromptCacheStateSegment>,
331 ) -> Result<Self, PromptCacheError> {
332 let identity = Self {
333 model_family: model_family.into(),
334 effective_model_type: effective_model_type.into(),
335 architecture_fingerprint: architecture_fingerprint.into(),
336 layer_count,
337 global_layer_start,
338 global_layer_end,
339 sink_tokens,
340 topology,
341 layer_layout,
342 layer_prefix_offsets,
343 state_segments,
344 };
345 for value in [
346 &identity.model_family,
347 &identity.effective_model_type,
348 &identity.architecture_fingerprint,
349 ] {
350 if value.trim().is_empty() {
351 return Err(PromptCacheError::Malformed(
352 "prompt-cache model identity strings must be non-empty".into(),
353 ));
354 }
355 }
356 identity.validate()?;
357 Ok(identity)
358 }
359
360 pub fn model_family(&self) -> &str {
362 &self.model_family
363 }
364 pub fn effective_model_type(&self) -> &str {
366 &self.effective_model_type
367 }
368 pub fn architecture_fingerprint(&self) -> &str {
370 &self.architecture_fingerprint
371 }
372 pub const fn layer_count(&self) -> usize {
374 self.layer_count
375 }
376 pub const fn global_layer_start(&self) -> usize {
378 self.global_layer_start
379 }
380 pub const fn global_layer_end(&self) -> usize {
382 self.global_layer_end
383 }
384 pub const fn sink_tokens(&self) -> usize {
386 self.sink_tokens
387 }
388 pub const fn topology(&self) -> &PromptCacheTopology {
390 &self.topology
391 }
392 pub const fn layer_layout(&self) -> &LayerSchedule<LayerCachePolicy> {
394 &self.layer_layout
395 }
396 pub fn layer_prefix_offsets(&self) -> &[i32] {
398 &self.layer_prefix_offsets
399 }
400 pub fn state_segments(&self) -> &[PromptCacheStateSegment] {
402 &self.state_segments
403 }
404 pub fn key_value_layouts(
406 sliding_windows: impl IntoIterator<Item = Option<i32>>,
407 num_key_value_heads: i32,
408 head_dim: i32,
409 ) -> Result<LayerSchedule<LayerCachePolicy>, PromptCacheError> {
410 let policies = sliding_windows
411 .into_iter()
412 .map(|window| {
413 let attention = AttentionPolicy::from_sliding_window(window)
414 .map_err(|error| PromptCacheError::Malformed(error.to_string()))?;
415 LayerCachePolicy::key_value(attention, num_key_value_heads, head_dim)
416 .map_err(PromptCacheError::from)
417 })
418 .collect::<Result<Vec<_>, _>>()?;
419 LayerSchedule::new(policies.len(), policies)
420 .map_err(|error| PromptCacheError::Malformed(error.to_string()))
421 }
422
423 pub fn compressed_layouts(
425 layer_count: usize,
426 latent_dim: i32,
427 rotary_dim: i32,
428 ) -> Result<LayerSchedule<LayerCachePolicy>, PromptCacheError> {
429 let policies = (0..layer_count)
430 .map(|_| {
431 LayerCachePolicy::compressed_latent_rotary(
432 AttentionPolicy::Full,
433 latent_dim,
434 rotary_dim,
435 )
436 .map_err(PromptCacheError::from)
437 })
438 .collect::<Result<Vec<_>, _>>()?;
439 LayerSchedule::new(layer_count, policies)
440 .map_err(|error| PromptCacheError::Malformed(error.to_string()))
441 }
442
443 pub fn validate(&self) -> Result<(), PromptCacheError> {
445 IdentityLayout {
446 layer_count: self.layer_count,
447 global_layer_start: self.global_layer_start,
448 global_layer_end: self.global_layer_end,
449 batch_size: 1,
450 layer_layout: &self.layer_layout,
451 layer_prefix_offsets: &self.layer_prefix_offsets,
452 state_segments: &self.state_segments,
453 topology: &self.topology,
454 }
455 .validate("loaded model")
456 }
457
458 pub fn state_segment(&self, id: &str) -> Result<&PromptCacheStateSegment, PromptCacheError> {
460 self.validate()?;
461 self.state_segments
462 .iter()
463 .find(|segment| segment.id() == id)
464 .ok_or_else(|| {
465 PromptCacheError::Incompatible(format!(
466 "loaded model has no prompt-cache state segment {id:?}"
467 ))
468 })
469 }
470
471 pub fn select_state_segment(&self, id: &str) -> Result<Self, PromptCacheError> {
473 let layers = self.state_segment(id)?.layers();
474 let length = layers.len();
475 let global_layer_start = self
476 .global_layer_start
477 .checked_add(layers.start)
478 .ok_or_else(|| PromptCacheError::Malformed("state segment range overflowed".into()))?;
479 let global_layer_end = global_layer_start
480 .checked_add(length)
481 .ok_or_else(|| PromptCacheError::Malformed("state segment range overflowed".into()))?;
482 let layer_layout = LayerSchedule::new(
483 length,
484 self.layer_layout
485 .iter()
486 .skip(layers.start)
487 .take(length)
488 .cloned()
489 .collect(),
490 )
491 .map_err(|error| PromptCacheError::Malformed(error.to_string()))?;
492 let layer_prefix_offsets = self
493 .layer_prefix_offsets
494 .get(layers.clone())
495 .ok_or_else(|| PromptCacheError::Malformed("state segment range is invalid".into()))?
496 .to_vec();
497 let selected = Self {
498 model_family: self.model_family.clone(),
499 effective_model_type: self.effective_model_type.clone(),
500 architecture_fingerprint: self.architecture_fingerprint.clone(),
501 layer_count: self.layer_count,
502 global_layer_start,
503 global_layer_end,
504 sink_tokens: self.sink_tokens,
505 topology: self.topology.clone(),
506 layer_layout,
507 layer_prefix_offsets,
508 state_segments: vec![PromptCacheStateSegment::new(id, 0..length)?],
509 };
510 selected.validate()?;
511 Ok(selected)
512 }
513}
514
515struct IdentityLayout<'a> {
516 layer_count: usize,
517 global_layer_start: usize,
518 global_layer_end: usize,
519 batch_size: usize,
520 layer_layout: &'a LayerSchedule<LayerCachePolicy>,
521 layer_prefix_offsets: &'a [i32],
522 state_segments: &'a [PromptCacheStateSegment],
523 topology: &'a PromptCacheTopology,
524}
525
526impl IdentityLayout<'_> {
527 fn validate(&self, subject: &str) -> Result<(), PromptCacheError> {
528 let owned = self
529 .global_layer_end
530 .checked_sub(self.global_layer_start)
531 .ok_or_else(|| {
532 PromptCacheError::Incompatible(format!("{subject} has an invalid layer range"))
533 })?;
534 if self.layer_count == 0
535 || self.global_layer_start >= self.global_layer_end
536 || self.global_layer_end > self.layer_count
537 || self.batch_size == 0
538 || self.batch_size > i32::MAX as usize
539 || self.layer_layout.len() != owned
540 || self.layer_prefix_offsets.len() != owned
541 || self.layer_prefix_offsets.iter().any(|offset| *offset > 0)
542 {
543 return Err(PromptCacheError::Incompatible(format!(
544 "{subject} supplied {} cache layouts and {} layer prefix offsets for {owned} owned layers",
545 self.layer_layout.len(),
546 self.layer_prefix_offsets.len()
547 )));
548 }
549 self.topology.validate()?;
550 validate_state_segments(self.state_segments, owned)
551 .map_err(|error| PromptCacheError::Incompatible(format!("{subject} {error}")))?;
552 for policy in self.layer_layout.iter() {
553 policy.validate()?;
554 }
555 Ok(())
556 }
557}
558
559pub fn validate_prompt_cache_model_identity(
561 expected: &PromptCacheDescriptor,
562 model: &PromptCacheModelIdentity,
563) -> Result<(), PromptCacheError> {
564 expected.validate()?;
565 model.validate()?;
566 macro_rules! require_equal {
567 ($field:ident) => {
568 if expected.$field != model.$field {
569 return Err(PromptCacheError::Incompatible(format!(
570 "caller descriptor {} does not match the loaded model",
571 stringify!($field)
572 )));
573 }
574 };
575 }
576 require_equal!(model_family);
577 require_equal!(effective_model_type);
578 require_equal!(architecture_fingerprint);
579 require_equal!(layer_count);
580 require_equal!(global_layer_start);
581 require_equal!(global_layer_end);
582 require_equal!(sink_tokens);
583 require_equal!(topology);
584 require_equal!(layer_layout);
585 require_equal!(layer_prefix_offsets);
586 require_equal!(state_segments);
587 Ok(())
588}
589
590fn validate_state_segments(
591 segments: &[PromptCacheStateSegment],
592 owned: usize,
593) -> Result<(), String> {
594 if segments.is_empty() {
595 return Err("has no named state segments".into());
596 }
597 let mut ids = BTreeSet::new();
598 let mut next = 0;
599 for segment in segments {
600 if segment.id.trim().is_empty() {
601 return Err("has an empty state segment identity".into());
602 }
603 if !ids.insert(segment.id.as_str()) {
604 return Err(format!(
605 "has duplicate state segment identity {:?}",
606 segment.id
607 ));
608 }
609 if segment.layers.start != next
610 || segment.layers.end <= segment.layers.start
611 || segment.layers.end > owned
612 {
613 return Err(format!(
614 "state segment {:?} range {}..{} does not continue an exact partition of {owned} owned layers",
615 segment.id, segment.layers.start, segment.layers.end
616 ));
617 }
618 next = segment.layers.end;
619 }
620 if next != owned {
621 return Err(format!(
622 "state segments cover {next} of {owned} owned layers"
623 ));
624 }
625 Ok(())
626}
627
628#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
630pub struct PromptCacheTopology {
631 stage: Option<(usize, usize)>,
633 shard: Option<(usize, usize)>,
635 addressable: Option<(usize, usize)>,
637 addressable_state_replicated: bool,
639}
640
641impl Default for PromptCacheTopology {
642 fn default() -> Self {
643 Self {
644 stage: None,
645 shard: None,
646 addressable: None,
647 addressable_state_replicated: true,
648 }
649 }
650}
651
652impl PromptCacheTopology {
653 pub fn new(
655 stage: Option<(usize, usize)>,
656 shard: Option<(usize, usize)>,
657 addressable: Option<(usize, usize)>,
658 addressable_state_replicated: bool,
659 ) -> Result<Self, PromptCacheError> {
660 let topology = Self {
661 stage,
662 shard,
663 addressable,
664 addressable_state_replicated,
665 };
666 topology.validate()?;
667 Ok(topology)
668 }
669
670 pub const fn stage(&self) -> Option<(usize, usize)> {
672 self.stage
673 }
674
675 pub const fn shard(&self) -> Option<(usize, usize)> {
677 self.shard
678 }
679
680 pub const fn addressable(&self) -> Option<(usize, usize)> {
682 self.addressable
683 }
684
685 pub const fn addressable_state_replicated(&self) -> bool {
687 self.addressable_state_replicated
688 }
689
690 pub fn validate(&self) -> Result<(), PromptCacheError> {
692 for (name, axis) in [
693 ("stage", self.stage),
694 ("state shard", self.shard),
695 ("addressable group", self.addressable),
696 ] {
697 if axis.is_some_and(|(size, rank)| size == 0 || rank >= size) {
698 return Err(PromptCacheError::Malformed(format!(
699 "invalid {name} topology"
700 )));
701 }
702 }
703 Ok(())
704 }
705
706 pub fn cache_rank_identity(&self) -> Option<CacheRankIdentity> {
708 (self.stage.is_some() || self.shard.is_some() || self.addressable.is_some()).then(|| {
709 CacheRankIdentity::new(
710 self.stage.map(|(_, rank)| rank),
711 self.shard.map(|(_, rank)| rank),
712 self.addressable.map(|(_, rank)| rank),
713 )
714 })
715 }
716}
717
718#[derive(Debug, Clone, Default)]
720pub struct PromptCacheOptions {
721 application_namespace: Option<String>,
723 replace_existing: bool,
725}
726
727impl PromptCacheOptions {
728 pub fn new(
730 application_namespace: Option<String>,
731 replace_existing: bool,
732 ) -> Result<Self, PromptCacheError> {
733 if application_namespace
734 .as_deref()
735 .is_some_and(|namespace| namespace.trim().is_empty())
736 {
737 return Err(PromptCacheError::Malformed(
738 "prompt-cache application namespace must not be empty".into(),
739 ));
740 }
741 Ok(Self {
742 application_namespace,
743 replace_existing,
744 })
745 }
746
747 pub fn application_namespace(&self) -> Option<&str> {
749 self.application_namespace.as_deref()
750 }
751
752 pub const fn replace_existing(&self) -> bool {
754 self.replace_existing
755 }
756}
757
758#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
760pub struct PromptCacheManifest {
761 pub schema_version: u32,
763 pub model_family: String,
765 pub effective_model_type: String,
767 pub checkpoint_fingerprint: String,
769 pub prefix_content_fingerprint: String,
771 pub architecture_fingerprint: String,
773 pub layer_count: usize,
775 pub global_layer_start: usize,
777 pub global_layer_end: usize,
779 pub block_size_tokens: i32,
781 pub batch_size: usize,
783 pub total_prefix_tokens: usize,
785 pub prefix_sha256: String,
787 pub layer_layout: LayerSchedule<LayerCachePolicy>,
789 pub layer_prefix_offsets: Vec<i32>,
791 pub state_segments: Vec<PromptCacheStateSegment>,
793 pub sink_tokens: usize,
795 pub topology: PromptCacheTopology,
797 #[serde(default)]
799 pub distributed_commit: Option<crate::DistributedCommitOutcome>,
800 pub application_namespace: Option<String>,
802 pub blocks: Vec<PromptCacheBlock>,
804 pub state_tensors: Vec<PromptCacheStateTensor>,
806}
807
808#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
810pub struct PromptCacheStateTensor {
811 pub owner: StateTensorOwner,
813 pub role: StateTensorRole,
815 pub shard: String,
817 pub array: String,
819 pub shape: Vec<i32>,
821 pub dtype: String,
823 pub logical_bytes: u64,
825 pub payload_sha256: String,
827}
828
829#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
831pub struct PromptCacheBlock {
832 pub global_layer: usize,
834 pub representation: CacheRepresentation,
836 pub start: i64,
838 pub end: i64,
840 pub rank: Option<CacheRankIdentity>,
842 pub shard: String,
844 pub first_array: String,
846 pub second_array: String,
848 pub first_shape: Vec<i32>,
850 pub second_shape: Vec<i32>,
852 pub first_dtype: String,
854 pub second_dtype: String,
856 pub logical_bytes: u64,
858 pub payload_sha256: String,
860}
861
862impl PromptCacheManifest {
863 pub fn validate(&self) -> Result<(), PromptCacheError> {
865 if self.schema_version != PROMPT_CACHE_SCHEMA_VERSION {
866 return Err(PromptCacheError::UnsupportedSchema(self.schema_version));
867 }
868 let owned = self.global_layer_end.checked_sub(self.global_layer_start);
869 if self.prefix_content_fingerprint.is_empty()
870 || self.block_size_tokens <= 0
871 || self.layer_count == 0
872 || self.global_layer_start >= self.global_layer_end
873 || self.global_layer_end > self.layer_count
874 || owned != Some(self.layer_layout.len())
875 || owned != Some(self.layer_prefix_offsets.len())
876 || self.batch_size == 0
877 || self.batch_size > i32::MAX as usize
878 || self.total_prefix_tokens == 0
879 || !is_sha256_hex(&self.prefix_sha256)
880 {
881 return Err(PromptCacheError::Malformed(
882 "invalid global cache dimensions".into(),
883 ));
884 }
885 self.topology.validate()?;
886 validate_state_segments(&self.state_segments, self.layer_layout.len())
887 .map_err(PromptCacheError::Malformed)?;
888 for (index, offset) in self.layer_prefix_offsets.iter().enumerate() {
889 layer_prefix_tokens(self.total_prefix_tokens, *offset).map_err(|error| {
890 PromptCacheError::Malformed(format!(
891 "invalid prefix frontier for global layer {}: {error}",
892 self.global_layer_start + index
893 ))
894 })?;
895 }
896 for (index, policy) in self.layer_layout.iter().enumerate() {
897 policy.validate().map_err(|error| {
898 PromptCacheError::Malformed(format!(
899 "invalid policy for global layer {}: {error}",
900 self.global_layer_start + index
901 ))
902 })?;
903 }
904 self.validate_blocks()?;
905 self.validate_state_tensors()?;
906 self.validate_coverage()
907 }
908
909 pub fn validate_compatibility(
911 &self,
912 expected: &PromptCacheDescriptor,
913 prefix_token_ids: &[u32],
914 ) -> Result<(), PromptCacheError> {
915 self.validate()?;
916 expected.validate()?;
917 macro_rules! require_equal {
918 ($field:ident) => {
919 if self.$field != expected.$field {
920 return Err(PromptCacheError::Incompatible(format!(
921 "{} mismatch",
922 stringify!($field)
923 )));
924 }
925 };
926 }
927 require_equal!(model_family);
928 require_equal!(effective_model_type);
929 require_equal!(checkpoint_fingerprint);
930 require_equal!(prefix_content_fingerprint);
931 require_equal!(architecture_fingerprint);
932 require_equal!(layer_count);
933 require_equal!(global_layer_start);
934 require_equal!(global_layer_end);
935 require_equal!(batch_size);
936 require_equal!(layer_layout);
937 require_equal!(layer_prefix_offsets);
938 require_equal!(state_segments);
939 require_equal!(sink_tokens);
940 require_equal!(topology);
941 if self.total_prefix_tokens != prefix_token_ids.len()
942 || self.prefix_sha256 != prompt_cache_token_fingerprint(prefix_token_ids)
943 {
944 return Err(PromptCacheError::PrefixIdentityMismatch);
945 }
946 Ok(())
947 }
948
949 fn validate_blocks(&self) -> Result<(), PromptCacheError> {
950 let mut previous = None;
951 for block in &self.blocks {
952 let layer_index = block
953 .global_layer
954 .checked_sub(self.global_layer_start)
955 .filter(|index| *index < self.layer_layout.len())
956 .ok_or_else(|| {
957 PromptCacheError::Malformed(format!(
958 "cache block layer {} is outside the owned range",
959 block.global_layer
960 ))
961 })?;
962 let layer_tokens = layer_prefix_tokens(
963 self.total_prefix_tokens,
964 self.layer_prefix_offsets[layer_index],
965 )?;
966 if block.start < 0
967 || block.end <= block.start
968 || block.end > layer_tokens as i64
969 || block.logical_bytes == 0
970 || block.first_shape.is_empty()
971 || block.second_shape.is_empty()
972 || !is_sha256_hex(&block.payload_sha256)
973 || !safe_relative_path(&block.shard)
974 {
975 return Err(PromptCacheError::Malformed(format!(
976 "invalid block at layer {} range {}..{}",
977 block.global_layer, block.start, block.end
978 )));
979 }
980 let order = (block.global_layer, block.start, block.end);
981 if previous.is_some_and(|value| value >= order) {
982 return Err(PromptCacheError::Malformed(format!(
983 "prompt-cache blocks are reordered or duplicated at layer {} range {}..{}",
984 block.global_layer, block.start, block.end
985 )));
986 }
987 previous = Some(order);
988 let policy = self.layer_layout.get(layer_index).expect("bounded");
989 let (representation, first_shape, second_shape) =
990 block_geometry(policy, self.batch_size, block.end - block.start)?;
991 if block.representation != representation
992 || block.first_shape != first_shape
993 || block.second_shape != second_shape
994 {
995 return Err(PromptCacheError::Malformed(format!(
996 "global layer {} payload geometry does not match its policy: actual {:?}/{:?}/{:?}, expected {:?}/{first_shape:?}/{second_shape:?}",
997 block.global_layer,
998 block.representation,
999 block.first_shape,
1000 block.second_shape,
1001 representation,
1002 )));
1003 }
1004 if block.rank != self.topology.cache_rank_identity() {
1005 return Err(PromptCacheError::Malformed(
1006 "block rank identity does not match the recorded topology".into(),
1007 ));
1008 }
1009 let names = array_names(block.representation);
1010 if block.first_array != names.0
1011 || block.second_array != names.1
1012 || block.first_dtype != block.second_dtype
1013 {
1014 return Err(PromptCacheError::Malformed(
1015 "block array names or dtypes do not match its representation".into(),
1016 ));
1017 }
1018 }
1019 Ok(())
1020 }
1021
1022 fn validate_state_tensors(&self) -> Result<(), PromptCacheError> {
1023 let actual = self
1024 .state_tensors
1025 .iter()
1026 .map(|entry| (entry.owner, entry.role))
1027 .collect::<BTreeSet<_>>();
1028 if actual.len() != self.state_tensors.len() {
1029 return Err(PromptCacheError::Malformed(
1030 "fixed-state tensors contain duplicate owner/role entries".into(),
1031 ));
1032 }
1033 let mut expected = Vec::new();
1034 for (index, layer) in self.layer_layout.iter().enumerate() {
1035 let owner = StateTensorOwner::Layer(self.global_layer_start + index);
1036 let tokens =
1037 layer_prefix_tokens(self.total_prefix_tokens, self.layer_prefix_offsets[index])?;
1038 for policy in layer.fixed_state() {
1039 if (tokens != 0 && policy.is_required_for(tokens))
1042 || actual.contains(&(owner, policy.role))
1043 {
1044 expected.push((owner, policy, tokens));
1045 }
1046 }
1047 }
1048 if self.state_tensors.len() != expected.len() {
1049 return Err(PromptCacheError::Malformed(format!(
1050 "fixed-state tensor count {} does not match layout count {}",
1051 self.state_tensors.len(),
1052 expected.len()
1053 )));
1054 }
1055 for (entry, (owner, policy, tokens)) in self.state_tensors.iter().zip(expected) {
1056 if entry.owner != owner
1057 || entry.role != policy.role
1058 || entry.shape != policy.resolved_shape(self.batch_size, tokens)?
1059 || !policy.accepts_dtype_name(&entry.dtype)
1060 || entry.logical_bytes == 0
1061 || !is_sha256_hex(&entry.payload_sha256)
1062 || entry.array != "state"
1063 || !safe_relative_path(&entry.shard)
1064 {
1065 return Err(PromptCacheError::Malformed(format!(
1066 "fixed-state tensor {:?} for {:?} does not match its policy: shape {:?} and dtype {}, expected shape {:?}",
1067 entry.role,
1068 entry.owner,
1069 entry.shape,
1070 entry.dtype,
1071 policy.resolved_shape(self.batch_size, tokens)?,
1072 )));
1073 }
1074 }
1075 Ok(())
1076 }
1077
1078 fn validate_coverage(&self) -> Result<(), PromptCacheError> {
1079 let mut by_layer: BTreeMap<usize, Vec<&PromptCacheBlock>> = BTreeMap::new();
1080 for block in &self.blocks {
1081 by_layer.entry(block.global_layer).or_default().push(block);
1082 }
1083 for (index, policy) in self.layer_layout.iter().enumerate() {
1084 let layer = self.global_layer_start + index;
1085 let tokens =
1086 layer_prefix_tokens(self.total_prefix_tokens, self.layer_prefix_offsets[index])?;
1087 let mut blocks = by_layer.remove(&layer).unwrap_or_default();
1088 if policy.attention().is_none() {
1089 if !blocks.is_empty() {
1090 return Err(PromptCacheError::Malformed(format!(
1091 "stateless global layer {layer} has unexpected blocks"
1092 )));
1093 }
1094 continue;
1095 }
1096 if blocks.is_empty() {
1097 if tokens == 0 {
1098 continue;
1099 }
1100 return Err(PromptCacheError::Malformed(format!(
1101 "missing blocks for global layer {layer}"
1102 )));
1103 }
1104 blocks.sort_by_key(|block| block.start);
1105 let required = required_persisted_start(policy, tokens)?;
1106 let mut end = blocks[0].start;
1107 if end > required
1108 || (matches!(policy.attention(), Some(AttentionPolicy::Full)) && end != 0)
1109 {
1110 return Err(PromptCacheError::Malformed(format!(
1111 "global layer {layer} starts at {end}, but its policy requires history from {required}"
1112 )));
1113 }
1114 for block in blocks {
1115 if block.start != end {
1116 return Err(PromptCacheError::Malformed(format!(
1117 "gap or overlap at global layer {layer}: expected {end}, found {}",
1118 block.start
1119 )));
1120 }
1121 end = block.end;
1122 }
1123 if end != tokens as i64 {
1124 return Err(PromptCacheError::Malformed(format!(
1125 "global layer {layer} ends at {end}, expected {tokens}"
1126 )));
1127 }
1128 }
1129 Ok(())
1130 }
1131}
1132
1133fn block_geometry(
1134 policy: &LayerCachePolicy,
1135 batch_size: usize,
1136 token_count: i64,
1137) -> Result<(CacheRepresentation, Vec<i32>, Vec<i32>), PromptCacheError> {
1138 let batch = i32::try_from(batch_size)
1139 .map_err(|_| PromptCacheError::Malformed("prompt-cache batch exceeds i32".into()))?;
1140 let tokens = i32::try_from(token_count)
1141 .map_err(|_| PromptCacheError::Malformed("cache block token count exceeds i32".into()))?;
1142 match policy {
1143 LayerCachePolicy::NoState | LayerCachePolicy::FixedState { .. } => Err(
1144 PromptCacheError::Malformed("stateless layer has an attention payload".into()),
1145 ),
1146 LayerCachePolicy::KeyValue {
1147 num_key_value_heads,
1148 head_dim,
1149 ..
1150 }
1151 | LayerCachePolicy::KeyValueWithFixedState {
1152 num_key_value_heads,
1153 head_dim,
1154 ..
1155 } => {
1156 let shape = vec![
1157 batch,
1158 num_key_value_heads.get() as i32,
1159 tokens,
1160 head_dim.get() as i32,
1161 ];
1162 Ok((CacheRepresentation::KeyValue, shape.clone(), shape))
1163 }
1164 LayerCachePolicy::KeyOnly {
1165 num_key_heads,
1166 head_dim,
1167 ..
1168 }
1169 | LayerCachePolicy::KeyOnlyWithFixedState {
1170 num_key_heads,
1171 head_dim,
1172 ..
1173 } => Ok((
1174 CacheRepresentation::KeyValue,
1175 vec![
1176 batch,
1177 num_key_heads.get() as i32,
1178 tokens,
1179 head_dim.get() as i32,
1180 ],
1181 vec![batch, num_key_heads.get() as i32, tokens, 1],
1182 )),
1183 LayerCachePolicy::CompressedLatentRotary {
1184 latent_dim,
1185 rotary_dim,
1186 ..
1187 } => Ok((
1188 CacheRepresentation::CompressedLatentRotary,
1189 vec![batch, tokens, latent_dim.get() as i32],
1190 vec![batch, tokens, rotary_dim.get() as i32],
1191 )),
1192 }
1193}
1194
1195fn required_persisted_start(
1196 policy: &LayerCachePolicy,
1197 total_prefix_tokens: usize,
1198) -> Result<i64, PromptCacheError> {
1199 let total = i64::try_from(total_prefix_tokens).map_err(|_| {
1200 PromptCacheError::Malformed("prompt-cache prefix length exceeds i64".into())
1201 })?;
1202 match policy.attention() {
1203 None | Some(AttentionPolicy::Full) => Ok(0),
1204 Some(AttentionPolicy::Sliding { window }) => {
1205 Ok((total - i64::from(window.get() - 1)).max(0))
1206 }
1207 }
1208}
1209
1210fn layer_prefix_tokens(total: usize, offset: i32) -> Result<usize, PromptCacheError> {
1211 if offset > 0 {
1212 return Err(PromptCacheError::Malformed(
1213 "layer prefix offsets must not advance beyond the persisted prefix".into(),
1214 ));
1215 }
1216 total
1217 .checked_sub(offset.unsigned_abs() as usize)
1218 .ok_or_else(|| {
1219 PromptCacheError::Malformed(format!(
1220 "layer prefix offset {offset} precedes the start of a {total}-token prefix"
1221 ))
1222 })
1223}
1224
1225fn array_names(representation: CacheRepresentation) -> (&'static str, &'static str) {
1226 match representation {
1227 CacheRepresentation::KeyValue => ("keys", "values"),
1228 CacheRepresentation::CompressedLatentRotary => ("latent", "rotary_key"),
1229 }
1230}
1231
1232fn safe_relative_path(value: &str) -> bool {
1233 !value.is_empty()
1234 && !value.starts_with('/')
1235 && value
1236 .split('/')
1237 .all(|part| !part.is_empty() && part != "." && part != "..")
1238 && !value.contains('\\')
1239}
1240
1241fn is_sha256_hex(value: &str) -> bool {
1242 value.len() == 64
1243 && value
1244 .bytes()
1245 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
1246}
1247
1248pub fn derive_prompt_cache_architecture_fingerprint<I, K, V>(
1250 model_family: &str,
1251 fields: I,
1252) -> String
1253where
1254 I: IntoIterator<Item = (K, V)>,
1255 K: Into<String>,
1256 V: Into<String>,
1257{
1258 let mut fields = fields
1259 .into_iter()
1260 .map(|(key, value)| (key.into(), value.into()))
1261 .collect::<Vec<_>>();
1262 fields.sort_unstable();
1263 let mut hasher = Sha256::new();
1264 hash_component(&mut hasher, b"eredu-prompt-cache-architecture-v1");
1265 hash_component(&mut hasher, model_family.as_bytes());
1266 for (key, value) in fields {
1267 hash_component(&mut hasher, key.as_bytes());
1268 hash_component(&mut hasher, value.as_bytes());
1269 }
1270 format!("sha256:{}", hex(hasher.finalize()))
1271}
1272
1273pub fn prompt_cache_token_fingerprint(tokens: &[u32]) -> String {
1275 let mut hasher = Sha256::new();
1276 for token in tokens {
1277 hasher.update(token.to_le_bytes());
1278 }
1279 hex(hasher.finalize())
1280}
1281
1282fn hash_component(hasher: &mut Sha256, value: &[u8]) {
1283 hasher.update((value.len() as u64).to_le_bytes());
1284 hasher.update(value);
1285}
1286
1287fn hex(digest: impl AsRef<[u8]>) -> String {
1288 const HEX: &[u8; 16] = b"0123456789abcdef";
1289 let mut encoded = String::with_capacity(digest.as_ref().len() * 2);
1290 for &byte in digest.as_ref() {
1291 encoded.push(HEX[usize::from(byte >> 4)] as char);
1292 encoded.push(HEX[usize::from(byte & 0x0f)] as char);
1293 }
1294 encoded
1295}
1296
1297#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1299pub enum PromptCacheError {
1300 #[error(transparent)]
1302 Policy(#[from] CachePolicyError),
1303 #[error("unsupported prompt cache schema version {0}")]
1305 UnsupportedSchema(u32),
1306 #[error("malformed prompt cache manifest: {0}")]
1308 Malformed(String),
1309 #[error("incompatible prompt cache: {0}")]
1311 Incompatible(String),
1312 #[error("prompt cache prefix token identity does not match")]
1314 PrefixIdentityMismatch,
1315}
1316
1317#[cfg(test)]
1318mod tests {
1319 use super::*;
1320
1321 fn manifest() -> PromptCacheManifest {
1322 let layout = LayerSchedule::new(
1323 1,
1324 vec![LayerCachePolicy::key_value(AttentionPolicy::Full, 2, 4).unwrap()],
1325 )
1326 .unwrap();
1327 PromptCacheManifest {
1328 schema_version: PROMPT_CACHE_SCHEMA_VERSION,
1329 model_family: "llama".into(),
1330 effective_model_type: "llama".into(),
1331 checkpoint_fingerprint: "checkpoint".into(),
1332 prefix_content_fingerprint: "content".into(),
1333 architecture_fingerprint: "architecture".into(),
1334 layer_count: 1,
1335 global_layer_start: 0,
1336 global_layer_end: 1,
1337 block_size_tokens: 2,
1338 batch_size: 1,
1339 total_prefix_tokens: 2,
1340 prefix_sha256: prompt_cache_token_fingerprint(&[7, 8]),
1341 layer_layout: layout,
1342 layer_prefix_offsets: vec![0],
1343 state_segments: vec![PromptCacheStateSegment::new("state", 0..1).unwrap()],
1344 sink_tokens: 0,
1345 topology: PromptCacheTopology::default(),
1346 distributed_commit: None,
1347 application_namespace: None,
1348 blocks: vec![PromptCacheBlock {
1349 global_layer: 0,
1350 representation: CacheRepresentation::KeyValue,
1351 start: 0,
1352 end: 2,
1353 rank: None,
1354 shard: "blocks/layer-0.safetensors".into(),
1355 first_array: "keys".into(),
1356 second_array: "values".into(),
1357 first_shape: vec![1, 2, 2, 4],
1358 second_shape: vec![1, 2, 2, 4],
1359 first_dtype: "Float16".into(),
1360 second_dtype: "Float16".into(),
1361 logical_bytes: 64,
1362 payload_sha256: "0".repeat(64),
1363 }],
1364 state_tensors: vec![],
1365 }
1366 }
1367
1368 #[test]
1369 fn manifest_round_trips_and_validates_without_a_backend() {
1370 let manifest = manifest();
1371 manifest.validate().unwrap();
1372 let json = serde_json::to_string(&manifest).unwrap();
1373 let restored: PromptCacheManifest = serde_json::from_str(&json).unwrap();
1374 restored.validate().unwrap();
1375 assert_eq!(restored, manifest);
1376 }
1377
1378 #[test]
1379 fn descriptor_derives_every_model_owned_field_from_identity() {
1380 let manifest = manifest();
1381 let identity = PromptCacheModelIdentity {
1382 model_family: manifest.model_family.clone(),
1383 effective_model_type: manifest.effective_model_type.clone(),
1384 architecture_fingerprint: manifest.architecture_fingerprint.clone(),
1385 layer_count: manifest.layer_count,
1386 global_layer_start: manifest.global_layer_start,
1387 global_layer_end: manifest.global_layer_end,
1388 sink_tokens: manifest.sink_tokens,
1389 topology: manifest.topology.clone(),
1390 layer_layout: manifest.layer_layout.clone(),
1391 layer_prefix_offsets: manifest.layer_prefix_offsets.clone(),
1392 state_segments: manifest.state_segments.clone(),
1393 };
1394
1395 let descriptor = PromptCacheDescriptor::from_model_identity(
1396 identity.clone(),
1397 "caller-checkpoint",
1398 "caller-prefix-content",
1399 3,
1400 )
1401 .unwrap();
1402
1403 validate_prompt_cache_model_identity(&descriptor, &identity).unwrap();
1404 assert_eq!(descriptor.checkpoint_fingerprint, "caller-checkpoint");
1405 assert_eq!(
1406 descriptor.prefix_content_fingerprint,
1407 "caller-prefix-content"
1408 );
1409 assert_eq!(descriptor.batch_size, 3);
1410 assert!(
1411 PromptCacheDescriptor::from_model_identity(identity, "checkpoint", "prefix", 0)
1412 .is_err()
1413 );
1414 }
1415
1416 #[test]
1417 fn architecture_fingerprint_uses_the_eredu_domain() {
1418 let fingerprint = derive_prompt_cache_architecture_fingerprint(
1419 "llama",
1420 [("layers", "32"), ("hidden_size", "4096")],
1421 );
1422 assert_eq!(
1423 fingerprint,
1424 "sha256:9ee0b30ea8687d04eb4b65db3a58ccfff0a72bdd502805e9fdd6edb223ca5949"
1425 );
1426 }
1427
1428 #[test]
1429 fn zero_frontier_prediction_state_needs_no_materialized_tensor() {
1430 let recurrent = crate::cache::StateTensorPolicy::new(
1431 StateTensorRole::Recurrent,
1432 vec![crate::cache::StateTensorDimension::Batch],
1433 crate::cache::StateTensorDtype::Floating,
1434 crate::cache::MutableStateResidency::LayerScopedOffloadable,
1435 )
1436 .unwrap();
1437 let mut value = manifest();
1438 value.total_prefix_tokens = 1;
1439 value.prefix_sha256 = prompt_cache_token_fingerprint(&[7]);
1440 value.layer_prefix_offsets = vec![-1];
1441 value.layer_layout = LayerSchedule::new(
1442 1,
1443 vec![LayerCachePolicy::fixed_only(vec![recurrent]).unwrap()],
1444 )
1445 .unwrap();
1446 value.blocks.clear();
1447 value.state_tensors.clear();
1448 value.validate().unwrap();
1449 }
1450
1451 #[test]
1452 fn rejects_bad_topology_geometry_coverage_and_paths() {
1453 let mut value = manifest();
1454 value.topology.shard = Some((1, 1));
1455 assert!(value.validate().is_err());
1456 let mut value = manifest();
1457 value.blocks[0].first_shape[2] = 1;
1458 assert!(value.validate().is_err());
1459 let mut value = manifest();
1460 value.blocks[0].shard = "../escape".into();
1461 assert!(value.validate().is_err());
1462 }
1463
1464 #[test]
1465 fn identity_and_prefix_compatibility_fail_closed() {
1466 let manifest = manifest();
1467 let descriptor = PromptCacheDescriptor {
1468 model_family: manifest.model_family.clone(),
1469 effective_model_type: manifest.effective_model_type.clone(),
1470 checkpoint_fingerprint: manifest.checkpoint_fingerprint.clone(),
1471 prefix_content_fingerprint: manifest.prefix_content_fingerprint.clone(),
1472 architecture_fingerprint: manifest.architecture_fingerprint.clone(),
1473 layer_count: 1,
1474 global_layer_start: 0,
1475 global_layer_end: 1,
1476 batch_size: 1,
1477 layer_layout: manifest.layer_layout.clone(),
1478 layer_prefix_offsets: vec![0],
1479 state_segments: manifest.state_segments.clone(),
1480 sink_tokens: 0,
1481 topology: PromptCacheTopology::default(),
1482 distributed_commit: None,
1483 };
1484 manifest
1485 .validate_compatibility(&descriptor, &[7, 8])
1486 .unwrap();
1487 assert!(manifest
1488 .validate_compatibility(&descriptor, &[8, 7])
1489 .is_err());
1490 let mut renamed = descriptor.clone();
1491 renamed.state_segments = vec![PromptCacheStateSegment::new("renamed", 0..1).unwrap()];
1492 assert!(matches!(
1493 manifest.validate_compatibility(&renamed, &[7, 8]),
1494 Err(PromptCacheError::Incompatible(_))
1495 ));
1496 let mut invalid = descriptor;
1497 invalid.layer_prefix_offsets[0] = 1;
1498 assert!(matches!(
1499 invalid.validate(),
1500 Err(PromptCacheError::Incompatible(_))
1501 ));
1502
1503 let mut malformed = manifest.clone();
1504 malformed.state_segments = vec![PromptCacheStateSegment::new("state", 0..2).unwrap()];
1505 assert!(matches!(
1506 malformed.validate(),
1507 Err(PromptCacheError::Malformed(_))
1508 ));
1509 }
1510}