Skip to main content

ferrum_interfaces/vnext/execution/memory/
startup.rs

1//! Metadata-only capacity evaluation over the compiled physical memory plan.
2
3use super::*;
4
5impl MemoryPlan {
6    /// Physical bytes needed by equal-length sequences and one execution wave.
7    ///
8    /// This is startup planning evidence, never a reservation or an admission
9    /// permit. Runtime admission still handles live cache retention, contention,
10    /// and changing device availability. Each sequence is allocated separately;
11    /// physical page padding, provider formulas, and proven workspace reuse are
12    /// evaluated by the same plan helpers used to size runtime backing.
13    pub fn startup_peak_bytes(
14        &self,
15        context_tokens: u64,
16        active_sequences: u32,
17        step_tokens: u64,
18    ) -> Result<u64, VNextError> {
19        self.startup_workload_peak_bytes(
20            context_tokens,
21            context_tokens,
22            active_sequences,
23            step_tokens,
24        )
25    }
26
27    /// Request-lifetime storage owns the complete requested input/output
28    /// ceiling; sequence state owns only its committed frontier. Keep these
29    /// dimensions separate when evaluating shallow concurrent decoding.
30    pub fn startup_workload_peak_bytes(
31        &self,
32        request_ceiling_tokens: u64,
33        sequence_frontier_tokens: u64,
34        active_sequences: u32,
35        step_tokens: u64,
36    ) -> Result<u64, VNextError> {
37        if request_ceiling_tokens == 0
38            || sequence_frontier_tokens == 0
39            || sequence_frontier_tokens > request_ceiling_tokens
40            || active_sequences == 0
41            || step_tokens == 0
42        {
43            return Err(invalid_plan("startup workload dimensions must be non-zero"));
44        }
45        if active_sequences > self.maximum_active_sequences {
46            return Err(invalid_plan(
47                "startup workload exceeds the compiled sequence ceiling",
48            ));
49        }
50        // Product language-model state is token-derived. Page-derived providers
51        // must supply actual page evidence instead of assuming a token/page ratio.
52        let request_shape = DynamicResourceShape::from_validated(1, request_ceiling_tokens, 0);
53        let sequence_shape = DynamicResourceShape::from_validated(1, sequence_frontier_tokens, 0);
54        let step_shape = DynamicResourceShape::from_validated(active_sequences, step_tokens, 0);
55        let descriptors = self
56            .dynamic_descriptors
57            .iter()
58            .map(|descriptor| (descriptor.base_resource_id.clone(), descriptor))
59            .collect::<BTreeMap<_, _>>();
60        let sealed_workspace = self
61            .reusable_execution
62            .as_ref()
63            .map(|reusable| reusable.startup_sealed_pool_workspace_bytes())
64            .transpose()?
65            .unwrap_or_default();
66
67        self.dynamic_pools
68            .iter()
69            .try_fold(self.static_bytes, |total, pool| {
70                let sequence_bytes = pool.resource_ids.iter().try_fold(0_u64, |bytes, id| {
71                    let descriptor = descriptors.get(id).ok_or_else(|| {
72                        invalid_plan("startup pool references a missing descriptor")
73                    })?;
74                    let shape = match descriptor.lifetime() {
75                        AllocationLifetime::Request => request_shape,
76                        AllocationLifetime::Sequence => sequence_shape,
77                        _ => return Ok(bytes),
78                    };
79                    let per_sequence = descriptor.evaluate_request_bytes_for_shape(shape)?;
80                    per_sequence
81                        .checked_mul(u64::from(active_sequences))
82                        .and_then(|amount| bytes.checked_add(amount))
83                        .ok_or_else(|| invalid_plan("startup sequence memory overflows u64"))
84                })?;
85                let step_bytes =
86                    Self::reusable_step_bytes_for_shape(pool, &descriptors, step_shape)?;
87                let invocation_bytes =
88                    Self::reusable_invocation_bytes_for_shape(pool, &descriptors, step_shape)?;
89                let mut wave_bytes = step_bytes
90                    .checked_add(invocation_bytes)
91                    .ok_or_else(|| invalid_plan("startup execution memory overflows u64"))?;
92
93                // Reusable execution can round the wave up to a compiled shape
94                // bucket. Preserve that physical capacity without charging every
95                // possible cached program as though it executes concurrently.
96                if let Some(reusable) = &self.reusable_execution {
97                    let mut covered_classes = BTreeSet::new();
98                    let mut class_buckets = BTreeMap::new();
99                    for resolved in reusable.buckets() {
100                        let bucket = resolved.bucket();
101                        if !covered_classes.contains(bucket.class_id()) {
102                            class_buckets.insert(bucket.class_id(), resolved);
103                            if bucket.capacity().covers(active_sequences, step_tokens, 0) {
104                                covered_classes.insert(bucket.class_id());
105                            }
106                        }
107                    }
108                    // Retaining the largest bucket beyond a class's coverage keeps
109                    // this upper bound monotone when execution falls back to eager.
110                    for resolved in class_buckets.into_values() {
111                        if let Some(budget) = resolved
112                            .pool_budgets()
113                            .iter()
114                            .find(|budget| budget.pool_id() == pool.pool_id())
115                        {
116                            wave_bytes = wave_bytes.max(budget.total_bytes()?);
117                        }
118                    }
119                    // Workspace buckets apply before exact-program lookup, so
120                    // even an eager miss can need a rounded, uncaptured arena.
121                    // Sealed captured arenas cannot be trimmed to make room.
122                    wave_bytes = wave_bytes
123                        .checked_add(sealed_workspace.get(pool.pool_id()).copied().unwrap_or(0))
124                        .ok_or_else(|| invalid_plan("sealed startup workspace overflows u64"))?;
125                }
126                let required = sequence_bytes
127                    .checked_add(wave_bytes)
128                    .ok_or_else(|| invalid_plan("startup pool memory overflows u64"))?
129                    .max(pool.provisioning.minimum_resident_bytes());
130                total
131                    .checked_add(required)
132                    .ok_or_else(|| invalid_plan("startup plan memory overflows u64"))
133            })
134    }
135}