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 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 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}