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