1use crate::{
4 cache::{
5 LayerCachePolicy, StateTensorDimension, StateTensorDtype, StateTensorPolicy,
6 StateTensorPresence, StateTensorRole,
7 },
8 AttentionPolicy, LayerSchedule, ObservationKind, Observed,
9};
10use serde::{Deserialize, Serialize};
11use std::num::NonZeroU8;
12
13#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
15pub struct InputModalities {
16 pub text: bool,
18 pub image: bool,
20 pub audio: bool,
22 pub video: bool,
24}
25
26impl InputModalities {
27 pub const TEXT: Self = Self {
29 text: true,
30 image: false,
31 audio: false,
32 video: false,
33 };
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(tag = "strategy", rename_all = "snake_case")]
39pub enum CacheStateStrategy {
40 FullKv,
42 SlidingKv {
44 window: u64,
46 },
47 SlidingKey {
49 window: u64,
51 layers: u64,
53 pooling_layers: u64,
55 },
56 MixedKv {
58 full_layers: u64,
60 sliding: Vec<SlidingWindowLayerCount>,
62 },
63 SharedFullKv {
65 cached_layers: u64,
67 shared_layers: u64,
69 full_attention_layers: u64,
71 sliding_attention: Vec<SlidingWindowLayerCount>,
73 },
74 CompressedMla {
76 latent_width: u64,
78 rotary_width: u64,
80 },
81 HybridRecurrent {
83 full_attention_layers: u64,
85 sliding_attention: Vec<SlidingWindowLayerCount>,
87 recurrent_layers: u64,
89 },
90 Multimodal {
92 decoder: Box<CacheStateStrategy>,
94 media_consumes_decoder_positions: bool,
96 },
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101pub struct SlidingWindowLayerCount {
102 pub window: u64,
104 pub layers: u64,
106}
107
108#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum EstimationCompleteness {
112 Complete,
114 Conservative,
116 PersistentStateOnly,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct ModelCapabilities {
123 pub effective_model_type: String,
125 pub native_max_context: Observed<u64>,
127 pub effective_max_context: Observed<u64>,
129 pub state_strategy: CacheStateStrategy,
131 pub modalities: InputModalities,
133 pub estimation: EstimationCompleteness,
135}
136
137#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
139pub struct InputTokenCount {
140 pub text_tokens: u64,
142 pub media_positions: u64,
144 pub model_positions: u64,
146 pub kind: ObservationKind,
148 media_execution_workspace_bytes: u64,
149 media_execution_workspace_kind: ObservationKind,
150}
151
152impl InputTokenCount {
153 pub const fn text(tokens: u64) -> Self {
155 Self {
156 text_tokens: tokens,
157 media_positions: 0,
158 model_positions: tokens,
159 kind: ObservationKind::Exact,
160 media_execution_workspace_bytes: 0,
161 media_execution_workspace_kind: ObservationKind::Exact,
162 }
163 }
164
165 pub const fn prepared(
167 text_tokens: u64,
168 media_positions: u64,
169 model_positions: u64,
170 media_execution_workspace_bytes: u64,
171 media_execution_workspace_kind: ObservationKind,
172 ) -> Self {
173 Self {
174 text_tokens,
175 media_positions,
176 model_positions,
177 kind: ObservationKind::Exact,
178 media_execution_workspace_bytes,
179 media_execution_workspace_kind,
180 }
181 }
182
183 pub const fn media_execution_workspace_bytes(&self) -> u64 {
185 self.media_execution_workspace_bytes
186 }
187
188 pub const fn media_execution_workspace_kind(&self) -> ObservationKind {
190 self.media_execution_workspace_kind
191 }
192}
193
194#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
196pub struct StateMemoryAssumptions {
197 pub floating_state_dtype_bytes: NonZeroU8,
201 pub batch_size: u64,
203 pub requested_positions: u64,
205 pub sliding_window_bounds: Vec<u64>,
207 pub allocation_granularity: u64,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213pub struct RuntimeStateEstimate {
214 pub fixed_state_bytes: u64,
216 pub bytes_per_position_per_batch: u64,
218 pub context_state_bytes: u64,
220 pub multimodal_embedding_bytes: u64,
222 pub media_execution_workspace_bytes: u64,
224 pub requested_state_bytes: u64,
226 pub assumptions: StateMemoryAssumptions,
228 pub completeness: EstimationCompleteness,
230}
231
232#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
234#[serde(rename_all = "snake_case")]
235pub enum PhysicalMemorySemantics {
236 Unified,
238 SeparateTiers,
240 Unknown,
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct StaticMemoryReport {
247 pub logical_parameter_bytes: Observed<u64>,
249 pub current_host_resident_bytes: Observed<u64>,
251 pub current_device_resident_bytes: Observed<u64>,
253 pub planned_disk_backed_bytes: Observed<u64>,
255 pub backend_active_allocation_bytes: Observed<u64>,
257 pub backend_allocator_cache_bytes: Observed<u64>,
259 pub physical_semantics: PhysicalMemorySemantics,
261 pub currently_cached_shards: Observed<u64>,
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct AvailableMemory {
268 pub physical_memory_bytes: Observed<u64>,
270 pub available_memory_bytes: Observed<u64>,
272 pub physical_semantics: PhysicalMemorySemantics,
274}
275
276#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
278pub struct AdmissionRequest {
279 pub input: InputTokenCount,
281 pub max_output_tokens: u64,
283 pub batch_size: u64,
285 pub safety_reserve_bytes: u64,
287 pub application_memory_budget_bytes: Option<u64>,
289 pub require_complete_estimate: bool,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct Admission {
296 pub requested_positions: u64,
298 pub state: RuntimeStateEstimate,
300 pub incremental_required_bytes: u64,
302 pub available_memory_bytes: Option<u64>,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(tag = "kind", rename_all = "snake_case")]
309pub enum AdmissionRejection {
310 PromptExceedsContext {
312 prompt_positions: u64,
314 maximum_positions: u64,
316 },
317 OutputHeadroomExceedsContext {
319 prompt_positions: u64,
321 output_tokens: u64,
323 maximum_positions: u64,
325 },
326 MemoryBudgetExceeded {
328 required_bytes: u64,
330 budget_bytes: u64,
332 },
333 InsufficientAvailableMemory {
335 required_bytes: u64,
337 available_bytes: u64,
339 },
340 AvailableMemoryUnavailable {
342 reason: String,
344 },
345 EstimationUnsupported {
347 reason: String,
349 },
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(tag = "status", rename_all = "snake_case")]
355pub enum AdmissionResult {
356 Admitted(Admission),
358 Rejected(AdmissionRejection),
360}
361
362#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
364pub enum CapabilityError {
365 #[error("invalid model capability field {field}: {detail}")]
367 InvalidConfiguration {
368 field: &'static str,
370 detail: String,
372 },
373 #[error("capability arithmetic overflow while computing {operation}")]
375 ArithmeticOverflow {
376 operation: &'static str,
378 },
379 #[error("unsupported prepared input for {architecture}: {reason}")]
381 UnsupportedInput {
382 architecture: String,
384 reason: String,
386 },
387 #[error("capability observation failed: {0}")]
389 Observation(String),
390}
391
392#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
399pub struct StateMemoryLayout {
400 layer_layout: LayerSchedule<LayerCachePolicy>,
402 layer_prefix_offsets: Vec<i32>,
404 pub hidden_size: u64,
406 pub allocation_granularity: u64,
408 pub completeness: EstimationCompleteness,
410}
411
412impl StateMemoryLayout {
413 pub fn new(
415 layer_layout: LayerSchedule<LayerCachePolicy>,
416 layer_prefix_offsets: Vec<i32>,
417 hidden_size: u64,
418 allocation_granularity: u64,
419 completeness: EstimationCompleteness,
420 ) -> Result<Self, CapabilityError> {
421 if layer_layout.is_empty()
422 || layer_prefix_offsets.len() != layer_layout.len()
423 || layer_prefix_offsets.iter().any(|offset| *offset > 0)
424 || hidden_size == 0
425 || allocation_granularity == 0
426 {
427 let (field, detail) = if layer_layout.is_empty() {
428 (
429 "layer_layout",
430 "must contain at least one executable state layer",
431 )
432 } else if layer_prefix_offsets.len() != layer_layout.len() {
433 (
434 "layer_prefix_offsets",
435 "must contain one entry per executable state layer",
436 )
437 } else if layer_prefix_offsets.iter().any(|offset| *offset > 0) {
438 (
439 "layer_prefix_offsets",
440 "must not advance beyond the request token frontier",
441 )
442 } else if hidden_size == 0 {
443 ("hidden_size", "must be positive")
444 } else {
445 ("allocation_granularity", "must be positive")
446 };
447 return Err(CapabilityError::InvalidConfiguration {
448 field,
449 detail: detail.into(),
450 });
451 }
452 for (layer, policy) in layer_layout.iter().enumerate() {
453 policy
454 .validate()
455 .map_err(|error| CapabilityError::InvalidConfiguration {
456 field: "layer_layout",
457 detail: format!("invalid state policy at layer {layer}: {error}"),
458 })?;
459 }
460 Ok(Self {
461 layer_layout,
462 layer_prefix_offsets,
463 hidden_size,
464 allocation_granularity,
465 completeness,
466 })
467 }
468
469 pub const fn layer_layout(&self) -> &LayerSchedule<LayerCachePolicy> {
471 &self.layer_layout
472 }
473
474 pub fn layer_prefix_offsets(&self) -> &[i32] {
476 &self.layer_prefix_offsets
477 }
478}
479
480fn checked_add(left: u64, right: u64, operation: &'static str) -> Result<u64, CapabilityError> {
481 left.checked_add(right)
482 .ok_or(CapabilityError::ArithmeticOverflow { operation })
483}
484
485fn checked_mul(left: u64, right: u64, operation: &'static str) -> Result<u64, CapabilityError> {
486 left.checked_mul(right)
487 .ok_or(CapabilityError::ArithmeticOverflow { operation })
488}
489
490fn attention_scalars_per_position(policy: &LayerCachePolicy) -> Result<u64, CapabilityError> {
491 let scalars = match policy {
492 LayerCachePolicy::KeyValue {
493 num_key_value_heads,
494 head_dim,
495 ..
496 }
497 | LayerCachePolicy::KeyValueWithFixedState {
498 num_key_value_heads,
499 head_dim,
500 ..
501 } => checked_mul(
502 checked_mul(
503 u64::from(num_key_value_heads.get()),
504 u64::from(head_dim.get()),
505 "key/value heads times head dimension",
506 )?,
507 2,
508 "key plus value scalars",
509 )?,
510 LayerCachePolicy::KeyOnly {
511 num_key_heads,
512 head_dim,
513 ..
514 }
515 | LayerCachePolicy::KeyOnlyWithFixedState {
516 num_key_heads,
517 head_dim,
518 ..
519 } => checked_mul(
520 u64::from(num_key_heads.get()),
521 u64::from(head_dim.get()),
522 "key heads times head dimension",
523 )?,
524 LayerCachePolicy::CompressedLatentRotary {
525 latent_dim,
526 rotary_dim,
527 ..
528 } => checked_add(
529 u64::from(latent_dim.get()),
530 u64::from(rotary_dim.get()),
531 "compressed latent plus rotary width",
532 )?,
533 LayerCachePolicy::NoState | LayerCachePolicy::FixedState { .. } => 0,
534 };
535 Ok(scalars)
536}
537
538fn is_context_dependent_dimension(dimension: &StateTensorDimension) -> bool {
539 matches!(
540 dimension,
541 StateTensorDimension::PrefixTokens
542 | StateTensorDimension::PrefixTokensDiv(_)
543 | StateTensorDimension::PrefixTokensRem(_)
544 )
545}
546
547fn state_tensor_dtype_bytes(tensor: &StateTensorPolicy, floating_scalar_bytes: u64) -> u64 {
548 match tensor.dtype {
549 StateTensorDtype::Floating => floating_scalar_bytes,
550 StateTensorDtype::Float32 | StateTensorDtype::Int32 | StateTensorDtype::Uint32 => 4,
551 }
552}
553
554fn state_tensor_is_present(tensor: &StateTensorPolicy, prefix_tokens: usize) -> bool {
555 match tensor.presence {
556 StateTensorPresence::Required => true,
557 StateTensorPresence::Optional => !matches!(tensor.role, StateTensorRole::PrefixEmbedding),
562 StateTensorPresence::PrefixRemainderNonZero(divisor) => {
563 !prefix_tokens.is_multiple_of(divisor.get() as usize)
564 }
565 StateTensorPresence::PrefixAtLeast(divisor) => prefix_tokens >= divisor.get() as usize,
566 }
567}
568
569fn state_tensor_bytes(
570 tensor: &StateTensorPolicy,
571 batch_size: usize,
572 prefix_tokens: usize,
573 floating_scalar_bytes: u64,
574) -> Result<u64, CapabilityError> {
575 if !state_tensor_is_present(tensor, prefix_tokens) {
576 return Ok(0);
577 }
578 let shape = tensor
579 .resolved_shape(batch_size, prefix_tokens)
580 .map_err(|error| CapabilityError::InvalidConfiguration {
581 field: "layer_layout",
582 detail: error.to_string(),
583 })?;
584 let scalars = shape.into_iter().try_fold(1_u64, |scalars, dimension| {
585 checked_mul(
586 scalars,
587 u64::try_from(dimension).map_err(|_| CapabilityError::InvalidConfiguration {
588 field: "layer_layout",
589 detail: "runtime state tensor has a negative resolved dimension".into(),
590 })?,
591 "runtime state tensor scalar count",
592 )
593 })?;
594 checked_mul(
595 scalars,
596 state_tensor_dtype_bytes(tensor, floating_scalar_bytes),
597 "runtime state tensor bytes",
598 )
599}
600
601fn state_tensor_bytes_per_position_per_batch(
602 tensor: &StateTensorPolicy,
603 floating_scalar_bytes: u64,
604) -> Result<u64, CapabilityError> {
605 let mut scalars = 1_u64;
606 let mut divisor = 1_u64;
607 let mut unbounded = false;
608 for dimension in &tensor.shape {
609 match dimension {
610 StateTensorDimension::Batch | StateTensorDimension::Scalar => {}
611 StateTensorDimension::Fixed(value) => {
612 scalars =
613 checked_mul(scalars, u64::from(value.get()), "state growth scalar count")?;
614 }
615 StateTensorDimension::PrefixTokens => unbounded = true,
616 StateTensorDimension::PrefixTokensDiv(value) => {
617 unbounded = true;
618 divisor = checked_mul(divisor, u64::from(value.get()), "state growth divisor")?;
619 }
620 StateTensorDimension::PrefixTokensRem(_) => return Ok(0),
621 }
622 }
623 if !unbounded {
624 return Ok(0);
625 }
626 let bytes = checked_mul(
627 scalars,
628 state_tensor_dtype_bytes(tensor, floating_scalar_bytes),
629 "state growth bytes",
630 )?;
631 Ok(bytes.div_ceil(divisor))
632}
633
634pub fn estimate_runtime_state(
636 layout: &StateMemoryLayout,
637 input: InputTokenCount,
638 max_output_tokens: u64,
639 batch_size: u64,
640 floating_state_dtype_bytes: NonZeroU8,
641) -> Result<RuntimeStateEstimate, CapabilityError> {
642 if batch_size == 0 {
643 return Err(CapabilityError::InvalidConfiguration {
644 field: "batch_size",
645 detail: "must be positive".into(),
646 });
647 }
648 let requested_positions = checked_add(
649 input.model_positions,
650 max_output_tokens,
651 "prompt plus output positions",
652 )?;
653 let floating_scalar_bytes = u64::from(floating_state_dtype_bytes.get());
654 let batch_size_usize =
655 usize::try_from(batch_size).map_err(|_| CapabilityError::InvalidConfiguration {
656 field: "batch_size",
657 detail: "exceeds the runtime state shape range".into(),
658 })?;
659 let mut fixed_state_bytes = 0;
660 let mut context_state_bytes = 0;
661 let mut unbounded_per_position = 0;
662 let mut sliding_window_bounds = Vec::new();
663 for (layer, policy) in layout.layer_layout.iter().enumerate() {
664 let layer_positions = requested_positions
665 .saturating_sub(u64::from(layout.layer_prefix_offsets[layer].unsigned_abs()));
666 let layer_positions_usize = usize::try_from(layer_positions).map_err(|_| {
667 CapabilityError::InvalidConfiguration {
668 field: "requested_positions",
669 detail: "exceeds the runtime state shape range".into(),
670 }
671 })?;
672 if let Some(attention) = policy.attention() {
673 let per_position = attention_scalars_per_position(policy)?;
674 let retained = match attention {
675 AttentionPolicy::Sliding { window } => {
676 let window = u64::from(window.get());
677 sliding_window_bounds.push(window);
678 layer_positions.min(window)
679 }
680 AttentionPolicy::Full => {
681 let adjustment = layout.allocation_granularity - 1;
682 checked_add(layer_positions, adjustment, "cache allocation rounding")?
683 / layout.allocation_granularity
684 * layout.allocation_granularity
685 }
686 };
687 let bytes = checked_mul(
688 checked_mul(
689 checked_mul(per_position, retained, "attention context scalars")?,
690 batch_size,
691 "attention context batch",
692 )?,
693 floating_scalar_bytes,
694 "attention context bytes",
695 )?;
696 context_state_bytes =
697 checked_add(context_state_bytes, bytes, "context state byte total")?;
698 if matches!(attention, AttentionPolicy::Full) {
699 unbounded_per_position = checked_add(
700 unbounded_per_position,
701 checked_mul(
702 per_position,
703 floating_scalar_bytes,
704 "unbounded bytes per position",
705 )?,
706 "unbounded bytes-per-position total",
707 )?;
708 }
709 }
710 for tensor in policy.fixed_state() {
711 let bytes = state_tensor_bytes(
712 tensor,
713 batch_size_usize,
714 layer_positions_usize,
715 floating_scalar_bytes,
716 )?;
717 if tensor.shape.iter().any(is_context_dependent_dimension) {
718 context_state_bytes =
719 checked_add(context_state_bytes, bytes, "context state byte total")?;
720 unbounded_per_position = checked_add(
721 unbounded_per_position,
722 state_tensor_bytes_per_position_per_batch(tensor, floating_scalar_bytes)?,
723 "unbounded bytes-per-position total",
724 )?;
725 } else {
726 fixed_state_bytes =
727 checked_add(fixed_state_bytes, bytes, "fixed state byte total")?;
728 }
729 }
730 }
731 sliding_window_bounds.sort_unstable();
732 sliding_window_bounds.dedup();
733 let multimodal_embedding_bytes = checked_mul(
734 checked_mul(
735 checked_mul(
736 input.media_positions,
737 layout.hidden_size,
738 "media positions times hidden size",
739 )?,
740 batch_size,
741 "media embeddings times batch",
742 )?,
743 floating_scalar_bytes,
744 "media embedding bytes",
745 )?;
746 let media_execution_workspace_bytes = checked_mul(
747 input.media_execution_workspace_bytes,
748 batch_size,
749 "media execution workspace times batch",
750 )?;
751 let requested_state_bytes = checked_add(
752 checked_add(
753 checked_add(
754 fixed_state_bytes,
755 context_state_bytes,
756 "fixed plus context state",
757 )?,
758 multimodal_embedding_bytes,
759 "persistent plus multimodal embedding state",
760 )?,
761 media_execution_workspace_bytes,
762 "persistent plus media execution workspace",
763 )?;
764 let completeness = if input.media_positions == 0
765 || input.media_execution_workspace_kind == ObservationKind::Exact
766 {
767 layout.completeness
768 } else {
769 EstimationCompleteness::Conservative
770 };
771 Ok(RuntimeStateEstimate {
772 fixed_state_bytes,
773 bytes_per_position_per_batch: unbounded_per_position,
774 context_state_bytes,
775 multimodal_embedding_bytes,
776 media_execution_workspace_bytes,
777 requested_state_bytes,
778 assumptions: StateMemoryAssumptions {
779 floating_state_dtype_bytes,
780 batch_size,
781 requested_positions,
782 sliding_window_bounds,
783 allocation_granularity: layout.allocation_granularity,
784 },
785 completeness,
786 })
787}
788
789pub fn apply_admission_policy(
791 capabilities: &ModelCapabilities,
792 request: AdmissionRequest,
793 state: RuntimeStateEstimate,
794 available: Option<&AvailableMemory>,
795) -> Result<AdmissionResult, CapabilityError> {
796 let maximum = match &capabilities.effective_max_context {
797 Observed::Available { value, .. } => *value,
798 Observed::Unsupported { reason } | Observed::Unavailable { reason } => {
799 return Ok(AdmissionResult::Rejected(
800 AdmissionRejection::EstimationUnsupported {
801 reason: reason.clone(),
802 },
803 ));
804 }
805 };
806 if request.input.model_positions > maximum {
807 return Ok(AdmissionResult::Rejected(
808 AdmissionRejection::PromptExceedsContext {
809 prompt_positions: request.input.model_positions,
810 maximum_positions: maximum,
811 },
812 ));
813 }
814 let requested_positions = checked_add(
815 request.input.model_positions,
816 request.max_output_tokens,
817 "admission prompt plus output",
818 )?;
819 if requested_positions > maximum {
820 return Ok(AdmissionResult::Rejected(
821 AdmissionRejection::OutputHeadroomExceedsContext {
822 prompt_positions: request.input.model_positions,
823 output_tokens: request.max_output_tokens,
824 maximum_positions: maximum,
825 },
826 ));
827 }
828 if request.require_complete_estimate
829 && state.completeness == EstimationCompleteness::PersistentStateOnly
830 {
831 return Ok(AdmissionResult::Rejected(
832 AdmissionRejection::EstimationUnsupported {
833 reason: format!(
834 "architecture estimator coverage is {:?}",
835 state.completeness
836 ),
837 },
838 ));
839 }
840 let incremental_required_bytes = checked_add(
841 state.requested_state_bytes,
842 request.safety_reserve_bytes,
843 "state plus safety reserve",
844 )?;
845 if let Some(budget_bytes) = request.application_memory_budget_bytes {
846 if incremental_required_bytes > budget_bytes {
847 return Ok(AdmissionResult::Rejected(
848 AdmissionRejection::MemoryBudgetExceeded {
849 required_bytes: incremental_required_bytes,
850 budget_bytes,
851 },
852 ));
853 }
854 }
855 let available_memory_bytes = match available {
856 Some(report) => match &report.available_memory_bytes {
857 Observed::Available { value, .. } => Some(*value),
858 Observed::Unsupported { reason } | Observed::Unavailable { reason } => {
859 return Ok(AdmissionResult::Rejected(
860 AdmissionRejection::AvailableMemoryUnavailable {
861 reason: reason.clone(),
862 },
863 ))
864 }
865 },
866 None => None,
867 };
868 if let Some(available_bytes) = available_memory_bytes {
869 if incremental_required_bytes > available_bytes {
870 return Ok(AdmissionResult::Rejected(
871 AdmissionRejection::InsufficientAvailableMemory {
872 required_bytes: incremental_required_bytes,
873 available_bytes,
874 },
875 ));
876 }
877 }
878 Ok(AdmissionResult::Admitted(Admission {
879 requested_positions,
880 state,
881 incremental_required_bytes,
882 available_memory_bytes,
883 }))
884}
885
886#[cfg(test)]
887mod tests {
888 use super::*;
889
890 #[test]
891 fn state_estimation_and_admission_are_backend_independent() {
892 let policies = (0..2)
893 .map(|_| LayerCachePolicy::key_only(AttentionPolicy::Full, 1, 8).unwrap())
894 .collect::<Vec<_>>();
895 let layout = StateMemoryLayout::new(
896 LayerSchedule::new(2, policies).unwrap(),
897 vec![0; 2],
898 32,
899 8,
900 EstimationCompleteness::Complete,
901 )
902 .unwrap();
903 let input = InputTokenCount::text(5);
904 let state =
905 estimate_runtime_state(&layout, input, 2, 1, NonZeroU8::new(4).unwrap()).unwrap();
906 assert_eq!(state.assumptions.requested_positions, 7);
907 assert_eq!(state.context_state_bytes, 512);
908 let capabilities = ModelCapabilities {
909 effective_model_type: "mock".into(),
910 native_max_context: Observed::exact(16, "mock"),
911 effective_max_context: Observed::exact(16, "mock"),
912 state_strategy: CacheStateStrategy::FullKv,
913 modalities: InputModalities::TEXT,
914 estimation: EstimationCompleteness::Complete,
915 };
916 let serialized = serde_json::to_value(&capabilities).unwrap();
917 assert_eq!(serialized["effective_model_type"], "mock");
918 assert!(serialized.get("model_type").is_none());
919 assert!(matches!(
920 apply_admission_policy(
921 &capabilities,
922 AdmissionRequest {
923 input,
924 max_output_tokens: 2,
925 batch_size: 1,
926 safety_reserve_bytes: 0,
927 application_memory_budget_bytes: Some(1024),
928 require_complete_estimate: true
929 },
930 state,
931 None
932 )
933 .unwrap(),
934 AdmissionResult::Admitted(_)
935 ));
936 }
937
938 #[test]
939 fn admission_rejections_are_portable_and_fail_closed() {
940 let capabilities = ModelCapabilities {
941 effective_model_type: "mock".into(),
942 native_max_context: Observed::exact(8, "mock"),
943 effective_max_context: Observed::exact(8, "mock"),
944 state_strategy: CacheStateStrategy::FullKv,
945 modalities: InputModalities::TEXT,
946 estimation: EstimationCompleteness::Complete,
947 };
948 let state = RuntimeStateEstimate {
949 fixed_state_bytes: 0,
950 bytes_per_position_per_batch: 0,
951 context_state_bytes: 0,
952 multimodal_embedding_bytes: 0,
953 media_execution_workspace_bytes: 0,
954 requested_state_bytes: 0,
955 assumptions: StateMemoryAssumptions {
956 floating_state_dtype_bytes: NonZeroU8::new(4).unwrap(),
957 batch_size: 1,
958 requested_positions: 9,
959 sliding_window_bounds: Vec::new(),
960 allocation_granularity: 1,
961 },
962 completeness: EstimationCompleteness::Complete,
963 };
964 let request = AdmissionRequest {
965 input: InputTokenCount::text(7),
966 max_output_tokens: 2,
967 batch_size: 1,
968 safety_reserve_bytes: 0,
969 application_memory_budget_bytes: None,
970 require_complete_estimate: true,
971 };
972 assert!(matches!(
973 apply_admission_policy(&capabilities, request, state, None).unwrap(),
974 AdmissionResult::Rejected(AdmissionRejection::OutputHeadroomExceedsContext { .. })
975 ));
976
977 let unavailable = AvailableMemory {
978 physical_memory_bytes: Observed::unavailable("not reported"),
979 available_memory_bytes: Observed::unavailable("not reported"),
980 physical_semantics: PhysicalMemorySemantics::Unknown,
981 };
982 let request = AdmissionRequest {
983 input: InputTokenCount::text(1),
984 max_output_tokens: 0,
985 batch_size: 1,
986 safety_reserve_bytes: 0,
987 application_memory_budget_bytes: None,
988 require_complete_estimate: true,
989 };
990 let state = estimate_runtime_state(
991 &StateMemoryLayout::new(
992 LayerSchedule::new(1, vec![LayerCachePolicy::NoState]).unwrap(),
993 vec![0],
994 1,
995 1,
996 EstimationCompleteness::Complete,
997 )
998 .unwrap(),
999 request.input,
1000 0,
1001 1,
1002 NonZeroU8::new(4).unwrap(),
1003 )
1004 .unwrap();
1005 assert!(matches!(
1006 apply_admission_policy(&capabilities, request, state, Some(&unavailable)).unwrap(),
1007 AdmissionResult::Rejected(AdmissionRejection::AvailableMemoryUnavailable { .. })
1008 ));
1009 }
1010
1011 #[test]
1012 fn capability_and_memory_schemas_round_trip_without_a_backend() {
1013 let report = StaticMemoryReport {
1014 logical_parameter_bytes: Observed::exact(1_024, "mock catalog"),
1015 current_host_resident_bytes: Observed::exact(512, "mock ledger"),
1016 current_device_resident_bytes: Observed::exact(512, "mock ledger"),
1017 planned_disk_backed_bytes: Observed::exact(0, "mock plan"),
1018 backend_active_allocation_bytes: Observed::unavailable("no allocator probe"),
1019 backend_allocator_cache_bytes: Observed::unsupported("no allocator cache"),
1020 physical_semantics: PhysicalMemorySemantics::SeparateTiers,
1021 currently_cached_shards: Observed::exact(1, "mock store"),
1022 };
1023 let encoded = serde_json::to_string(&report).unwrap();
1024 let decoded: StaticMemoryReport = serde_json::from_str(&encoded).unwrap();
1025 assert_eq!(decoded, report);
1026 }
1027}