1use std::{collections::BTreeSet, num::NonZeroU32};
4
5use serde::{Deserialize, Serialize};
6
7use crate::attention::AttentionPolicy;
8
9#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum CacheRepresentation {
13 KeyValue,
15 CompressedLatentRotary,
17}
18
19#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
21pub struct CacheRankIdentity {
22 stage_rank: Option<usize>,
24 shard_rank: Option<usize>,
26 addressable_rank: Option<usize>,
28}
29
30impl CacheRankIdentity {
31 pub const fn new(
33 stage_rank: Option<usize>,
34 shard_rank: Option<usize>,
35 addressable_rank: Option<usize>,
36 ) -> Self {
37 Self {
38 stage_rank,
39 shard_rank,
40 addressable_rank,
41 }
42 }
43
44 pub const fn stage_rank(&self) -> Option<usize> {
46 self.stage_rank
47 }
48
49 pub const fn shard_rank(&self) -> Option<usize> {
51 self.shard_rank
52 }
53
54 pub const fn addressable_rank(&self) -> Option<usize> {
56 self.addressable_rank
57 }
58}
59
60#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
62pub struct CacheBlockId {
63 pub session_id: u64,
65 pub global_layer: usize,
67 pub representation: CacheRepresentation,
69 pub start: i64,
71 pub end: i64,
73 pub rank: Option<CacheRankIdentity>,
75}
76
77#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum CacheTier {
81 Device,
83 Host,
85 Disk,
87}
88
89#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum LayerCachePolicy {
93 NoState,
95 KeyValue {
97 attention: AttentionPolicy,
99 num_key_value_heads: NonZeroU32,
101 head_dim: NonZeroU32,
103 },
104 KeyOnly {
106 attention: AttentionPolicy,
108 num_key_heads: NonZeroU32,
110 head_dim: NonZeroU32,
112 },
113 CompressedLatentRotary {
115 attention: AttentionPolicy,
117 latent_dim: NonZeroU32,
119 rotary_dim: NonZeroU32,
121 },
122 FixedState {
124 tensors: Vec<StateTensorPolicy>,
126 },
127 KeyValueWithFixedState {
129 attention: AttentionPolicy,
131 num_key_value_heads: NonZeroU32,
133 head_dim: NonZeroU32,
135 tensors: Vec<StateTensorPolicy>,
137 },
138 KeyOnlyWithFixedState {
140 attention: AttentionPolicy,
142 num_key_heads: NonZeroU32,
144 head_dim: NonZeroU32,
146 tensors: Vec<StateTensorPolicy>,
148 },
149}
150
151#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
153#[serde(rename_all = "snake_case")]
154pub enum StateTensorRole {
155 Convolution {
157 slot: u32,
159 },
160 Recurrent,
162 PrefixEmbedding,
164 PositionDelta,
166 Pooling {
168 stream: u32,
170 component: PoolingStateComponent,
172 },
173}
174
175#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
177#[serde(rename_all = "snake_case")]
178pub enum PoolingStateComponent {
179 PendingValues,
181 PendingGates,
183 Pooled,
185 OverlapValues,
187 OverlapGates,
189}
190
191#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194pub enum StateResidencyClass {
195 AlwaysDeviceMutable,
197 SealablePaged,
199 LayerScopedOffloadable,
201}
202
203#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
205#[serde(rename_all = "snake_case")]
206pub enum MutableStateResidency {
207 AlwaysDeviceMutable,
209 LayerScopedOffloadable,
211}
212
213impl From<MutableStateResidency> for StateResidencyClass {
214 fn from(value: MutableStateResidency) -> Self {
215 match value {
216 MutableStateResidency::AlwaysDeviceMutable => Self::AlwaysDeviceMutable,
217 MutableStateResidency::LayerScopedOffloadable => Self::LayerScopedOffloadable,
218 }
219 }
220}
221
222#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum StateTensorDimension {
226 Batch,
228 PrefixTokens,
230 PrefixTokensDiv(NonZeroU32),
232 PrefixTokensRem(NonZeroU32),
234 Fixed(NonZeroU32),
236 Scalar,
238}
239
240#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
242#[serde(rename_all = "snake_case")]
243pub enum StateTensorPresence {
244 Required,
246 Optional,
248 PrefixRemainderNonZero(NonZeroU32),
250 PrefixAtLeast(NonZeroU32),
252}
253
254#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257pub enum StateTensorDtype {
258 Floating,
260 Float32,
262 Int32,
264 Uint32,
266}
267
268#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
270pub struct StateTensorPolicy {
271 pub role: StateTensorRole,
273 pub shape: Vec<StateTensorDimension>,
275 pub dtype: StateTensorDtype,
277 pub residency: StateResidencyClass,
279 pub presence: StateTensorPresence,
281}
282
283#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
285#[serde(rename_all = "snake_case")]
286pub enum StateTensorOwner {
287 Layer(usize),
289}
290
291#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
293#[serde(rename_all = "snake_case")]
294pub enum StateComponentRole {
295 AttentionKeys,
297 AttentionValues,
299 CompressedLatent,
301 RotaryKeys,
303 Fixed(StateTensorRole),
305}
306
307impl StateComponentRole {
308 pub fn stable_name(self) -> String {
310 match self {
311 Self::AttentionKeys => "attention.keys".into(),
312 Self::AttentionValues => "attention.values".into(),
313 Self::CompressedLatent => "attention.compressed_latent".into(),
314 Self::RotaryKeys => "attention.rotary_keys".into(),
315 Self::Fixed(StateTensorRole::Convolution { slot }) => {
316 format!("state.convolution.{slot}")
317 }
318 Self::Fixed(StateTensorRole::Recurrent) => "state.recurrent".into(),
319 Self::Fixed(StateTensorRole::PrefixEmbedding) => "state.prefix_embedding".into(),
320 Self::Fixed(StateTensorRole::PositionDelta) => "state.position_delta".into(),
321 Self::Fixed(StateTensorRole::Pooling { stream, component }) => {
322 let component = match component {
323 PoolingStateComponent::PendingValues => "pending_values",
324 PoolingStateComponent::PendingGates => "pending_gates",
325 PoolingStateComponent::Pooled => "pooled",
326 PoolingStateComponent::OverlapValues => "overlap_values",
327 PoolingStateComponent::OverlapGates => "overlap_gates",
328 };
329 format!("state.pooling.{stream}.{component}")
330 }
331 }
332 }
333}
334
335#[derive(Debug, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
337pub struct StateComponentPolicy {
338 role: StateComponentRole,
340 shape: Vec<StateTensorDimension>,
342 dtype: StateTensorDtype,
344 residency: StateResidencyClass,
346 presence: StateTensorPresence,
348}
349
350impl StateComponentPolicy {
351 pub const fn role(&self) -> StateComponentRole {
353 self.role
354 }
355
356 pub fn shape(&self) -> &[StateTensorDimension] {
358 &self.shape
359 }
360
361 pub const fn dtype(&self) -> StateTensorDtype {
363 self.dtype
364 }
365
366 pub const fn residency(&self) -> StateResidencyClass {
368 self.residency
369 }
370
371 pub const fn presence(&self) -> StateTensorPresence {
373 self.presence
374 }
375}
376
377impl LayerCachePolicy {
378 pub const fn attention_residency_class(&self) -> Option<StateResidencyClass> {
380 match self {
381 Self::NoState | Self::FixedState { .. } => None,
382 Self::KeyValue { .. }
383 | Self::KeyOnly { .. }
384 | Self::CompressedLatentRotary { .. }
385 | Self::KeyValueWithFixedState { .. }
386 | Self::KeyOnlyWithFixedState { .. } => Some(StateResidencyClass::SealablePaged),
387 }
388 }
389
390 pub fn key_value(
392 attention: AttentionPolicy,
393 num_key_value_heads: i32,
394 head_dim: i32,
395 ) -> Result<Self, CachePolicyError> {
396 let policy = Self::KeyValue {
397 attention,
398 num_key_value_heads: positive_u32(num_key_value_heads, "key/value head count")?,
399 head_dim: positive_u32(head_dim, "key/value head dimension")?,
400 };
401 policy.validate()?;
402 Ok(policy)
403 }
404
405 pub fn key_only(
407 attention: AttentionPolicy,
408 num_key_heads: i32,
409 head_dim: i32,
410 ) -> Result<Self, CachePolicyError> {
411 let policy = Self::KeyOnly {
412 attention,
413 num_key_heads: positive_u32(num_key_heads, "key head count")?,
414 head_dim: positive_u32(head_dim, "key head dimension")?,
415 };
416 policy.validate()?;
417 Ok(policy)
418 }
419
420 pub fn compressed_latent_rotary(
422 attention: AttentionPolicy,
423 latent_dim: i32,
424 rotary_dim: i32,
425 ) -> Result<Self, CachePolicyError> {
426 let policy = Self::CompressedLatentRotary {
427 attention,
428 latent_dim: positive_u32(latent_dim, "compressed latent dimension")?,
429 rotary_dim: positive_u32(rotary_dim, "rotary-key dimension")?,
430 };
431 policy.validate()?;
432 Ok(policy)
433 }
434
435 pub fn fixed_only(tensors: Vec<StateTensorPolicy>) -> Result<Self, CachePolicyError> {
437 let policy = Self::FixedState { tensors };
438 policy.validate()?;
439 Ok(policy)
440 }
441
442 pub fn key_value_with_fixed_state(
444 attention: AttentionPolicy,
445 num_key_value_heads: i32,
446 head_dim: i32,
447 tensors: Vec<StateTensorPolicy>,
448 ) -> Result<Self, CachePolicyError> {
449 let policy = Self::KeyValueWithFixedState {
450 attention,
451 num_key_value_heads: positive_u32(num_key_value_heads, "key/value head count")?,
452 head_dim: positive_u32(head_dim, "key/value head dimension")?,
453 tensors,
454 };
455 policy.validate()?;
456 Ok(policy)
457 }
458
459 pub fn key_only_with_fixed_state(
461 attention: AttentionPolicy,
462 num_key_heads: i32,
463 head_dim: i32,
464 tensors: Vec<StateTensorPolicy>,
465 ) -> Result<Self, CachePolicyError> {
466 let policy = Self::KeyOnlyWithFixedState {
467 attention,
468 num_key_heads: positive_u32(num_key_heads, "key head count")?,
469 head_dim: positive_u32(head_dim, "key head dimension")?,
470 tensors,
471 };
472 policy.validate()?;
473 Ok(policy)
474 }
475
476 pub const fn attention(&self) -> Option<AttentionPolicy> {
478 match self {
479 Self::NoState | Self::FixedState { .. } => None,
480 Self::KeyValue { attention, .. }
481 | Self::KeyOnly { attention, .. }
482 | Self::CompressedLatentRotary { attention, .. }
483 | Self::KeyValueWithFixedState { attention, .. }
484 | Self::KeyOnlyWithFixedState { attention, .. } => Some(*attention),
485 }
486 }
487
488 pub fn fixed_state(&self) -> &[StateTensorPolicy] {
490 match self {
491 Self::FixedState { tensors }
492 | Self::KeyValueWithFixedState { tensors, .. }
493 | Self::KeyOnlyWithFixedState { tensors, .. } => tensors,
494 _ => &[],
495 }
496 }
497
498 pub fn components(&self) -> Vec<StateComponentPolicy> {
501 let mut components = Vec::new();
502 let floating = StateTensorDtype::Floating;
503 let required = StateTensorPresence::Required;
504 match self {
505 Self::NoState | Self::FixedState { .. } => {}
506 Self::KeyValue {
507 num_key_value_heads,
508 head_dim,
509 ..
510 }
511 | Self::KeyValueWithFixedState {
512 num_key_value_heads,
513 head_dim,
514 ..
515 } => {
516 let shape = vec![
517 StateTensorDimension::Batch,
518 StateTensorDimension::Fixed(*num_key_value_heads),
519 StateTensorDimension::PrefixTokens,
520 StateTensorDimension::Fixed(*head_dim),
521 ];
522 for role in [
523 StateComponentRole::AttentionKeys,
524 StateComponentRole::AttentionValues,
525 ] {
526 components.push(StateComponentPolicy {
527 role,
528 shape: shape.clone(),
529 dtype: floating,
530 residency: StateResidencyClass::SealablePaged,
531 presence: required,
532 });
533 }
534 }
535 Self::KeyOnly {
536 num_key_heads,
537 head_dim,
538 ..
539 }
540 | Self::KeyOnlyWithFixedState {
541 num_key_heads,
542 head_dim,
543 ..
544 } => components.push(StateComponentPolicy {
545 role: StateComponentRole::AttentionKeys,
546 shape: vec![
547 StateTensorDimension::Batch,
548 StateTensorDimension::Fixed(*num_key_heads),
549 StateTensorDimension::PrefixTokens,
550 StateTensorDimension::Fixed(*head_dim),
551 ],
552 dtype: floating,
553 residency: StateResidencyClass::SealablePaged,
554 presence: required,
555 }),
556 Self::CompressedLatentRotary {
557 latent_dim,
558 rotary_dim,
559 ..
560 } => {
561 for (role, dimension) in [
562 (StateComponentRole::CompressedLatent, *latent_dim),
563 (StateComponentRole::RotaryKeys, *rotary_dim),
564 ] {
565 components.push(StateComponentPolicy {
566 role,
567 shape: vec![
568 StateTensorDimension::Batch,
569 StateTensorDimension::PrefixTokens,
570 StateTensorDimension::Fixed(dimension),
571 ],
572 dtype: floating,
573 residency: StateResidencyClass::SealablePaged,
574 presence: required,
575 });
576 }
577 }
578 }
579 components.extend(
580 self.fixed_state()
581 .iter()
582 .map(|tensor| StateComponentPolicy {
583 role: StateComponentRole::Fixed(tensor.role),
584 shape: tensor.shape.clone(),
585 dtype: tensor.dtype,
586 residency: tensor.residency_class(),
587 presence: tensor.presence,
588 }),
589 );
590 components
591 }
592
593 pub fn validate(&self) -> Result<(), CachePolicyError> {
595 if let Some(attention) = self.attention() {
596 attention
597 .sliding_window_i32()
598 .map_err(|error| CachePolicyError::Invalid(error.to_string()))?;
599 }
600 let validate_dimension = |dimension: NonZeroU32| {
601 (dimension.get() <= i32::MAX as u32)
602 .then_some(())
603 .ok_or_else(|| {
604 CachePolicyError::Invalid(format!(
605 "prompt-cache layer dimension {dimension} exceeds the runtime i32 range"
606 ))
607 })
608 };
609 match self {
610 Self::NoState | Self::FixedState { .. } => {}
611 Self::KeyValue {
612 num_key_value_heads,
613 head_dim,
614 ..
615 }
616 | Self::KeyValueWithFixedState {
617 num_key_value_heads,
618 head_dim,
619 ..
620 } => {
621 validate_dimension(*num_key_value_heads)?;
622 validate_dimension(*head_dim)?;
623 }
624 Self::KeyOnly {
625 num_key_heads,
626 head_dim,
627 ..
628 }
629 | Self::KeyOnlyWithFixedState {
630 num_key_heads,
631 head_dim,
632 ..
633 } => {
634 validate_dimension(*num_key_heads)?;
635 validate_dimension(*head_dim)?;
636 }
637 Self::CompressedLatentRotary {
638 latent_dim,
639 rotary_dim,
640 ..
641 } => {
642 validate_dimension(*latent_dim)?;
643 validate_dimension(*rotary_dim)?;
644 }
645 }
646 let tensors = self.fixed_state();
647 if tensors.is_empty()
648 && matches!(
649 self,
650 Self::FixedState { .. }
651 | Self::KeyValueWithFixedState { .. }
652 | Self::KeyOnlyWithFixedState { .. }
653 )
654 {
655 return Err(CachePolicyError::Invalid(
656 "fixed-state cache policy must contain at least one tensor".into(),
657 ));
658 }
659 validate_state_tensor_policies(tensors)
660 }
661}
662
663impl StateTensorDimension {
664 pub fn fixed(value: i32) -> Result<Self, CachePolicyError> {
666 positive_u32(value, "fixed-state tensor dimension").map(Self::Fixed)
667 }
668}
669
670impl StateTensorPolicy {
671 pub fn new(
673 role: StateTensorRole,
674 shape: Vec<StateTensorDimension>,
675 dtype: StateTensorDtype,
676 residency: MutableStateResidency,
677 ) -> Result<Self, CachePolicyError> {
678 Self::new_with_residency(role, shape, dtype, residency.into())
679 }
680
681 pub fn new_with_residency(
683 role: StateTensorRole,
684 shape: Vec<StateTensorDimension>,
685 dtype: StateTensorDtype,
686 residency: StateResidencyClass,
687 ) -> Result<Self, CachePolicyError> {
688 let policy = Self {
689 role,
690 shape,
691 dtype,
692 residency,
693 presence: StateTensorPresence::Required,
694 };
695 validate_state_tensor_policies(std::slice::from_ref(&policy))?;
696 Ok(policy)
697 }
698
699 pub const fn optional(mut self) -> Self {
701 self.presence = StateTensorPresence::Optional;
702 self
703 }
704
705 pub const fn when_prefix_remainder_nonzero(mut self, divisor: NonZeroU32) -> Self {
707 self.presence = StateTensorPresence::PrefixRemainderNonZero(divisor);
708 self
709 }
710
711 pub const fn when_prefix_at_least(mut self, divisor: NonZeroU32) -> Self {
713 self.presence = StateTensorPresence::PrefixAtLeast(divisor);
714 self
715 }
716
717 pub fn is_required_for(&self, prefix_tokens: usize) -> bool {
719 match self.presence {
720 StateTensorPresence::Required => true,
721 StateTensorPresence::Optional => false,
722 StateTensorPresence::PrefixRemainderNonZero(divisor) => {
723 !prefix_tokens.is_multiple_of(divisor.get() as usize)
724 }
725 StateTensorPresence::PrefixAtLeast(divisor) => prefix_tokens >= divisor.get() as usize,
726 }
727 }
728
729 pub fn residency_class(&self) -> StateResidencyClass {
731 self.residency
732 }
733
734 pub fn resolved_shape(
736 &self,
737 batch_size: usize,
738 prefix_tokens: usize,
739 ) -> Result<Vec<i32>, CachePolicyError> {
740 self.shape
741 .iter()
742 .map(|dimension| match dimension {
743 StateTensorDimension::Batch => i32::try_from(batch_size),
744 StateTensorDimension::PrefixTokens => i32::try_from(prefix_tokens),
745 StateTensorDimension::PrefixTokensDiv(divisor) => {
746 i32::try_from(prefix_tokens / divisor.get() as usize)
747 }
748 StateTensorDimension::PrefixTokensRem(divisor) => {
749 i32::try_from(prefix_tokens % divisor.get() as usize)
750 }
751 StateTensorDimension::Fixed(value) => i32::try_from(value.get()),
752 StateTensorDimension::Scalar => Ok(1),
753 })
754 .collect::<Result<Vec<_>, _>>()
755 .map_err(|_| {
756 CachePolicyError::Invalid(
757 "fixed-state tensor dimension exceeds runtime i32 range".into(),
758 )
759 })
760 }
761
762 pub fn accepts_dtype_name(&self, dtype: &str) -> bool {
764 match self.dtype {
765 StateTensorDtype::Floating => {
766 matches!(dtype, "Float16" | "Bfloat16" | "Float32" | "Float64")
767 }
768 StateTensorDtype::Float32 => dtype == "Float32",
769 StateTensorDtype::Int32 => dtype == "Int32",
770 StateTensorDtype::Uint32 => dtype == "Uint32",
771 }
772 }
773}
774
775fn positive_u32(value: i32, field: &str) -> Result<NonZeroU32, CachePolicyError> {
776 u32::try_from(value)
777 .ok()
778 .and_then(NonZeroU32::new)
779 .ok_or_else(|| {
780 CachePolicyError::Invalid(format!(
781 "prompt-cache {field} must be positive and fit u32, got {value}"
782 ))
783 })
784}
785
786fn validate_state_tensor_policies(tensors: &[StateTensorPolicy]) -> Result<(), CachePolicyError> {
787 let mut roles = BTreeSet::new();
788 for tensor in tensors {
789 if !roles.insert(tensor.role) {
790 return Err(CachePolicyError::Invalid(format!(
791 "duplicate fixed-state tensor role {:?}",
792 tensor.role
793 )));
794 }
795 if tensor.shape.is_empty()
796 || (tensor.shape.contains(&StateTensorDimension::Scalar)
797 && tensor.shape.as_slice() != [StateTensorDimension::Scalar])
798 {
799 return Err(CachePolicyError::Invalid(format!(
800 "invalid fixed-state tensor shape for role {:?}",
801 tensor.role
802 )));
803 }
804 let expected = match tensor.role {
805 StateTensorRole::Recurrent => StateResidencyClass::LayerScopedOffloadable,
806 StateTensorRole::Convolution { .. }
807 | StateTensorRole::PrefixEmbedding
808 | StateTensorRole::PositionDelta => StateResidencyClass::AlwaysDeviceMutable,
809 StateTensorRole::Pooling {
810 component: PoolingStateComponent::Pooled,
811 ..
812 } => StateResidencyClass::SealablePaged,
813 StateTensorRole::Pooling { .. } => StateResidencyClass::AlwaysDeviceMutable,
814 };
815 if tensor.residency != expected {
816 return Err(CachePolicyError::Invalid(format!(
817 "fixed-state tensor role {:?} requires {:?} residency, got {:?}",
818 tensor.role, expected, tensor.residency
819 )));
820 }
821 }
822 Ok(())
823}
824
825#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
827pub enum CachePolicyError {
828 #[error("{0}")]
830 Invalid(String),
831}
832
833#[cfg(test)]
834mod tests {
835 use super::*;
836
837 #[test]
838 fn validates_layer_and_fixed_state_contracts() {
839 let recurrent = StateTensorPolicy::new(
840 StateTensorRole::Recurrent,
841 vec![
842 StateTensorDimension::Batch,
843 StateTensorDimension::fixed(16).unwrap(),
844 ],
845 StateTensorDtype::Floating,
846 MutableStateResidency::LayerScopedOffloadable,
847 )
848 .unwrap();
849 let layer = LayerCachePolicy::key_value_with_fixed_state(
850 AttentionPolicy::sliding(128).unwrap(),
851 8,
852 64,
853 vec![recurrent.clone()],
854 )
855 .unwrap();
856 assert_eq!(
857 layer.attention_residency_class(),
858 Some(StateResidencyClass::SealablePaged)
859 );
860 assert_eq!(recurrent.resolved_shape(2, 9).unwrap(), vec![2, 16]);
861 assert!(recurrent.accepts_dtype_name("Float16"));
862 assert!(!recurrent.accepts_dtype_name("Int32"));
863 }
864
865 #[test]
866 fn rejects_invalid_policy_without_a_backend() {
867 assert!(LayerCachePolicy::key_value(AttentionPolicy::Full, 0, 64).is_err());
868 assert!(StateTensorPolicy::new(
869 StateTensorRole::Recurrent,
870 vec![StateTensorDimension::Scalar, StateTensorDimension::Batch],
871 StateTensorDtype::Floating,
872 MutableStateResidency::LayerScopedOffloadable,
873 )
874 .is_err());
875 }
876
877 #[test]
878 fn policy_schema_round_trips() {
879 let policy = LayerCachePolicy::key_only(AttentionPolicy::Full, 4, 32).unwrap();
880 let json = serde_json::to_string(&policy).unwrap();
881 assert_eq!(
882 serde_json::from_str::<LayerCachePolicy>(&json).unwrap(),
883 policy
884 );
885 }
886}