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