Skip to main content

eredu_core/cache/
prompt.rs

1//! Portable reusable prompt-cache identity, catalog, and validation.
2
3use 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
18/// Current reusable prompt-cache schema version.
19pub const PROMPT_CACHE_SCHEMA_VERSION: u32 = 8;
20
21/// One named contiguous state range in a portable prompt-cache identity.
22#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
23pub struct PromptCacheStateSegment {
24    id: String,
25    layers: Range<usize>,
26}
27
28impl PromptCacheStateSegment {
29    /// Creates a named non-empty local state range.
30    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    /// Returns the architecture-declared stable segment identity.
46    pub fn id(&self) -> &str {
47        &self.id
48    }
49
50    /// Returns the segment's local range in the identity's ordered layout.
51    pub fn layers(&self) -> Range<usize> {
52        self.layers.clone()
53    }
54}
55
56/// Caller-supplied identity and geometry for a reusable prefix cache.
57#[derive(Debug, Clone, Eq, Hash, PartialEq)]
58pub struct PromptCacheDescriptor {
59    /// Stable architecture family.
60    model_family: String,
61    /// Effective normalized model type.
62    effective_model_type: String,
63    /// Caller-verified checkpoint identity.
64    checkpoint_fingerprint: String,
65    /// Identity of all content that produced the cached activations.
66    prefix_content_fingerprint: String,
67    /// Cache-relevant architecture identity.
68    architecture_fingerprint: String,
69    /// Total model layer count.
70    layer_count: usize,
71    /// Inclusive first global layer stored by this rank.
72    global_layer_start: usize,
73    /// Exclusive global layer boundary stored by this rank.
74    global_layer_end: usize,
75    /// Prefix batch size.
76    batch_size: usize,
77    /// Ordered cache layout for the owned layer range.
78    layer_layout: LayerSchedule<LayerCachePolicy>,
79    /// Per-layer processed-token delta relative to the persisted prefix.
80    layer_prefix_offsets: Vec<i32>,
81    /// Architecture-declared named ranges in the ordered state layout.
82    state_segments: Vec<PromptCacheStateSegment>,
83    /// Attention sink or pinned-prefix token count.
84    sink_tokens: usize,
85    /// Distributed rank-local layout.
86    topology: PromptCacheTopology,
87    /// Last distributed commit observation associated with the persisted state.
88    distributed_commit: Option<crate::DistributedCommitOutcome>,
89}
90
91impl PromptCacheDescriptor {
92    /// Creates and validates a complete reusable prefix-cache descriptor.
93    #[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    /// Stable architecture family.
145    pub fn model_family(&self) -> &str {
146        &self.model_family
147    }
148    /// Effective normalized model type.
149    pub fn effective_model_type(&self) -> &str {
150        &self.effective_model_type
151    }
152    /// Caller-verified checkpoint identity.
153    pub fn checkpoint_fingerprint(&self) -> &str {
154        &self.checkpoint_fingerprint
155    }
156    /// Prefix-content identity.
157    pub fn prefix_content_fingerprint(&self) -> &str {
158        &self.prefix_content_fingerprint
159    }
160    /// Cache-relevant architecture identity.
161    pub fn architecture_fingerprint(&self) -> &str {
162        &self.architecture_fingerprint
163    }
164    /// Total model layer count.
165    pub const fn layer_count(&self) -> usize {
166        self.layer_count
167    }
168    /// Inclusive first global layer stored by this rank.
169    pub const fn global_layer_start(&self) -> usize {
170        self.global_layer_start
171    }
172    /// Exclusive global layer boundary stored by this rank.
173    pub const fn global_layer_end(&self) -> usize {
174        self.global_layer_end
175    }
176    /// Prefix batch size.
177    pub const fn batch_size(&self) -> usize {
178        self.batch_size
179    }
180    /// Ordered cache layout.
181    pub const fn layer_layout(&self) -> &LayerSchedule<LayerCachePolicy> {
182        &self.layer_layout
183    }
184    /// Per-layer processed-token deltas.
185    pub fn layer_prefix_offsets(&self) -> &[i32] {
186        &self.layer_prefix_offsets
187    }
188    /// Named state-layout ranges.
189    pub fn state_segments(&self) -> &[PromptCacheStateSegment] {
190        &self.state_segments
191    }
192    /// Attention sink token count.
193    pub const fn sink_tokens(&self) -> usize {
194        self.sink_tokens
195    }
196    /// Distributed rank-local layout.
197    pub const fn topology(&self) -> &PromptCacheTopology {
198        &self.topology
199    }
200    /// Last distributed commit observation attached by the owning session.
201    pub const fn distributed_commit(&self) -> Option<crate::DistributedCommitOutcome> {
202        self.distributed_commit
203    }
204    /// Attaches the owning session's exact distributed commit observation.
205    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    /// Replaces the distributed topology and revalidates the descriptor.
213    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    /// Replaces the cache-relevant architecture fingerprint.
222    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    /// Replaces the total model layer count while preserving the owned range.
236    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    /// Derives every model-owned field from a prepared model identity.
242    ///
243    /// The checkpoint and prefix-content fingerprints remain caller-owned
244    /// because they identify the concrete weights and processed input rather
245    /// than model structure.
246    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    /// Validates the complete portable identity and cache geometry.
274    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/// Cache-relevant structure derived from a prepared model.
290#[derive(Debug, Clone, Eq, Hash, PartialEq)]
291pub struct PromptCacheModelIdentity {
292    /// Stable architecture family.
293    model_family: String,
294    /// Effective normalized model type.
295    effective_model_type: String,
296    /// Cache-relevant architecture identity.
297    architecture_fingerprint: String,
298    /// Total model layer count.
299    layer_count: usize,
300    /// Inclusive first global layer owned by this model instance.
301    global_layer_start: usize,
302    /// Exclusive global layer boundary owned by this model instance.
303    global_layer_end: usize,
304    /// Attention sink or pinned-prefix token count.
305    sink_tokens: usize,
306    /// Distributed rank-local layout.
307    topology: PromptCacheTopology,
308    /// Ordered cache layout for the owned layer range.
309    layer_layout: LayerSchedule<LayerCachePolicy>,
310    /// Per-layer processed-token delta relative to the persisted prefix.
311    layer_prefix_offsets: Vec<i32>,
312    /// Architecture-declared named ranges in the ordered state layout.
313    state_segments: Vec<PromptCacheStateSegment>,
314}
315
316impl PromptCacheModelIdentity {
317    /// Creates and validates cache-relevant prepared-model identity.
318    #[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    /// Stable architecture family.
361    pub fn model_family(&self) -> &str {
362        &self.model_family
363    }
364    /// Effective normalized model type.
365    pub fn effective_model_type(&self) -> &str {
366        &self.effective_model_type
367    }
368    /// Cache-relevant architecture identity.
369    pub fn architecture_fingerprint(&self) -> &str {
370        &self.architecture_fingerprint
371    }
372    /// Total model layer count.
373    pub const fn layer_count(&self) -> usize {
374        self.layer_count
375    }
376    /// Inclusive first global layer owned locally.
377    pub const fn global_layer_start(&self) -> usize {
378        self.global_layer_start
379    }
380    /// Exclusive global layer boundary owned locally.
381    pub const fn global_layer_end(&self) -> usize {
382        self.global_layer_end
383    }
384    /// Attention sink token count.
385    pub const fn sink_tokens(&self) -> usize {
386        self.sink_tokens
387    }
388    /// Distributed rank-local layout.
389    pub const fn topology(&self) -> &PromptCacheTopology {
390        &self.topology
391    }
392    /// Ordered cache layout.
393    pub const fn layer_layout(&self) -> &LayerSchedule<LayerCachePolicy> {
394        &self.layer_layout
395    }
396    /// Per-layer processed-token deltas.
397    pub fn layer_prefix_offsets(&self) -> &[i32] {
398        &self.layer_prefix_offsets
399    }
400    /// Named state-layout ranges.
401    pub fn state_segments(&self) -> &[PromptCacheStateSegment] {
402        &self.state_segments
403    }
404    /// Builds an ordered ordinary key/value layout from runtime window values.
405    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    /// Builds a uniform compressed-latent layout.
424    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    /// Validates the owned layer range and every policy.
444    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    /// Returns one architecture-declared state segment by stable identity.
459    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    /// Selects one named state segment as a validated standalone identity.
472    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
559/// Verifies that a caller descriptor was derived from the prepared model.
560pub 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/// Rank-local topology recorded in a prompt-cache manifest.
629#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
630pub struct PromptCacheTopology {
631    /// Ordered-stage partition size and rank.
632    stage: Option<(usize, usize)>,
633    /// State-shard partition size and rank.
634    shard: Option<(usize, usize)>,
635    /// Addressable-group size and rank.
636    addressable: Option<(usize, usize)>,
637    /// Whether cache state is replicated on the addressable axis.
638    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    /// Creates and validates an exact cache-placement topology.
654    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    /// Returns ordered-stage size and rank when partitioned.
671    pub const fn stage(&self) -> Option<(usize, usize)> {
672        self.stage
673    }
674
675    /// Returns state-shard size and rank when partitioned.
676    pub const fn shard(&self) -> Option<(usize, usize)> {
677        self.shard
678    }
679
680    /// Returns addressable-group size and rank when partitioned.
681    pub const fn addressable(&self) -> Option<(usize, usize)> {
682        self.addressable
683    }
684
685    /// Returns whether cache state is replicated on the addressable axis.
686    pub const fn addressable_state_replicated(&self) -> bool {
687        self.addressable_state_replicated
688    }
689
690    /// Validates every optional world-size/rank pair.
691    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    /// Returns the rank identity stored on cache blocks, if distributed.
707    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/// Explicit publication behavior for a reusable prefix cache.
719#[derive(Debug, Clone, Default)]
720pub struct PromptCacheOptions {
721    /// Optional application grouping label; never used for compatibility.
722    application_namespace: Option<String>,
723    /// Allows atomically replacing an existing destination.
724    replace_existing: bool,
725}
726
727impl PromptCacheOptions {
728    /// Creates validated prompt-cache publication options.
729    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    /// Returns the optional application grouping label.
748    pub fn application_namespace(&self) -> Option<&str> {
749        self.application_namespace.as_deref()
750    }
751
752    /// Returns whether an existing destination may be replaced atomically.
753    pub const fn replace_existing(&self) -> bool {
754        self.replace_existing
755    }
756}
757
758/// Versioned metadata inspectable without loading backend arrays.
759#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
760pub struct PromptCacheManifest {
761    /// Persistence schema version.
762    pub schema_version: u32,
763    /// Model architecture family.
764    pub model_family: String,
765    /// Effective normalized model type.
766    pub effective_model_type: String,
767    /// Caller-selected checkpoint identity.
768    pub checkpoint_fingerprint: String,
769    /// Identity of all content that produced this prefix.
770    pub prefix_content_fingerprint: String,
771    /// Cache-relevant architecture identity.
772    pub architecture_fingerprint: String,
773    /// Total model layer count.
774    pub layer_count: usize,
775    /// Inclusive first global layer represented locally.
776    pub global_layer_start: usize,
777    /// Exclusive global layer boundary represented locally.
778    pub global_layer_end: usize,
779    /// Block size used by the producer.
780    pub block_size_tokens: i32,
781    /// Prefix batch size.
782    pub batch_size: usize,
783    /// Exact prefix token count.
784    pub total_prefix_tokens: usize,
785    /// SHA-256 over little-endian prefix token IDs.
786    pub prefix_sha256: String,
787    /// Ordered cache layout for the owned layer range.
788    pub layer_layout: LayerSchedule<LayerCachePolicy>,
789    /// Per-layer processed-token delta relative to the prefix.
790    pub layer_prefix_offsets: Vec<i32>,
791    /// Architecture-declared named ranges in the ordered state layout.
792    pub state_segments: Vec<PromptCacheStateSegment>,
793    /// Pinned prefix or sink token count.
794    pub sink_tokens: usize,
795    /// Distributed rank-local representation.
796    pub topology: PromptCacheTopology,
797    /// Durable rank-local observation of the distributed commit epoch.
798    #[serde(default)]
799    pub distributed_commit: Option<crate::DistributedCommitOutcome>,
800    /// Optional non-authoritative application grouping label.
801    pub application_namespace: Option<String>,
802    /// Ordered immutable cache blocks.
803    pub blocks: Vec<PromptCacheBlock>,
804    /// Ordered fixed-size state tensors.
805    pub state_tensors: Vec<PromptCacheStateTensor>,
806}
807
808/// One independently validated fixed-size state tensor catalog entry.
809#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
810pub struct PromptCacheStateTensor {
811    /// Layer owner.
812    pub owner: StateTensorOwner,
813    /// Semantic role declared by the canonical layout.
814    pub role: StateTensorRole,
815    /// Safe relative backend shard path.
816    pub shard: String,
817    /// Array name within the shard.
818    pub array: String,
819    /// Exact stored shape.
820    pub shape: Vec<i32>,
821    /// Exact stored dtype.
822    pub dtype: String,
823    /// Logical bytes in the array.
824    pub logical_bytes: u64,
825    /// SHA-256 of the exact payload bytes.
826    pub payload_sha256: String,
827}
828
829/// One cache block catalog entry in a prompt-cache manifest.
830#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
831pub struct PromptCacheBlock {
832    /// Architecture-global layer identity.
833    pub global_layer: usize,
834    /// Stored attention representation.
835    pub representation: CacheRepresentation,
836    /// Inclusive absolute token position.
837    pub start: i64,
838    /// Exclusive absolute token position.
839    pub end: i64,
840    /// Optional rank identity.
841    pub rank: Option<CacheRankIdentity>,
842    /// Safe relative backend shard path.
843    pub shard: String,
844    /// First array name.
845    pub first_array: String,
846    /// Second array name.
847    pub second_array: String,
848    /// First array shape.
849    pub first_shape: Vec<i32>,
850    /// Second array shape.
851    pub second_shape: Vec<i32>,
852    /// First array dtype.
853    pub first_dtype: String,
854    /// Second array dtype.
855    pub second_dtype: String,
856    /// Logical bytes in both arrays.
857    pub logical_bytes: u64,
858    /// SHA-256 of the exact payload bytes.
859    pub payload_sha256: String,
860}
861
862impl PromptCacheManifest {
863    /// Validates all backend-independent schema, geometry, and coverage rules.
864    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    /// Validates compatibility with a caller descriptor and exact prefix IDs.
910    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                // A zero-token frontier has no materialized recurrent value,
1040                // even when that value is required once execution begins.
1041                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
1248/// Derives a stable cache architecture fingerprint from ordered semantic fields.
1249pub 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
1273/// Hashes exact prefix token IDs as little-endian `u32` values.
1274pub 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/// Invalid reusable prompt-cache identity, schema, or catalog.
1298#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1299pub enum PromptCacheError {
1300    /// A layer or state policy is invalid.
1301    #[error(transparent)]
1302    Policy(#[from] CachePolicyError),
1303    /// The persistence schema version is unsupported.
1304    #[error("unsupported prompt cache schema version {0}")]
1305    UnsupportedSchema(u32),
1306    /// The portable manifest structure is malformed.
1307    #[error("malformed prompt cache manifest: {0}")]
1308    Malformed(String),
1309    /// The prepared model or caller identity differs from the producer.
1310    #[error("incompatible prompt cache: {0}")]
1311    Incompatible(String),
1312    /// Exact prefix IDs differ from the persisted identity.
1313    #[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}