Skip to main content

eredu_core/
capability.rs

1//! Portable model capabilities, runtime-state accounting, and admission policy.
2
3use 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/// Model inputs accepted by a prepared model.
14#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
15pub struct InputModalities {
16    /// Ordinary tokenizer IDs.
17    pub text: bool,
18    /// Prepared image inputs.
19    pub image: bool,
20    /// Prepared audio inputs.
21    pub audio: bool,
22    /// Prepared video inputs.
23    pub video: bool,
24}
25
26impl InputModalities {
27    /// Text-only input support.
28    pub const TEXT: Self = Self {
29        text: true,
30        image: false,
31        audio: false,
32        video: false,
33    };
34}
35
36/// Persistent decoder-state strategy used by a model.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(tag = "strategy", rename_all = "snake_case")]
39pub enum CacheStateStrategy {
40    /// Ordinary full-context K/V attention.
41    FullKv,
42    /// Every attention cache is bounded by a sliding window.
43    SlidingKv {
44        /// Maximum retained positions per attention layer.
45        window: u64,
46    },
47    /// Sliding-window key-only attention, optionally with pooled history.
48    SlidingKey {
49        /// Maximum retained key positions per attention layer.
50        window: u64,
51        /// Total layers retaining local keys.
52        layers: u64,
53        /// Layers that additionally retain append-only pooling state.
54        pooling_layers: u64,
55    },
56    /// Full-context and sliding-window attention layers.
57    MixedKv {
58        /// Number of full-context layers.
59        full_layers: u64,
60        /// Bounded layer counts grouped by retained window.
61        sliding: Vec<SlidingWindowLayerCount>,
62    },
63    /// Full-context KV backing with layers that reuse earlier K/V state.
64    SharedFullKv {
65        /// Layers that allocate their own K/V state.
66        cached_layers: u64,
67        /// Layers that reuse K/V produced by an earlier layer.
68        shared_layers: u64,
69        /// Total full-attention layer count.
70        full_attention_layers: u64,
71        /// Sliding-mask layers, which do not bound KV allocation.
72        sliding_attention: Vec<SlidingWindowLayerCount>,
73    },
74    /// Multi-head latent attention compressed state.
75    CompressedMla {
76        /// Compressed latent width per layer and position.
77        latent_width: u64,
78        /// Shared rotary-key width per layer and position.
79        rotary_width: u64,
80    },
81    /// Attention combined with bounded convolution or recurrent state.
82    HybridRecurrent {
83        /// Full-context attention layer count.
84        full_attention_layers: u64,
85        /// Bounded attention layers grouped by exact window.
86        sliding_attention: Vec<SlidingWindowLayerCount>,
87        /// Recurrent/linear-attention layer count.
88        recurrent_layers: u64,
89    },
90    /// Multimodal preparation feeding positions into a decoder strategy.
91    Multimodal {
92        /// Underlying decoder state.
93        decoder: Box<CacheStateStrategy>,
94        /// Whether media embeddings consume persistent decoder positions.
95        media_consumes_decoder_positions: bool,
96    },
97}
98
99/// Sliding-attention layer count sharing one retained window.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101pub struct SlidingWindowLayerCount {
102    /// Exact positive retained positions, including the current token.
103    pub window: u64,
104    /// Number of layers using this window.
105    pub layers: u64,
106}
107
108/// Coverage of a runtime-state estimate.
109#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum EstimationCompleteness {
112    /// Persistent request state is modeled exactly.
113    Complete,
114    /// The estimate is a complete safe upper bound.
115    Conservative,
116    /// Persistent state is covered but execution transients are not.
117    PersistentStateOnly,
118}
119
120/// Capabilities derived from validated model configuration.
121#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
122pub struct ModelCapabilities {
123    /// Parsed implementation or nested text-model type.
124    pub effective_model_type: String,
125    /// Original trained context before a supported extension.
126    pub native_max_context: Observed<u64>,
127    /// Maximum positions accepted by the prepared model.
128    pub effective_max_context: Observed<u64>,
129    /// Persistent cache or recurrent-state model.
130    pub state_strategy: CacheStateStrategy,
131    /// Accepted input modalities.
132    pub modalities: InputModalities,
133    /// Runtime-state estimator coverage.
134    pub estimation: EstimationCompleteness,
135}
136
137/// Accounting for a tokenized or backend-prepared input.
138#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
139pub struct InputTokenCount {
140    /// Ordinary tokenizer IDs present in the input.
141    pub text_tokens: u64,
142    /// Positions inserted by prepared media.
143    pub media_positions: u64,
144    /// Total decoder/model positions consumed by prefill.
145    pub model_positions: u64,
146    /// Semantics of the model-position count.
147    pub kind: ObservationKind,
148    media_execution_workspace_bytes: u64,
149    media_execution_workspace_kind: ObservationKind,
150}
151
152impl InputTokenCount {
153    /// Creates an exact count for tokenized text.
154    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    /// Creates an exact position count for backend-prepared input.
166    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    /// Conservative media-tower workspace attributed to this input.
184    pub const fn media_execution_workspace_bytes(&self) -> u64 {
185        self.media_execution_workspace_bytes
186    }
187
188    /// Measurement semantics of the media workspace.
189    pub const fn media_execution_workspace_kind(&self) -> ObservationKind {
190        self.media_execution_workspace_kind
191    }
192}
193
194/// Floating-dtype and request assumptions used by state estimation.
195#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
196pub struct StateMemoryAssumptions {
197    /// Bytes per architecture-declared generic floating-state scalar.
198    ///
199    /// Fixed-dtype tensors use their own widths from the exact state policy.
200    pub floating_state_dtype_bytes: NonZeroU8,
201    /// Logical request batch size.
202    pub batch_size: u64,
203    /// Total requested positions, including output allowance.
204    pub requested_positions: u64,
205    /// Distinct sliding-window bounds in ascending order.
206    pub sliding_window_bounds: Vec<u64>,
207    /// Backing-array growth granularity for unbounded caches.
208    pub allocation_granularity: u64,
209}
210
211/// Persistent and transient runtime-state estimate for one request.
212#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
213pub struct RuntimeStateEstimate {
214    /// Context-independent recurrent/convolution state.
215    pub fixed_state_bytes: u64,
216    /// Unbounded bytes added per position before multiplying by batch.
217    pub bytes_per_position_per_batch: u64,
218    /// Persistent context-dependent bytes at the requested length.
219    pub context_state_bytes: u64,
220    /// Prepared-media embedding bytes retained during prefill.
221    pub multimodal_embedding_bytes: u64,
222    /// Conservative media-tower execution workspace.
223    pub media_execution_workspace_bytes: u64,
224    /// Total modeled state for prompt plus output allowance.
225    pub requested_state_bytes: u64,
226    /// Estimator assumptions.
227    pub assumptions: StateMemoryAssumptions,
228    /// Estimator coverage.
229    pub completeness: EstimationCompleteness,
230}
231
232/// Physical relationship between logical host and device tiers.
233#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
234#[serde(rename_all = "snake_case")]
235pub enum PhysicalMemorySemantics {
236    /// Host and accelerator allocations share physical capacity.
237    Unified,
238    /// Host and accelerator memory are physically separate.
239    SeparateTiers,
240    /// The backend cannot determine the relationship.
241    Unknown,
242}
243
244/// Static checkpoint and current residency observations.
245#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
246pub struct StaticMemoryReport {
247    /// Logical bytes in parameters or the complete residency plan.
248    pub logical_parameter_bytes: Observed<u64>,
249    /// Current logical host-resident bytes.
250    pub current_host_resident_bytes: Observed<u64>,
251    /// Current logical device-resident bytes.
252    pub current_device_resident_bytes: Observed<u64>,
253    /// Planned logical disk-backed bytes.
254    pub planned_disk_backed_bytes: Observed<u64>,
255    /// Process-global backend active allocation counter.
256    pub backend_active_allocation_bytes: Observed<u64>,
257    /// Process-global backend allocator-cache counter.
258    pub backend_allocator_cache_bytes: Observed<u64>,
259    /// Whether logical host/device tiers share physical capacity.
260    pub physical_semantics: PhysicalMemorySemantics,
261    /// Currently retained checkpoint shard buffers or readers.
262    pub currently_cached_shards: Observed<u64>,
263}
264
265/// System memory usable as an admission signal.
266#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
267pub struct AvailableMemory {
268    /// Unified/host physical memory.
269    pub physical_memory_bytes: Observed<u64>,
270    /// Defensible point-in-time availability estimate.
271    pub available_memory_bytes: Observed<u64>,
272    /// Physical tier semantics.
273    pub physical_semantics: PhysicalMemorySemantics,
274}
275
276/// One pre-generation admission request.
277#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
278pub struct AdmissionRequest {
279    /// Authoritative prompt accounting.
280    pub input: InputTokenCount,
281    /// Maximum generated-token allowance.
282    pub max_output_tokens: u64,
283    /// Logical batch size.
284    pub batch_size: u64,
285    /// Caller-selected reserve added to modeled state.
286    pub safety_reserve_bytes: u64,
287    /// Optional application budget for incremental state plus reserve.
288    pub application_memory_budget_bytes: Option<u64>,
289    /// Reject estimates that omit execution transients.
290    pub require_complete_estimate: bool,
291}
292
293/// Detailed successful admission.
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct Admission {
296    /// Prompt plus output allowance.
297    pub requested_positions: u64,
298    /// Runtime-state estimate.
299    pub state: RuntimeStateEstimate,
300    /// State plus caller reserve.
301    pub incremental_required_bytes: u64,
302    /// Availability signal used, when supplied.
303    pub available_memory_bytes: Option<u64>,
304}
305
306/// Structured reason a request was rejected before generation.
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308#[serde(tag = "kind", rename_all = "snake_case")]
309pub enum AdmissionRejection {
310    /// Prompt alone exceeds configured context.
311    PromptExceedsContext {
312        /// Prompt model positions.
313        prompt_positions: u64,
314        /// Effective model limit.
315        maximum_positions: u64,
316    },
317    /// Prompt fits but output allowance does not.
318    OutputHeadroomExceedsContext {
319        /// Prompt model positions.
320        prompt_positions: u64,
321        /// Requested maximum output tokens.
322        output_tokens: u64,
323        /// Effective model limit.
324        maximum_positions: u64,
325    },
326    /// Application budget is smaller than modeled state plus reserve.
327    MemoryBudgetExceeded {
328        /// Required incremental bytes.
329        required_bytes: u64,
330        /// Caller-supplied budget.
331        budget_bytes: u64,
332    },
333    /// Current availability is smaller than modeled state plus reserve.
334    InsufficientAvailableMemory {
335        /// Required incremental bytes.
336        required_bytes: u64,
337        /// Observed available bytes.
338        available_bytes: u64,
339    },
340    /// A requested availability check could not be performed.
341    AvailableMemoryUnavailable {
342        /// Platform report detail.
343        reason: String,
344    },
345    /// Policy requires estimator coverage the model cannot provide.
346    EstimationUnsupported {
347        /// Coverage detail.
348        reason: String,
349    },
350}
351
352/// Admission outcome.
353#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
354#[serde(tag = "status", rename_all = "snake_case")]
355pub enum AdmissionResult {
356    /// Request may proceed.
357    Admitted(Admission),
358    /// Request was rejected.
359    Rejected(AdmissionRejection),
360}
361
362/// Structured capability and accounting failures.
363#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
364pub enum CapabilityError {
365    /// A validated architecture exposed an invalid value.
366    #[error("invalid model capability field {field}: {detail}")]
367    InvalidConfiguration {
368        /// Invalid field name.
369        field: &'static str,
370        /// Invalid-value detail.
371        detail: String,
372    },
373    /// Checked byte or position arithmetic overflowed.
374    #[error("capability arithmetic overflow while computing {operation}")]
375    ArithmeticOverflow {
376        /// Stable operation label.
377        operation: &'static str,
378    },
379    /// Prepared input does not match the loaded architecture.
380    #[error("unsupported prepared input for {architecture}: {reason}")]
381    UnsupportedInput {
382        /// Effective architecture name.
383        architecture: String,
384        /// Unsupported-input detail.
385        reason: String,
386    },
387    /// A runtime observation could not be obtained.
388    #[error("capability observation failed: {0}")]
389    Observation(String),
390}
391
392/// Memory-accounting view of an executable runtime-state layout.
393///
394/// The ordered layer policies are copied directly from the architecture's
395/// executable [`LayerSchedule`]. They are intentionally not summarized into a
396/// second scalar geometry, so execution and admission share one semantic
397/// source for every state-bearing layer and component.
398#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
399pub struct StateMemoryLayout {
400    /// Exact ordered executable state policies.
401    layer_layout: LayerSchedule<LayerCachePolicy>,
402    /// Executable processed-token offset for every state layer.
403    layer_prefix_offsets: Vec<i32>,
404    /// Model hidden width used by retained media embeddings.
405    pub hidden_size: u64,
406    /// Allocation granularity for unbounded caches.
407    pub allocation_granularity: u64,
408    /// Coverage supplied by the layout.
409    pub completeness: EstimationCompleteness,
410}
411
412impl StateMemoryLayout {
413    /// Creates accounting metadata around an exact executable layer schedule.
414    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    /// Borrows the exact ordered executable state policies.
470    pub const fn layer_layout(&self) -> &LayerSchedule<LayerCachePolicy> {
471        &self.layer_layout
472    }
473
474    /// Returns executable processed-token offsets in state-layer order.
475    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        // Prepared prefix embeddings are accounted once through the input's
558        // authoritative media-position count below. Any other optional state
559        // is included conservatively because its request-time presence is not
560        // otherwise represented in the portable input descriptor.
561        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
634/// Estimates request state from exact executable layer policies.
635pub 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
789/// Applies context and memory policy to an already-computed state estimate.
790pub 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}