Skip to main content

ferrum_interfaces/vnext/execution/
memory.rs

1use super::{
2    invalid_plan, minimum_for_lifetime, node_completion_precedes, quantize_storage_bytes,
3    static_contiguous_storage_profile, validate_active_sequence_ceiling,
4    validate_pool_liveness_rows, workspace_storage_layout_fingerprint, AllocationKind,
5    AllocationLifetime, BTreeMap, BTreeSet, BufferRequest, BufferUsage, CanonicalU128, Deserialize,
6    Deserializer, DynamicBackingPoolId, DynamicBackingPoolSpec, DynamicResourceDemand,
7    DynamicResourceDescriptor, DynamicResourceShape, ElementType, InvocationLivenessMode,
8    InvocationResourceLiveness, NodeId, PlanNode, PoolAggregateEvidence, PoolCompatibilityKey,
9    ResolvedReusableExecutionBucket, ResourceAllocation, ResourceId, ReusableExecutionMemoryPlan,
10    ReusableExecutionPolicy, ReusablePoolWorkspaceBudget, Serialize, StepResourceSlot,
11    StepResourceSlotKind, VNextError, MAX_EXECUTION_PLAN_RESOURCE_ROWS,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
15pub struct MemoryPlan {
16    pub(super) device_capacity_bytes: u64,
17    pub(super) policy_capacity_bytes: u64,
18    pub(super) reserve_bytes: u64,
19    pub(super) usable_capacity_bytes: u64,
20    pub(super) maximum_active_sequences: u32,
21    pub(super) static_bytes: u64,
22    pub(super) minimum_request_bytes: u64,
23    pub(super) minimum_sequence_bytes: u64,
24    pub(super) minimum_step_bytes: u64,
25    pub(super) minimum_invocation_peak_bytes: u64,
26    pub(super) minimum_runnable_request_bytes: u64,
27    pub(super) theoretical_ceiling_bytes: CanonicalU128,
28    pub(super) static_allocations: Vec<ResourceAllocation>,
29    pub(super) dynamic_descriptors: Vec<DynamicResourceDescriptor>,
30    pub(super) dynamic_pools: Vec<DynamicBackingPoolSpec>,
31    pub(super) reusable_execution: Option<ReusableExecutionMemoryPlan>,
32    pub(super) invocation_liveness_mode: InvocationLivenessMode,
33    pub(super) invocation_liveness: Vec<InvocationResourceLiveness>,
34}
35
36impl MemoryPlan {
37    #[cfg(test)]
38    pub(super) fn from_core(
39        device_capacity_bytes: u64,
40        policy_capacity_bytes: u64,
41        reserve_bytes: u64,
42        maximum_active_sequences: u32,
43        static_allocations: Vec<ResourceAllocation>,
44        dynamic_descriptors: Vec<DynamicResourceDescriptor>,
45        nodes: &[PlanNode],
46        reusable_execution_policy: Option<&ReusableExecutionPolicy>,
47    ) -> Result<Self, VNextError> {
48        Self::from_core_with_completion_retention(
49            device_capacity_bytes,
50            policy_capacity_bytes,
51            reserve_bytes,
52            maximum_active_sequences,
53            static_allocations,
54            dynamic_descriptors,
55            nodes,
56            reusable_execution_policy,
57            &BTreeSet::new(),
58        )
59    }
60
61    #[allow(clippy::too_many_arguments)]
62    pub(super) fn from_core_with_completion_retention(
63        device_capacity_bytes: u64,
64        policy_capacity_bytes: u64,
65        reserve_bytes: u64,
66        maximum_active_sequences: u32,
67        mut static_allocations: Vec<ResourceAllocation>,
68        mut dynamic_descriptors: Vec<DynamicResourceDescriptor>,
69        nodes: &[PlanNode],
70        reusable_execution_policy: Option<&ReusableExecutionPolicy>,
71        retained_completion_resources: &BTreeSet<ResourceId>,
72    ) -> Result<Self, VNextError> {
73        if static_allocations.len() + dynamic_descriptors.len() > MAX_EXECUTION_PLAN_RESOURCE_ROWS {
74            return Err(invalid_plan(format!(
75                "execution plan resource rows exceed {MAX_EXECUTION_PLAN_RESOURCE_ROWS}"
76            )));
77        }
78        let usable_capacity_bytes = policy_capacity_bytes
79            .checked_sub(reserve_bytes)
80            .ok_or_else(|| invalid_plan("memory reserve exceeds the policy capacity"))?;
81        static_allocations.sort_by(|left, right| left.resource_id.cmp(&right.resource_id));
82        dynamic_descriptors
83            .sort_by(|left, right| left.base_resource_id.cmp(&right.base_resource_id));
84        for resource_id in retained_completion_resources {
85            let descriptor = dynamic_descriptors
86                .iter()
87                .find(|descriptor| &descriptor.base_resource_id == resource_id)
88                .ok_or_else(|| {
89                    invalid_plan(format!(
90                        "retained completion resource `{resource_id}` has no dynamic descriptor"
91                    ))
92                })?;
93            if descriptor.usage != BufferUsage::Activations
94                || !matches!(
95                    descriptor.lifetime,
96                    AllocationLifetime::Step | AllocationLifetime::Request
97                )
98            {
99                return Err(invalid_plan(format!(
100                    "retained completion resource `{resource_id}` is not a readable activation"
101                )));
102            }
103        }
104        let static_bytes = static_allocations
105            .iter()
106            .try_fold(0_u64, |total, allocation| {
107                total
108                    .checked_add(allocation.size_bytes)
109                    .ok_or_else(|| invalid_plan("static memory total overflows u64"))
110            })?;
111        let dynamic_capacity_bytes = usable_capacity_bytes
112            .checked_sub(static_bytes)
113            .ok_or_else(|| invalid_plan("static memory exceeds usable capacity"))?;
114        let base_dynamic_pools = Self::derive_dynamic_pools_with_completion_retention(
115            &dynamic_descriptors,
116            nodes,
117            dynamic_capacity_bytes,
118            retained_completion_resources,
119        )?;
120        let reusable_execution = reusable_execution_policy
121            .map(|policy| {
122                Self::derive_reusable_execution(
123                    policy,
124                    nodes.len(),
125                    &dynamic_descriptors,
126                    &base_dynamic_pools,
127                )
128            })
129            .transpose()?;
130        let reusable_workspace_ceilings = reusable_execution
131            .as_ref()
132            .map(ReusableExecutionMemoryPlan::pool_workspace_ceilings)
133            .transpose()?
134            .unwrap_or_default();
135        let dynamic_pools = Self::derive_dynamic_pools_with_reusable(
136            &dynamic_descriptors,
137            nodes,
138            dynamic_capacity_bytes,
139            &reusable_workspace_ceilings,
140            retained_completion_resources,
141        )?;
142        let (invocation_liveness_mode, invocation_liveness) =
143            Self::summarize_pool_invocation_liveness(&dynamic_pools)?;
144        let minimum_request_bytes = dynamic_pools.iter().try_fold(0_u64, |total, pool| {
145            total
146                .checked_add(pool.minimum_request_bytes)
147                .ok_or_else(|| invalid_plan("minimum request bytes overflow u64"))
148        })?;
149        let minimum_sequence_bytes = dynamic_pools.iter().try_fold(0_u64, |total, pool| {
150            total
151                .checked_add(pool.minimum_sequence_bytes)
152                .ok_or_else(|| invalid_plan("minimum sequence bytes overflow u64"))
153        })?;
154        let minimum_step_bytes = dynamic_pools.iter().try_fold(0_u64, |total, pool| {
155            total
156                .checked_add(pool.minimum_step_bytes)
157                .ok_or_else(|| invalid_plan("minimum step bytes overflow u64"))
158        })?;
159        let minimum_invocation_peak_bytes =
160            dynamic_pools.iter().try_fold(0_u64, |total, pool| {
161                total
162                    .checked_add(pool.minimum_invocation_peak_bytes)
163                    .ok_or_else(|| invalid_plan("invocation pool minimum bytes overflow u64"))
164            })?;
165        let minimum_runnable_request_bytes = minimum_request_bytes
166            .checked_add(minimum_sequence_bytes)
167            .and_then(|bytes| bytes.checked_add(minimum_step_bytes))
168            .and_then(|bytes| bytes.checked_add(minimum_invocation_peak_bytes))
169            .ok_or_else(|| invalid_plan("minimum runnable request bytes overflow u64"))?;
170        let theoretical_dynamic_ceiling =
171            dynamic_pools.iter().try_fold(0_u128, |total, pool| {
172                total
173                    .checked_add(pool.theoretical_ceiling_bytes.get())
174                    .and_then(|bytes| {
175                        bytes.checked_add(u128::from(pool.reusable_workspace_ceiling_bytes))
176                    })
177                    .ok_or_else(|| invalid_plan("dynamic theoretical total overflows u128"))
178            })?;
179        let theoretical_ceiling_bytes = u128::from(static_bytes)
180            .checked_add(theoretical_dynamic_ceiling)
181            .ok_or_else(|| invalid_plan("plan theoretical ceiling overflows u128"))?;
182        let minimum_runnable_bytes = static_bytes
183            .checked_add(minimum_runnable_request_bytes)
184            .ok_or_else(|| invalid_plan("minimum runnable plan bytes overflow u64"))?;
185        if minimum_runnable_bytes > usable_capacity_bytes {
186            return Err(invalid_plan(format!(
187                "minimum runnable plan requires {minimum_runnable_bytes} bytes, exceeding usable capacity {usable_capacity_bytes}"
188            )));
189        }
190        let plan = Self {
191            device_capacity_bytes,
192            policy_capacity_bytes,
193            reserve_bytes,
194            usable_capacity_bytes,
195            maximum_active_sequences,
196            static_bytes,
197            minimum_request_bytes,
198            minimum_sequence_bytes,
199            minimum_step_bytes,
200            minimum_invocation_peak_bytes,
201            minimum_runnable_request_bytes,
202            theoretical_ceiling_bytes: CanonicalU128::new(theoretical_ceiling_bytes),
203            static_allocations,
204            dynamic_descriptors,
205            dynamic_pools,
206            reusable_execution,
207            invocation_liveness_mode,
208            invocation_liveness,
209        };
210        plan.validate()?;
211        Ok(plan)
212    }
213
214    pub(super) fn validate(&self) -> Result<(), VNextError> {
215        validate_active_sequence_ceiling(self.maximum_active_sequences)?;
216        if self.static_allocations.len() + self.dynamic_descriptors.len()
217            > MAX_EXECUTION_PLAN_RESOURCE_ROWS
218        {
219            return Err(invalid_plan(format!(
220                "execution plan resource rows exceed {MAX_EXECUTION_PLAN_RESOURCE_ROWS}"
221            )));
222        }
223        if self
224            .static_allocations
225            .windows(2)
226            .any(|pair| pair[0].resource_id >= pair[1].resource_id)
227            || self
228                .dynamic_descriptors
229                .windows(2)
230                .any(|pair| pair[0].base_resource_id >= pair[1].base_resource_id)
231            || self
232                .dynamic_pools
233                .windows(2)
234                .any(|pair| pair[0].pool_id >= pair[1].pool_id)
235        {
236            return Err(invalid_plan("memory plan resource rows are not canonical"));
237        }
238        if self.device_capacity_bytes == 0
239            || self.policy_capacity_bytes == 0
240            || self.policy_capacity_bytes > self.device_capacity_bytes
241            || self.reserve_bytes >= self.policy_capacity_bytes
242            || self.usable_capacity_bytes
243                != self
244                    .policy_capacity_bytes
245                    .checked_sub(self.reserve_bytes)
246                    .ok_or_else(|| invalid_plan("memory reserve underflows policy capacity"))?
247        {
248            return Err(invalid_plan("memory plan capacity or reserve is invalid"));
249        }
250        let mut resources = BTreeSet::new();
251        let workspace_layout_fingerprint = workspace_storage_layout_fingerprint()?;
252        let static_contiguous_profile = static_contiguous_storage_profile()?;
253        let actual_static =
254            self.static_allocations
255                .iter()
256                .try_fold(0_u64, |total, allocation| {
257                    if !resources.insert(allocation.resource_id.clone())
258                        || allocation.per_instance_bytes == 0
259                        || allocation.instance_stride_bytes
260                            != quantize_storage_bytes(
261                                allocation.per_instance_bytes,
262                                allocation.alignment_bytes,
263                                allocation.storage.profile(),
264                            )?
265                        || allocation.instance_count != 1
266                        || allocation.size_bytes != allocation.instance_stride_bytes
267                        || allocation.lifetime != AllocationLifetime::Plan
268                        || match &allocation.kind {
269                            AllocationKind::Value => {
270                                allocation.storage.profile() != static_contiguous_profile
271                            }
272                            AllocationKind::Persistent { .. } => {
273                                allocation.usage != BufferUsage::Persistent
274                                    || allocation.element_type != ElementType::U8
275                                    || allocation.storage.logical_layout_fingerprint()
276                                        != workspace_layout_fingerprint
277                            }
278                            AllocationKind::Scratch { .. } | AllocationKind::Binding { .. } => true,
279                        }
280                    {
281                        return Err(invalid_plan(format!(
282                            "static resource `{}` is duplicate or invalid",
283                            allocation.resource_id
284                        )));
285                    }
286                    total
287                        .checked_add(allocation.size_bytes)
288                        .ok_or_else(|| invalid_plan("static allocation total overflows u64"))
289                })?;
290        if actual_static != self.static_bytes {
291            return Err(invalid_plan("static byte total is not core-derived"));
292        }
293        let mut actual_theoretical_dynamic = 0_u128;
294        let dynamic_by_id = self
295            .dynamic_descriptors
296            .iter()
297            .map(|descriptor| (descriptor.base_resource_id.clone(), descriptor))
298            .collect::<BTreeMap<_, _>>();
299        for descriptor in &self.dynamic_descriptors {
300            if !resources.insert(descriptor.base_resource_id.clone()) {
301                return Err(invalid_plan(format!(
302                    "dynamic resource `{}` is duplicated",
303                    descriptor.base_resource_id
304                )));
305            }
306            validate_active_sequence_ceiling(descriptor.theoretical_maximum_instances)?;
307            descriptor.demand.validate()?;
308            if descriptor.lifetime == AllocationLifetime::Plan {
309                return Err(invalid_plan(
310                    "dynamic descriptor cannot have plan-static lifetime",
311                ));
312            }
313            actual_theoretical_dynamic = actual_theoretical_dynamic
314                .checked_add(descriptor.theoretical_maximum_resident_bytes()?)
315                .ok_or_else(|| invalid_plan("dynamic ceiling total overflows u128"))?;
316        }
317        let reusable_workspace_ceilings = self
318            .reusable_execution
319            .as_ref()
320            .map(|plan| {
321                plan.validate_local()?;
322                plan.pool_workspace_ceilings()
323            })
324            .transpose()?
325            .unwrap_or_default();
326        let actual_reusable_workspace =
327            reusable_workspace_ceilings
328                .values()
329                .try_fold(0_u128, |total, bytes| {
330                    total
331                        .checked_add(u128::from(*bytes))
332                        .ok_or_else(|| invalid_plan("reusable workspace total overflows u128"))
333                })?;
334        let actual_theoretical = u128::from(actual_static)
335            .checked_add(actual_theoretical_dynamic)
336            .and_then(|bytes| bytes.checked_add(actual_reusable_workspace))
337            .ok_or_else(|| invalid_plan("theoretical plan ceiling overflows u128"))?;
338        let dynamic_capacity_bytes = self
339            .usable_capacity_bytes
340            .checked_sub(actual_static)
341            .ok_or_else(|| invalid_plan("static memory exceeds usable capacity"))?;
342        let aggregate = Self::validate_dynamic_pools_structure(
343            &self.dynamic_pools,
344            &dynamic_by_id,
345            dynamic_capacity_bytes,
346            &reusable_workspace_ceilings,
347        )?;
348        let (actual_liveness_mode, actual_liveness) =
349            Self::summarize_pool_invocation_liveness(&self.dynamic_pools)?;
350        if actual_liveness_mode != self.invocation_liveness_mode
351            || actual_liveness != self.invocation_liveness
352        {
353            return Err(invalid_plan(
354                "global invocation liveness is not derived from per-pool evidence",
355            ));
356        }
357        let actual_request_minimum = aggregate.minimum_request_bytes;
358        let actual_sequence_minimum = aggregate.minimum_sequence_bytes;
359        let actual_step_minimum = aggregate.minimum_step_bytes;
360        let actual_invocation_peak = aggregate.minimum_invocation_peak_bytes;
361        let actual_minimum = actual_request_minimum
362            .checked_add(actual_sequence_minimum)
363            .and_then(|bytes| bytes.checked_add(actual_step_minimum))
364            .and_then(|bytes| bytes.checked_add(actual_invocation_peak))
365            .ok_or_else(|| invalid_plan("minimum runnable request bytes overflow u64"))?;
366        if aggregate.theoretical_ceiling_bytes != actual_theoretical_dynamic
367            || aggregate.reusable_workspace_ceiling_bytes != actual_reusable_workspace
368            || actual_request_minimum != self.minimum_request_bytes
369            || actual_sequence_minimum != self.minimum_sequence_bytes
370            || actual_step_minimum != self.minimum_step_bytes
371            || actual_invocation_peak != self.minimum_invocation_peak_bytes
372            || actual_minimum != self.minimum_runnable_request_bytes
373            || actual_theoretical != self.theoretical_ceiling_bytes.get()
374            || actual_static
375                .checked_add(actual_minimum)
376                .is_none_or(|minimum| minimum > self.usable_capacity_bytes)
377        {
378            return Err(invalid_plan(
379                "dynamic minimum or theoretical ceiling is not core-derived",
380            ));
381        }
382        Ok(())
383    }
384
385    #[cfg(test)]
386    pub(crate) fn derive_dynamic_pools(
387        dynamic_descriptors: &[DynamicResourceDescriptor],
388        nodes: &[PlanNode],
389        dynamic_capacity_bytes: u64,
390    ) -> Result<Vec<DynamicBackingPoolSpec>, VNextError> {
391        Self::derive_dynamic_pools_with_reusable(
392            dynamic_descriptors,
393            nodes,
394            dynamic_capacity_bytes,
395            &BTreeMap::new(),
396            &BTreeSet::new(),
397        )
398    }
399
400    pub(super) fn derive_dynamic_pools_with_completion_retention(
401        dynamic_descriptors: &[DynamicResourceDescriptor],
402        nodes: &[PlanNode],
403        dynamic_capacity_bytes: u64,
404        retained_completion_resources: &BTreeSet<ResourceId>,
405    ) -> Result<Vec<DynamicBackingPoolSpec>, VNextError> {
406        Self::derive_dynamic_pools_with_reusable(
407            dynamic_descriptors,
408            nodes,
409            dynamic_capacity_bytes,
410            &BTreeMap::new(),
411            retained_completion_resources,
412        )
413    }
414
415    pub(super) fn derive_dynamic_pools_with_reusable(
416        dynamic_descriptors: &[DynamicResourceDescriptor],
417        nodes: &[PlanNode],
418        dynamic_capacity_bytes: u64,
419        reusable_workspace_ceilings: &BTreeMap<DynamicBackingPoolId, u64>,
420        retained_completion_resources: &BTreeSet<ResourceId>,
421    ) -> Result<Vec<DynamicBackingPoolSpec>, VNextError> {
422        let mut groups = BTreeMap::<
423            DynamicBackingPoolId,
424            (PoolCompatibilityKey, Vec<&DynamicResourceDescriptor>),
425        >::new();
426        for descriptor in dynamic_descriptors {
427            let compatibility = PoolCompatibilityKey::new(
428                &descriptor.storage,
429                descriptor.usage,
430                descriptor.element_type,
431                descriptor.alignment_bytes,
432            )?;
433            let expected_pool_id = DynamicBackingPoolId::from_compatibility(&compatibility)?;
434            if descriptor.pool_id != expected_pool_id {
435                return Err(invalid_plan(format!(
436                    "dynamic resource `{}` has a non-derived backing pool id",
437                    descriptor.base_resource_id
438                )));
439            }
440            match groups.entry(expected_pool_id) {
441                std::collections::btree_map::Entry::Vacant(entry) => {
442                    entry.insert((compatibility, vec![descriptor]));
443                }
444                std::collections::btree_map::Entry::Occupied(mut entry) => {
445                    if entry.get().0 != compatibility {
446                        return Err(invalid_plan(
447                            "dynamic backing pool hash collision has incompatible contracts",
448                        ));
449                    }
450                    entry.get_mut().1.push(descriptor);
451                }
452            }
453        }
454
455        let pools = groups
456            .into_values()
457            .map(|(compatibility, mut descriptors)| {
458                descriptors
459                    .sort_by(|left, right| left.base_resource_id.cmp(&right.base_resource_id));
460                let resource_ids = descriptors
461                    .iter()
462                    .map(|descriptor| descriptor.base_resource_id.clone())
463                    .collect::<Vec<_>>();
464                let minimum_request_bytes = minimum_for_lifetime(
465                    &descriptors,
466                    AllocationLifetime::Request,
467                    "pool request minimum",
468                )?;
469                let minimum_sequence_bytes = minimum_for_lifetime(
470                    &descriptors,
471                    AllocationLifetime::Sequence,
472                    "pool sequence minimum",
473                )?;
474                let step_resource_slots = Self::derive_pool_step_slots(
475                    nodes,
476                    &descriptors,
477                    retained_completion_resources,
478                )?;
479                let minimum_step_bytes = Self::step_slot_bytes(
480                    &step_resource_slots,
481                    &descriptors,
482                    false,
483                    "pool step minimum",
484                )?;
485                let theoretical_ceiling_bytes =
486                    descriptors.iter().try_fold(0_u128, |total, descriptor| {
487                        total
488                            .checked_add(descriptor.theoretical_maximum_resident_bytes()?)
489                            .ok_or_else(|| {
490                                invalid_plan("dynamic pool theoretical ceiling overflows u128")
491                            })
492                    })?;
493                let (invocation_liveness_mode, invocation_liveness, invocation_peak) =
494                    Self::derive_pool_invocation_liveness(nodes, &descriptors)?;
495                let pool_id = DynamicBackingPoolId::from_compatibility(&compatibility)?;
496                let reusable_workspace_ceiling_bytes = reusable_workspace_ceilings
497                    .get(&pool_id)
498                    .copied()
499                    .unwrap_or(0);
500                DynamicBackingPoolSpec::from_core(
501                    compatibility,
502                    resource_ids,
503                    minimum_request_bytes,
504                    minimum_sequence_bytes,
505                    minimum_step_bytes,
506                    invocation_peak,
507                    step_resource_slots,
508                    theoretical_ceiling_bytes,
509                    reusable_workspace_ceiling_bytes,
510                    dynamic_capacity_bytes,
511                    invocation_liveness_mode,
512                    invocation_liveness,
513                )
514            })
515            .collect::<Result<Vec<_>, VNextError>>()?;
516        if reusable_workspace_ceilings
517            .keys()
518            .any(|pool_id| !pools.iter().any(|pool| &pool.pool_id == pool_id))
519        {
520            return Err(invalid_plan(
521                "reusable workspace ceiling references an unknown dynamic pool",
522            ));
523        }
524        Ok(pools)
525    }
526
527    pub(super) fn derive_reusable_execution(
528        policy: &ReusableExecutionPolicy,
529        node_count: usize,
530        dynamic_descriptors: &[DynamicResourceDescriptor],
531        base_pools: &[DynamicBackingPoolSpec],
532    ) -> Result<ReusableExecutionMemoryPlan, VNextError> {
533        policy.validate()?;
534        let node_count = u64::try_from(node_count)
535            .ok()
536            .filter(|count| *count > 0)
537            .ok_or_else(|| invalid_plan("reusable execution requires at least one plan node"))?;
538        let startup_capture_case_count = u64::try_from(policy.startup_capture_case_count())
539            .map_err(|_| {
540                invalid_plan("reusable execution startup capture case count exceeds u64")
541            })?;
542        let maximum_device_executables = node_count
543            .checked_mul(startup_capture_case_count)
544            .and_then(|count| count.checked_mul(u64::from(policy.maximum_reusable_lanes())))
545            .ok_or_else(|| invalid_plan("reusable device executable count overflows u64"))?;
546        let descriptors = dynamic_descriptors
547            .iter()
548            .map(|descriptor| (descriptor.base_resource_id.clone(), descriptor))
549            .collect::<BTreeMap<_, _>>();
550        let mut buckets = Vec::with_capacity(policy.buckets().len());
551        for bucket in policy.buckets() {
552            let capacity = bucket.capacity();
553            let shape = DynamicResourceShape::from_validated(
554                capacity.maximum_sequences(),
555                capacity.maximum_tokens(),
556                capacity.maximum_pages(),
557            );
558            let mut pool_budgets = Vec::new();
559            for pool in base_pools {
560                let step_bytes = Self::reusable_step_bytes_for_shape(pool, &descriptors, shape)?;
561                let invocation_bytes =
562                    Self::reusable_invocation_bytes_for_shape(pool, &descriptors, shape)?;
563                if step_bytes != 0 || invocation_bytes != 0 {
564                    pool_budgets.push(ReusablePoolWorkspaceBudget::new(
565                        pool.pool_id.clone(),
566                        step_bytes,
567                        invocation_bytes,
568                    )?);
569                }
570            }
571            buckets.push(ResolvedReusableExecutionBucket::new(
572                bucket.clone(),
573                pool_budgets,
574            )?);
575        }
576        ReusableExecutionMemoryPlan::new_with_program_policy(
577            policy.maximum_reusable_lanes(),
578            maximum_device_executables,
579            buckets,
580            policy.program_policy().cloned(),
581        )
582    }
583
584    fn reusable_step_bytes_for_shape(
585        pool: &DynamicBackingPoolSpec,
586        descriptors: &BTreeMap<ResourceId, &DynamicResourceDescriptor>,
587        shape: DynamicResourceShape,
588    ) -> Result<u64, VNextError> {
589        pool.step_resource_slots
590            .iter()
591            .try_fold(0_u64, |total, slot| {
592                let slot_bytes =
593                    slot.resource_ids
594                        .iter()
595                        .try_fold(0_u64, |maximum, resource_id| {
596                            let descriptor = descriptors.get(resource_id).ok_or_else(|| {
597                                invalid_plan(
598                                    "reusable Step slot references a missing dynamic descriptor",
599                                )
600                            })?;
601                            Ok::<u64, VNextError>(
602                                maximum.max(descriptor.evaluate_request_bytes_for_shape(shape)?),
603                            )
604                        })?;
605                total
606                    .checked_add(slot_bytes)
607                    .ok_or_else(|| invalid_plan("reusable Step workspace budget overflows u64"))
608            })
609    }
610
611    fn reusable_invocation_bytes_for_shape(
612        pool: &DynamicBackingPoolSpec,
613        descriptors: &BTreeMap<ResourceId, &DynamicResourceDescriptor>,
614        shape: DynamicResourceShape,
615    ) -> Result<u64, VNextError> {
616        let row_bytes = |row: &InvocationResourceLiveness| {
617            row.resource_ids
618                .iter()
619                .try_fold(0_u64, |total, resource_id| {
620                    total
621                        .checked_add(
622                            descriptors
623                                .get(resource_id)
624                                .ok_or_else(|| {
625                                    invalid_plan(
626                                        "reusable invocation row references a missing descriptor",
627                                    )
628                                })?
629                                .evaluate_request_bytes_for_shape(shape)?,
630                        )
631                        .ok_or_else(|| invalid_plan("reusable invocation row budget overflows u64"))
632                })
633        };
634        match pool.invocation_liveness_mode {
635            InvocationLivenessMode::NoInvocationResources => Ok(0),
636            InvocationLivenessMode::TotalOrderReuse => pool
637                .invocation_liveness
638                .iter()
639                .try_fold(0_u64, |maximum, row| {
640                    Ok::<u64, VNextError>(maximum.max(row_bytes(row)?))
641                }),
642            InvocationLivenessMode::ConservativeConcurrent => pool
643                .invocation_liveness
644                .iter()
645                .try_fold(0_u64, |total, row| {
646                    total.checked_add(row_bytes(row)?).ok_or_else(|| {
647                        invalid_plan("reusable concurrent invocation budget overflows u64")
648                    })
649                }),
650        }
651    }
652
653    pub(super) fn derive_pool_step_slots(
654        nodes: &[PlanNode],
655        descriptors: &[&DynamicResourceDescriptor],
656        retained_completion_resources: &BTreeSet<ResourceId>,
657    ) -> Result<Vec<StepResourceSlot>, VNextError> {
658        struct Interval<'a> {
659            resource_id: &'a ResourceId,
660            first_user: usize,
661            last_user: usize,
662            reusable: bool,
663        }
664
665        let nodes_by_id = nodes
666            .iter()
667            .map(|node| (node.id.clone(), node))
668            .collect::<BTreeMap<_, _>>();
669        let mut intervals = descriptors
670            .iter()
671            .filter(|descriptor| descriptor.lifetime == AllocationLifetime::Step)
672            .map(|descriptor| {
673                let users = nodes
674                    .iter()
675                    .enumerate()
676                    .filter_map(|(index, node)| {
677                        node.resources
678                            .contains(&descriptor.base_resource_id)
679                            .then_some(index)
680                    })
681                    .collect::<Vec<_>>();
682                let first_user = users.first().copied().ok_or_else(|| {
683                    invalid_plan(format!(
684                        "step resource `{}` is not referenced by a plan node",
685                        descriptor.base_resource_id
686                    ))
687                })?;
688                let last_user = users.last().copied().ok_or_else(|| {
689                    invalid_plan(format!(
690                        "step resource `{}` is not referenced by a plan node",
691                        descriptor.base_resource_id
692                    ))
693                })?;
694                Ok(Interval {
695                    resource_id: &descriptor.base_resource_id,
696                    first_user,
697                    last_user,
698                    // Completion diagnostics read after the terminal fence, so
699                    // a retained activation cannot share a liveness slot.
700                    reusable: Self::is_reusable_step_activation(descriptor)
701                        && !retained_completion_resources.contains(&descriptor.base_resource_id),
702                })
703            })
704            .collect::<Result<Vec<_>, VNextError>>()?;
705        intervals.sort_by(|left, right| {
706            (left.first_user, left.last_user, left.resource_id).cmp(&(
707                right.first_user,
708                right.last_user,
709                right.resource_id,
710            ))
711        });
712
713        let ordered_without_overlap = |left: &Interval<'_>, right: &Interval<'_>| {
714            let left_before = left.last_user < right.first_user
715                && node_completion_precedes(
716                    &nodes_by_id,
717                    &nodes[left.last_user].id,
718                    &nodes[right.first_user].id,
719                )?;
720            let right_before = right.last_user < left.first_user
721                && node_completion_precedes(
722                    &nodes_by_id,
723                    &nodes[right.last_user].id,
724                    &nodes[left.first_user].id,
725                )?;
726            Ok::<bool, VNextError>(left_before || right_before)
727        };
728
729        let mut slots = Vec::<Vec<Interval<'_>>>::new();
730        for interval in intervals {
731            if !interval.reusable {
732                slots.push(vec![interval]);
733                continue;
734            }
735            let mut reusable_slot = None;
736            for (index, slot) in slots.iter().enumerate() {
737                let mut reusable = slot.iter().all(|member| member.reusable);
738                if reusable {
739                    for member in slot {
740                        reusable = ordered_without_overlap(member, &interval)?;
741                        if !reusable {
742                            break;
743                        }
744                    }
745                }
746                if reusable {
747                    reusable_slot = Some(index);
748                    break;
749                }
750            }
751            if let Some(index) = reusable_slot {
752                slots[index].push(interval);
753            } else {
754                slots.push(vec![interval]);
755            }
756        }
757        let mut slots = slots
758            .into_iter()
759            .map(|slot| {
760                let resource_ids = slot
761                    .into_iter()
762                    .map(|interval| interval.resource_id.clone())
763                    .collect::<Vec<_>>();
764                if resource_ids.len() == 1 {
765                    Ok(StepResourceSlot::dedicated(
766                        resource_ids
767                            .into_iter()
768                            .next()
769                            .expect("single resource slot"),
770                    ))
771                } else {
772                    StepResourceSlot::ordered_single_fence_wave(resource_ids)
773                }
774            })
775            .collect::<Result<Vec<_>, VNextError>>()?;
776        slots.sort_by(|left, right| left.resource_ids.cmp(&right.resource_ids));
777        Ok(slots)
778    }
779
780    fn is_reusable_step_activation(descriptor: &DynamicResourceDescriptor) -> bool {
781        descriptor.lifetime == AllocationLifetime::Step
782            && descriptor.usage == BufferUsage::Activations
783            && matches!(descriptor.kind, AllocationKind::Value)
784            && matches!(descriptor.demand, DynamicResourceDemand::Tokens { .. })
785    }
786
787    fn step_slot_bytes(
788        slots: &[StepResourceSlot],
789        descriptors: &[&DynamicResourceDescriptor],
790        theoretical: bool,
791        overflow_context: &str,
792    ) -> Result<u64, VNextError> {
793        let descriptors = descriptors
794            .iter()
795            .map(|descriptor| (descriptor.base_resource_id.clone(), *descriptor))
796            .collect::<BTreeMap<_, _>>();
797        slots.iter().try_fold(0_u64, |total, slot| {
798            let slot_bytes = slot
799                .resource_ids
800                .iter()
801                .try_fold(0_u64, |maximum, resource_id| {
802                    let descriptor = descriptors.get(resource_id).ok_or_else(|| {
803                        invalid_plan("step slot references a missing dynamic descriptor")
804                    })?;
805                    let bytes = if theoretical {
806                        descriptor.theoretical_maximum_request_bytes()?
807                    } else {
808                        descriptor.minimum_request_bytes()?
809                    };
810                    Ok::<u64, VNextError>(maximum.max(bytes))
811                })?;
812            total
813                .checked_add(slot_bytes)
814                .ok_or_else(|| invalid_plan(format!("{overflow_context} overflows u64")))
815        })
816    }
817
818    pub(super) fn derive_pool_invocation_liveness(
819        nodes: &[PlanNode],
820        descriptors: &[&DynamicResourceDescriptor],
821    ) -> Result<(InvocationLivenessMode, Vec<InvocationResourceLiveness>, u64), VNextError> {
822        let invocation_ids = descriptors
823            .iter()
824            .filter(|descriptor| descriptor.lifetime == AllocationLifetime::Invocation)
825            .map(|descriptor| descriptor.base_resource_id.clone())
826            .collect::<BTreeSet<_>>();
827        if invocation_ids.is_empty() {
828            return Ok((InvocationLivenessMode::NoInvocationResources, Vec::new(), 0));
829        }
830        let descriptor_by_id = descriptors
831            .iter()
832            .map(|descriptor| (descriptor.base_resource_id.clone(), *descriptor))
833            .collect::<BTreeMap<_, _>>();
834        let mut covered = BTreeSet::new();
835        let liveness_in_execution_order = nodes
836            .iter()
837            .filter_map(|node| {
838                let resource_ids = node
839                    .resources
840                    .iter()
841                    .filter(|resource_id| invocation_ids.contains(*resource_id))
842                    .cloned()
843                    .collect::<Vec<_>>();
844                covered.extend(resource_ids.iter().cloned());
845                (!resource_ids.is_empty()).then(|| InvocationResourceLiveness {
846                    node_id: node.id.clone(),
847                    resource_ids,
848                })
849            })
850            .collect::<Vec<_>>();
851        if covered != invocation_ids {
852            return Err(invalid_plan(
853                "pool node liveness does not cover every invocation resource",
854            ));
855        }
856        let nodes_by_id = nodes
857            .iter()
858            .map(|node| (node.id.clone(), node))
859            .collect::<BTreeMap<_, _>>();
860        let contains_binding = descriptors.iter().any(|descriptor| {
861            descriptor.lifetime == AllocationLifetime::Invocation
862                && matches!(descriptor.kind, AllocationKind::Binding { .. })
863        });
864        let total_ordered = !contains_binding
865            && liveness_in_execution_order
866                .windows(2)
867                .try_fold(true, |ordered, pair| {
868                    Ok::<bool, VNextError>(
869                        ordered
870                            && node_completion_precedes(
871                                &nodes_by_id,
872                                &pair[0].node_id,
873                                &pair[1].node_id,
874                            )?,
875                    )
876                })?;
877        let row_bytes = |row: &InvocationResourceLiveness| {
878            row.resource_ids
879                .iter()
880                .try_fold(0_u64, |total, resource_id| {
881                    total
882                        .checked_add(
883                            descriptor_by_id
884                                .get(resource_id)
885                                .ok_or_else(|| {
886                                    invalid_plan("pool liveness references an unknown descriptor")
887                                })?
888                                .minimum_request_bytes()?,
889                        )
890                        .ok_or_else(|| invalid_plan("pool invocation row bytes overflow u64"))
891                })
892        };
893        let invocation_peak = if total_ordered {
894            liveness_in_execution_order
895                .iter()
896                .try_fold(0_u64, |peak, row| {
897                    Ok::<u64, VNextError>(peak.max(row_bytes(row)?))
898                })?
899        } else {
900            liveness_in_execution_order
901                .iter()
902                .try_fold(0_u64, |total, row| {
903                    total.checked_add(row_bytes(row)?).ok_or_else(|| {
904                        invalid_plan("pool concurrent invocation bytes overflow u64")
905                    })
906                })?
907        };
908        let mut invocation_liveness = liveness_in_execution_order;
909        invocation_liveness.sort_by(|left, right| left.node_id.cmp(&right.node_id));
910        Ok((
911            if total_ordered {
912                InvocationLivenessMode::TotalOrderReuse
913            } else {
914                InvocationLivenessMode::ConservativeConcurrent
915            },
916            invocation_liveness,
917            invocation_peak,
918        ))
919    }
920
921    pub(super) fn validate_dynamic_pools_structure(
922        pools: &[DynamicBackingPoolSpec],
923        descriptors: &BTreeMap<ResourceId, &DynamicResourceDescriptor>,
924        dynamic_capacity_bytes: u64,
925        reusable_workspace_ceilings: &BTreeMap<DynamicBackingPoolId, u64>,
926    ) -> Result<PoolAggregateEvidence, VNextError> {
927        let mut expected_members =
928            BTreeMap::<DynamicBackingPoolId, (PoolCompatibilityKey, Vec<ResourceId>)>::new();
929        for descriptor in descriptors.values() {
930            let compatibility = PoolCompatibilityKey::new(
931                &descriptor.storage,
932                descriptor.usage,
933                descriptor.element_type,
934                descriptor.alignment_bytes,
935            )?;
936            let expected_id = DynamicBackingPoolId::from_compatibility(&compatibility)?;
937            if descriptor.pool_id != expected_id {
938                return Err(invalid_plan(
939                    "dynamic descriptor has a non-derived pool identity",
940                ));
941            }
942            match expected_members.entry(expected_id) {
943                std::collections::btree_map::Entry::Vacant(entry) => {
944                    entry.insert((compatibility, vec![descriptor.base_resource_id.clone()]));
945                }
946                std::collections::btree_map::Entry::Occupied(mut entry) => {
947                    if entry.get().0 != compatibility {
948                        return Err(invalid_plan(
949                            "dynamic pool hash collision has incompatible descriptors",
950                        ));
951                    }
952                    entry.get_mut().1.push(descriptor.base_resource_id.clone());
953                }
954            }
955        }
956        if pools.len() != expected_members.len() {
957            return Err(invalid_plan(
958                "dynamic backing pool count is not derived from descriptors",
959            ));
960        }
961        if reusable_workspace_ceilings
962            .keys()
963            .any(|pool_id| !expected_members.contains_key(pool_id))
964        {
965            return Err(invalid_plan(
966                "reusable workspace ceiling references an unknown dynamic pool",
967            ));
968        }
969        let mut aggregate = PoolAggregateEvidence::default();
970        for pool in pools {
971            pool.validate_local()?;
972            let (compatibility, resource_ids) = expected_members
973                .remove(&pool.pool_id)
974                .ok_or_else(|| invalid_plan("dynamic backing pool has no descriptor members"))?;
975            if pool.compatibility != compatibility || pool.resource_ids != resource_ids {
976                return Err(invalid_plan(
977                    "dynamic backing pool compatibility or membership is not core-derived",
978                ));
979            }
980            let members =
981                pool.resource_ids
982                    .iter()
983                    .map(|resource_id| {
984                        descriptors.get(resource_id).copied().ok_or_else(|| {
985                            invalid_plan("dynamic pool member descriptor is missing")
986                        })
987                    })
988                    .collect::<Result<Vec<_>, VNextError>>()?;
989            let request = minimum_for_lifetime(
990                &members,
991                AllocationLifetime::Request,
992                "pool request minimum",
993            )?;
994            let sequence = minimum_for_lifetime(
995                &members,
996                AllocationLifetime::Sequence,
997                "pool sequence minimum",
998            )?;
999            let expected_step_resources = members
1000                .iter()
1001                .filter(|descriptor| descriptor.lifetime == AllocationLifetime::Step)
1002                .map(|descriptor| descriptor.base_resource_id.clone())
1003                .collect::<BTreeSet<_>>();
1004            let actual_step_resources = pool
1005                .step_resource_slots
1006                .iter()
1007                .flat_map(|slot| slot.resource_ids.iter().cloned())
1008                .collect::<BTreeSet<_>>();
1009            if actual_step_resources != expected_step_resources {
1010                return Err(invalid_plan(
1011                    "step resource slots do not cover exactly the pool's Step descriptors",
1012                ));
1013            }
1014            for slot in &pool.step_resource_slots {
1015                slot.validate()?;
1016                if slot.kind == StepResourceSlotKind::OrderedSingleFenceStepWave
1017                    && slot.resource_ids.iter().any(|resource_id| {
1018                        descriptors
1019                            .get(resource_id)
1020                            .is_none_or(|descriptor| !Self::is_reusable_step_activation(descriptor))
1021                    })
1022                {
1023                    return Err(invalid_plan(
1024                        "shared Step slot contains a non-transient activation resource",
1025                    ));
1026                }
1027            }
1028            let step = Self::step_slot_bytes(
1029                &pool.step_resource_slots,
1030                &members,
1031                false,
1032                "pool step minimum",
1033            )?;
1034            let theoretical = members.iter().try_fold(0_u128, |total, descriptor| {
1035                total
1036                    .checked_add(descriptor.theoretical_maximum_resident_bytes()?)
1037                    .ok_or_else(|| invalid_plan("pool theoretical ceiling overflows u128"))
1038            })?;
1039            let invocation_ids = members
1040                .iter()
1041                .filter(|descriptor| descriptor.lifetime == AllocationLifetime::Invocation)
1042                .map(|descriptor| descriptor.base_resource_id.clone())
1043                .collect::<BTreeSet<_>>();
1044            let invocation_peak = validate_pool_liveness_rows(
1045                pool.invocation_liveness_mode,
1046                &pool.invocation_liveness,
1047                &invocation_ids,
1048                descriptors,
1049            )?;
1050            let reusable_workspace_ceiling_bytes = reusable_workspace_ceilings
1051                .get(&pool.pool_id)
1052                .copied()
1053                .unwrap_or(0);
1054            let combined_ceiling = theoretical
1055                .checked_add(u128::from(reusable_workspace_ceiling_bytes))
1056                .ok_or_else(|| invalid_plan("pool combined ceiling overflows u128"))?;
1057            let maximum_resident =
1058                u64::try_from(combined_ceiling.min(u128::from(dynamic_capacity_bytes)))
1059                    .map_err(|_| invalid_plan("pool resident ceiling exceeds u64"))?;
1060            if pool.minimum_request_bytes != request
1061                || pool.minimum_sequence_bytes != sequence
1062                || pool.minimum_step_bytes != step
1063                || pool.minimum_invocation_peak_bytes != invocation_peak
1064                || pool.theoretical_ceiling_bytes.get() != theoretical
1065                || pool.reusable_workspace_ceiling_bytes != reusable_workspace_ceiling_bytes
1066                || pool.provisioning.maximum_resident_bytes != maximum_resident
1067            {
1068                return Err(invalid_plan(
1069                    "dynamic backing pool bounds or liveness are not core-derived",
1070                ));
1071            }
1072            aggregate.add(pool)?;
1073        }
1074        if !expected_members.is_empty() {
1075            return Err(invalid_plan(
1076                "dynamic descriptors are missing canonical backing pools",
1077            ));
1078        }
1079        Ok(aggregate)
1080    }
1081
1082    pub(super) fn summarize_pool_invocation_liveness(
1083        pools: &[DynamicBackingPoolSpec],
1084    ) -> Result<(InvocationLivenessMode, Vec<InvocationResourceLiveness>), VNextError> {
1085        let mut by_node = BTreeMap::<NodeId, BTreeSet<ResourceId>>::new();
1086        let mut has_invocation = false;
1087        let mut all_total_ordered = true;
1088        for pool in pools {
1089            match pool.invocation_liveness_mode {
1090                InvocationLivenessMode::NoInvocationResources => {}
1091                InvocationLivenessMode::TotalOrderReuse => has_invocation = true,
1092                InvocationLivenessMode::ConservativeConcurrent => {
1093                    has_invocation = true;
1094                    all_total_ordered = false;
1095                }
1096            }
1097            for row in &pool.invocation_liveness {
1098                by_node
1099                    .entry(row.node_id.clone())
1100                    .or_default()
1101                    .extend(row.resource_ids.iter().cloned());
1102            }
1103        }
1104        let liveness = by_node
1105            .into_iter()
1106            .map(|(node_id, resource_ids)| InvocationResourceLiveness {
1107                node_id,
1108                resource_ids: resource_ids.into_iter().collect(),
1109            })
1110            .collect::<Vec<_>>();
1111        let reference_count = liveness.iter().try_fold(0_usize, |total, row| {
1112            total.checked_add(row.resource_ids.len())
1113        });
1114        if reference_count.is_none_or(|count| count > MAX_EXECUTION_PLAN_RESOURCE_ROWS) {
1115            return Err(invalid_plan(
1116                "invocation liveness reference count is invalid",
1117            ));
1118        }
1119        Ok((
1120            if !has_invocation {
1121                InvocationLivenessMode::NoInvocationResources
1122            } else if all_total_ordered {
1123                InvocationLivenessMode::TotalOrderReuse
1124            } else {
1125                InvocationLivenessMode::ConservativeConcurrent
1126            },
1127            liveness,
1128        ))
1129    }
1130
1131    pub const fn device_capacity_bytes(&self) -> u64 {
1132        self.device_capacity_bytes
1133    }
1134
1135    pub const fn policy_capacity_bytes(&self) -> u64 {
1136        self.policy_capacity_bytes
1137    }
1138
1139    pub const fn reserve_bytes(&self) -> u64 {
1140        self.reserve_bytes
1141    }
1142
1143    pub const fn usable_capacity_bytes(&self) -> u64 {
1144        self.usable_capacity_bytes
1145    }
1146
1147    pub const fn maximum_active_sequences(&self) -> u32 {
1148        self.maximum_active_sequences
1149    }
1150
1151    pub const fn capacity_bytes(&self) -> u64 {
1152        self.usable_capacity_bytes
1153    }
1154
1155    pub const fn static_bytes(&self) -> u64 {
1156        self.static_bytes
1157    }
1158
1159    pub const fn minimum_request_bytes(&self) -> u64 {
1160        self.minimum_request_bytes
1161    }
1162
1163    pub const fn minimum_sequence_bytes(&self) -> u64 {
1164        self.minimum_sequence_bytes
1165    }
1166
1167    pub const fn minimum_step_bytes(&self) -> u64 {
1168        self.minimum_step_bytes
1169    }
1170
1171    pub const fn minimum_invocation_peak_bytes(&self) -> u64 {
1172        self.minimum_invocation_peak_bytes
1173    }
1174
1175    pub const fn minimum_runnable_request_bytes(&self) -> u64 {
1176        self.minimum_runnable_request_bytes
1177    }
1178
1179    pub const fn invocation_liveness_mode(&self) -> InvocationLivenessMode {
1180        self.invocation_liveness_mode
1181    }
1182
1183    pub fn invocation_liveness(&self) -> &[InvocationResourceLiveness] {
1184        &self.invocation_liveness
1185    }
1186
1187    /// Conservative checked evidence across static allocations, live provider
1188    /// formula maxima, reusable execution workspace, and the protocol ceiling.
1189    /// It is never itself a reservation, admission target, or performance claim.
1190    pub fn theoretical_ceiling_bytes(&self) -> u128 {
1191        self.theoretical_ceiling_bytes.get()
1192    }
1193
1194    pub fn static_allocations(&self) -> &[ResourceAllocation] {
1195        &self.static_allocations
1196    }
1197
1198    pub fn dynamic_descriptors(&self) -> &[DynamicResourceDescriptor] {
1199        &self.dynamic_descriptors
1200    }
1201
1202    pub fn dynamic_pools(&self) -> &[DynamicBackingPoolSpec] {
1203        &self.dynamic_pools
1204    }
1205
1206    pub fn reusable_execution(&self) -> Option<&ReusableExecutionMemoryPlan> {
1207        self.reusable_execution.as_ref()
1208    }
1209
1210    pub fn static_buffer_requests(&self) -> Result<Vec<BufferRequest>, VNextError> {
1211        self.static_allocations
1212            .iter()
1213            .map(ResourceAllocation::buffer_request)
1214            .collect()
1215    }
1216}
1217
1218#[derive(Deserialize)]
1219#[serde(deny_unknown_fields)]
1220pub(super) struct MemoryPlanWire {
1221    pub(super) device_capacity_bytes: u64,
1222    pub(super) policy_capacity_bytes: u64,
1223    pub(super) reserve_bytes: u64,
1224    pub(super) usable_capacity_bytes: u64,
1225    pub(super) maximum_active_sequences: u32,
1226    pub(super) static_bytes: u64,
1227    pub(super) minimum_request_bytes: u64,
1228    pub(super) minimum_sequence_bytes: u64,
1229    pub(super) minimum_step_bytes: u64,
1230    pub(super) minimum_invocation_peak_bytes: u64,
1231    pub(super) minimum_runnable_request_bytes: u64,
1232    pub(super) theoretical_ceiling_bytes: CanonicalU128,
1233    pub(super) static_allocations: Vec<ResourceAllocation>,
1234    pub(super) dynamic_descriptors: Vec<DynamicResourceDescriptor>,
1235    pub(super) dynamic_pools: Vec<DynamicBackingPoolSpec>,
1236    pub(super) reusable_execution: Option<ReusableExecutionMemoryPlan>,
1237    pub(super) invocation_liveness_mode: InvocationLivenessMode,
1238    pub(super) invocation_liveness: Vec<InvocationResourceLiveness>,
1239}
1240
1241impl<'de> Deserialize<'de> for MemoryPlan {
1242    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1243    where
1244        D: Deserializer<'de>,
1245    {
1246        let wire = MemoryPlanWire::deserialize(deserializer)?;
1247        let plan = Self {
1248            device_capacity_bytes: wire.device_capacity_bytes,
1249            policy_capacity_bytes: wire.policy_capacity_bytes,
1250            reserve_bytes: wire.reserve_bytes,
1251            usable_capacity_bytes: wire.usable_capacity_bytes,
1252            maximum_active_sequences: wire.maximum_active_sequences,
1253            static_bytes: wire.static_bytes,
1254            minimum_request_bytes: wire.minimum_request_bytes,
1255            minimum_sequence_bytes: wire.minimum_sequence_bytes,
1256            minimum_step_bytes: wire.minimum_step_bytes,
1257            minimum_invocation_peak_bytes: wire.minimum_invocation_peak_bytes,
1258            minimum_runnable_request_bytes: wire.minimum_runnable_request_bytes,
1259            theoretical_ceiling_bytes: wire.theoretical_ceiling_bytes,
1260            static_allocations: wire.static_allocations,
1261            dynamic_descriptors: wire.dynamic_descriptors,
1262            dynamic_pools: wire.dynamic_pools,
1263            reusable_execution: wire.reusable_execution,
1264            invocation_liveness_mode: wire.invocation_liveness_mode,
1265            invocation_liveness: wire.invocation_liveness,
1266        };
1267        plan.validate().map_err(serde::de::Error::custom)?;
1268        Ok(plan)
1269    }
1270}