1use super::{
2 canonical_fingerprint, invalid_plan, is_canonical_sha256, quantize_storage_bytes,
3 validate_active_sequence_ceiling, AllocationKind, AllocationLifetime, BTreeSet,
4 BlockedTensorPadding, BufferUsage, ContractVersion, Deserialize, Deserializer,
5 DynamicResourceDemand, DynamicResourceShape, DynamicStorageProfile, ElementType, NodeId,
6 ResolvedTensorLayout, ResourceId, ResourceWorkShape, Serialize, StateInitialization,
7 VNextError,
8};
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
11#[serde(transparent)]
12pub struct DynamicBackingPoolId(String);
13
14impl DynamicBackingPoolId {
15 pub(super) fn from_compatibility(key: &PoolCompatibilityKey) -> Result<Self, VNextError> {
16 Ok(Self(format!(
17 "dynamic-pool/sha256/{}",
18 canonical_fingerprint(key, "fingerprint dynamic pool compatibility")?
19 )))
20 }
21
22 pub(super) fn validate(&self) -> Result<(), VNextError> {
23 let Some(hash) = self.0.strip_prefix("dynamic-pool/sha256/") else {
24 return Err(invalid_plan(
25 "dynamic backing pool id has an invalid prefix",
26 ));
27 };
28 if !is_canonical_sha256(hash) {
29 return Err(invalid_plan("dynamic backing pool id has an invalid hash"));
30 }
31 Ok(())
32 }
33
34 pub fn as_str(&self) -> &str {
35 &self.0
36 }
37}
38
39impl<'de> Deserialize<'de> for DynamicBackingPoolId {
40 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41 where
42 D: Deserializer<'de>,
43 {
44 let id = Self(String::deserialize(deserializer)?);
45 id.validate().map_err(serde::de::Error::custom)?;
46 Ok(id)
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
51pub struct DynamicStorageContract {
52 pub(super) profile: DynamicStorageProfile,
53 pub(super) logical_layout_fingerprint: String,
54}
55
56impl DynamicStorageContract {
57 pub(super) fn new(
58 profile: DynamicStorageProfile,
59 logical_layout_fingerprint: String,
60 ) -> Result<Self, VNextError> {
61 if !is_canonical_sha256(&logical_layout_fingerprint) {
62 return Err(invalid_plan(
63 "dynamic storage logical layout fingerprint is invalid",
64 ));
65 }
66 Ok(Self {
67 profile,
68 logical_layout_fingerprint,
69 })
70 }
71
72 #[cfg(test)]
73 pub(crate) fn resource_test_contract(
74 profile: DynamicStorageProfile,
75 logical_layout_fingerprint: String,
76 ) -> Result<Self, VNextError> {
77 Self::new(profile, logical_layout_fingerprint)
78 }
79
80 pub const fn profile(&self) -> DynamicStorageProfile {
81 self.profile
82 }
83
84 pub fn logical_layout_fingerprint(&self) -> &str {
85 &self.logical_layout_fingerprint
86 }
87}
88
89#[derive(Deserialize)]
90#[serde(deny_unknown_fields)]
91pub(super) struct DynamicStorageContractWire {
92 pub(super) profile: DynamicStorageProfile,
93 pub(super) logical_layout_fingerprint: String,
94}
95
96impl<'de> Deserialize<'de> for DynamicStorageContract {
97 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98 where
99 D: Deserializer<'de>,
100 {
101 let wire = DynamicStorageContractWire::deserialize(deserializer)?;
102 Self::new(wire.profile, wire.logical_layout_fingerprint).map_err(serde::de::Error::custom)
103 }
104}
105
106#[derive(Serialize)]
107#[serde(rename_all = "snake_case")]
108pub(super) enum TensorStorageLayoutClass<'a> {
109 Contiguous,
110 Strided {
111 byte_strides: &'a [u64],
112 },
113 Blocked {
114 block: &'a [u64],
115 axis_order: &'a [u32],
116 padding: BlockedStoragePaddingClass,
117 },
118}
119
120#[derive(Serialize)]
121#[serde(rename_all = "snake_case")]
122pub(super) enum BlockedStoragePaddingClass {
123 Exact,
124 ZeroFill,
125}
126
127#[derive(Serialize)]
128#[serde(rename_all = "snake_case")]
129pub(super) enum WorkspaceStorageLayoutClass {
130 OpaqueBytesV1,
131}
132
133pub(super) fn tensor_storage_layout_fingerprint(
134 layout: &ResolvedTensorLayout,
135) -> Result<String, VNextError> {
136 let class = match layout {
137 ResolvedTensorLayout::Contiguous => TensorStorageLayoutClass::Contiguous,
138 ResolvedTensorLayout::Strided { byte_strides } => {
139 TensorStorageLayoutClass::Strided { byte_strides }
140 }
141 ResolvedTensorLayout::Blocked {
142 block,
143 axis_order,
144 padding,
145 } => TensorStorageLayoutClass::Blocked {
146 block,
147 axis_order,
148 padding: match padding {
149 BlockedTensorPadding::Exact => BlockedStoragePaddingClass::Exact,
150 BlockedTensorPadding::ZeroFill { .. } => BlockedStoragePaddingClass::ZeroFill,
151 },
152 },
153 };
154 canonical_fingerprint(&class, "fingerprint tensor storage layout class")
155}
156
157pub(super) fn workspace_storage_layout_fingerprint() -> Result<String, VNextError> {
158 canonical_fingerprint(
159 &WorkspaceStorageLayoutClass::OpaqueBytesV1,
160 "fingerprint workspace storage layout class",
161 )
162}
163
164pub(super) fn static_contiguous_storage_profile() -> Result<DynamicStorageProfile, VNextError> {
165 DynamicStorageProfile::new(
166 super::DynamicStorageAllocator::LinearArena,
167 super::DynamicStorageView::Contiguous,
168 )
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
172pub struct PoolCompatibilityKey {
173 pub(super) version: ContractVersion,
174 pub(super) profile: DynamicStorageProfile,
175 pub(super) usage: BufferUsage,
176 pub(super) element_type: ElementType,
177 pub(super) logical_layout_fingerprint: String,
178 pub(super) alignment_bytes: u64,
179}
180
181impl PoolCompatibilityKey {
182 pub(super) fn new(
183 storage: &DynamicStorageContract,
184 usage: BufferUsage,
185 element_type: ElementType,
186 alignment_bytes: u64,
187 ) -> Result<Self, VNextError> {
188 if alignment_bytes == 0 || !alignment_bytes.is_power_of_two() {
189 return Err(invalid_plan(
190 "dynamic pool compatibility alignment is invalid",
191 ));
192 }
193 let key = Self {
194 version: ContractVersion::new(1, 0),
195 profile: storage.profile,
196 usage,
197 element_type,
198 logical_layout_fingerprint: storage.logical_layout_fingerprint.clone(),
199 alignment_bytes,
200 };
201 key.validate()?;
202 Ok(key)
203 }
204
205 pub(super) fn validate(&self) -> Result<(), VNextError> {
206 if self.version != ContractVersion::new(1, 0)
207 || !is_canonical_sha256(&self.logical_layout_fingerprint)
208 || self.alignment_bytes == 0
209 || !self.alignment_bytes.is_power_of_two()
210 {
211 return Err(invalid_plan("dynamic pool compatibility key is invalid"));
212 }
213 Ok(())
214 }
215
216 pub const fn profile(&self) -> DynamicStorageProfile {
217 self.profile
218 }
219
220 pub const fn usage(&self) -> BufferUsage {
221 self.usage
222 }
223
224 pub const fn element_type(&self) -> ElementType {
225 self.element_type
226 }
227
228 pub fn logical_layout_fingerprint(&self) -> &str {
229 &self.logical_layout_fingerprint
230 }
231
232 pub const fn alignment_bytes(&self) -> u64 {
233 self.alignment_bytes
234 }
235}
236
237#[derive(Deserialize)]
238#[serde(deny_unknown_fields)]
239pub(super) struct PoolCompatibilityKeyWire {
240 pub(super) version: ContractVersion,
241 pub(super) profile: DynamicStorageProfile,
242 pub(super) usage: BufferUsage,
243 pub(super) element_type: ElementType,
244 pub(super) logical_layout_fingerprint: String,
245 pub(super) alignment_bytes: u64,
246}
247
248impl<'de> Deserialize<'de> for PoolCompatibilityKey {
249 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
250 where
251 D: Deserializer<'de>,
252 {
253 let wire = PoolCompatibilityKeyWire::deserialize(deserializer)?;
254 let key = Self {
255 version: wire.version,
256 profile: wire.profile,
257 usage: wire.usage,
258 element_type: wire.element_type,
259 logical_layout_fingerprint: wire.logical_layout_fingerprint,
260 alignment_bytes: wire.alignment_bytes,
261 };
262 key.validate().map_err(serde::de::Error::custom)?;
263 Ok(key)
264 }
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
268#[serde(rename_all = "snake_case")]
269pub enum DynamicPoolProvisioningMode {
270 DemandDrivenElastic,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
278#[serde(deny_unknown_fields)]
279pub struct DynamicPoolProvisioningPolicy {
280 pub(super) mode: DynamicPoolProvisioningMode,
281 pub(super) minimum_resident_bytes: u64,
282 pub(super) maximum_resident_bytes: u64,
283}
284
285impl DynamicPoolProvisioningPolicy {
286 pub(super) fn demand_driven(
287 minimum_resident_bytes: u64,
288 maximum_resident_bytes: u64,
289 ) -> Result<Self, VNextError> {
290 let policy = Self {
291 mode: DynamicPoolProvisioningMode::DemandDrivenElastic,
292 minimum_resident_bytes,
293 maximum_resident_bytes,
294 };
295 policy.validate()?;
296 Ok(policy)
297 }
298
299 pub(super) fn validate(&self) -> Result<(), VNextError> {
300 if self.minimum_resident_bytes == 0
301 || self.maximum_resident_bytes < self.minimum_resident_bytes
302 {
303 return Err(invalid_plan("dynamic pool provisioning bounds are invalid"));
304 }
305 Ok(())
306 }
307
308 pub const fn mode(&self) -> DynamicPoolProvisioningMode {
309 self.mode
310 }
311
312 pub const fn minimum_resident_bytes(&self) -> u64 {
313 self.minimum_resident_bytes
314 }
315
316 pub const fn maximum_resident_bytes(&self) -> u64 {
317 self.maximum_resident_bytes
318 }
319}
320
321#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
326pub struct DynamicBackingPoolSpec {
327 pub(super) pool_id: DynamicBackingPoolId,
328 pub(super) compatibility: PoolCompatibilityKey,
329 pub(super) resource_ids: Vec<ResourceId>,
330 pub(super) minimum_request_bytes: u64,
331 pub(super) minimum_sequence_bytes: u64,
332 pub(super) minimum_step_bytes: u64,
333 pub(super) minimum_invocation_peak_bytes: u64,
334 pub(super) step_resource_slots: Vec<StepResourceSlot>,
335 pub(super) theoretical_ceiling_bytes: CanonicalU128,
336 pub(super) reusable_workspace_ceiling_bytes: u64,
337 #[serde(skip_serializing_if = "is_zero_bytes")]
338 pub(super) checkpoint_growth_ceiling_bytes: u64,
339 pub(super) provisioning: DynamicPoolProvisioningPolicy,
340 pub(super) invocation_liveness_mode: InvocationLivenessMode,
341 pub(super) invocation_liveness: Vec<InvocationResourceLiveness>,
342}
343
344impl DynamicBackingPoolSpec {
345 #[allow(clippy::too_many_arguments)]
346 pub(super) fn from_core(
347 compatibility: PoolCompatibilityKey,
348 resource_ids: Vec<ResourceId>,
349 minimum_request_bytes: u64,
350 minimum_sequence_bytes: u64,
351 minimum_step_bytes: u64,
352 minimum_invocation_peak_bytes: u64,
353 step_resource_slots: Vec<StepResourceSlot>,
354 theoretical_ceiling_bytes: u128,
355 reusable_workspace_ceiling_bytes: u64,
356 checkpoint_growth_ceiling_bytes: u64,
357 dynamic_capacity_bytes: u64,
358 invocation_liveness_mode: InvocationLivenessMode,
359 invocation_liveness: Vec<InvocationResourceLiveness>,
360 ) -> Result<Self, VNextError> {
361 compatibility.validate()?;
362 let pool_id = DynamicBackingPoolId::from_compatibility(&compatibility)?;
363 let minimum_resident_bytes = minimum_request_bytes
364 .checked_add(minimum_sequence_bytes)
365 .and_then(|bytes| bytes.checked_add(minimum_step_bytes))
366 .and_then(|bytes| bytes.checked_add(minimum_invocation_peak_bytes))
367 .ok_or_else(|| invalid_plan("dynamic pool runnable minimum overflows u64"))?;
368 let combined_ceiling_bytes = theoretical_ceiling_bytes
369 .checked_add(u128::from(reusable_workspace_ceiling_bytes))
370 .and_then(|bytes| bytes.checked_add(u128::from(checkpoint_growth_ceiling_bytes)))
371 .ok_or_else(|| invalid_plan("dynamic pool combined ceiling overflows u128"))?;
372 let maximum_resident_bytes =
373 u64::try_from(combined_ceiling_bytes.min(u128::from(dynamic_capacity_bytes)))
374 .map_err(|_| invalid_plan("dynamic pool resident ceiling exceeds u64"))?;
375 let spec = Self {
376 pool_id,
377 compatibility,
378 resource_ids,
379 minimum_request_bytes,
380 minimum_sequence_bytes,
381 minimum_step_bytes,
382 minimum_invocation_peak_bytes,
383 step_resource_slots,
384 theoretical_ceiling_bytes: CanonicalU128::new(theoretical_ceiling_bytes),
385 reusable_workspace_ceiling_bytes,
386 checkpoint_growth_ceiling_bytes,
387 provisioning: DynamicPoolProvisioningPolicy::demand_driven(
388 minimum_resident_bytes,
389 maximum_resident_bytes,
390 )?,
391 invocation_liveness_mode,
392 invocation_liveness,
393 };
394 spec.validate_local()?;
395 Ok(spec)
396 }
397
398 pub(super) fn validate_local(&self) -> Result<(), VNextError> {
399 self.pool_id.validate()?;
400 self.compatibility.validate()?;
401 self.provisioning.validate()?;
402 for slot in &self.step_resource_slots {
403 slot.validate()?;
404 }
405 let minimum_resident_bytes = self
406 .minimum_request_bytes
407 .checked_add(self.minimum_sequence_bytes)
408 .and_then(|bytes| bytes.checked_add(self.minimum_step_bytes))
409 .and_then(|bytes| bytes.checked_add(self.minimum_invocation_peak_bytes))
410 .ok_or_else(|| invalid_plan("dynamic pool runnable minimum overflows u64"))?;
411 if self.pool_id != DynamicBackingPoolId::from_compatibility(&self.compatibility)?
412 || self.resource_ids.is_empty()
413 || (self.checkpoint_growth_ceiling_bytes != 0
414 && self.compatibility.usage != BufferUsage::State)
415 || self.resource_ids.windows(2).any(|pair| pair[0] >= pair[1])
416 || minimum_resident_bytes != self.provisioning.minimum_resident_bytes
417 || u128::from(self.provisioning.maximum_resident_bytes)
418 > self
419 .theoretical_ceiling_bytes
420 .get()
421 .checked_add(u128::from(self.reusable_workspace_ceiling_bytes))
422 .and_then(|bytes| {
423 bytes.checked_add(u128::from(self.checkpoint_growth_ceiling_bytes))
424 })
425 .ok_or_else(|| invalid_plan("dynamic pool combined ceiling overflows u128"))?
426 || self
427 .step_resource_slots
428 .windows(2)
429 .any(|pair| pair[0].resource_ids >= pair[1].resource_ids)
430 || self
431 .step_resource_slots
432 .iter()
433 .flat_map(|slot| slot.resource_ids.iter())
434 .collect::<BTreeSet<_>>()
435 .len()
436 != self
437 .step_resource_slots
438 .iter()
439 .map(|slot| slot.resource_ids.len())
440 .sum::<usize>()
441 {
442 return Err(invalid_plan(
443 "dynamic backing pool identity, membership, or bounds are invalid",
444 ));
445 }
446 match self.invocation_liveness_mode {
447 InvocationLivenessMode::NoInvocationResources => {
448 if self.minimum_invocation_peak_bytes != 0 || !self.invocation_liveness.is_empty() {
449 return Err(invalid_plan(
450 "non-invocation pool carries invocation liveness evidence",
451 ));
452 }
453 }
454 InvocationLivenessMode::TotalOrderReuse
455 | InvocationLivenessMode::ConservativeConcurrent => {
456 if self.minimum_invocation_peak_bytes == 0
457 || self.invocation_liveness.is_empty()
458 || self
459 .invocation_liveness
460 .windows(2)
461 .any(|pair| pair[0].node_id >= pair[1].node_id)
462 {
463 return Err(invalid_plan(
464 "invocation pool liveness evidence is empty or non-canonical",
465 ));
466 }
467 }
468 }
469 Ok(())
470 }
471
472 pub fn pool_id(&self) -> &DynamicBackingPoolId {
473 &self.pool_id
474 }
475
476 pub fn compatibility(&self) -> &PoolCompatibilityKey {
477 &self.compatibility
478 }
479
480 pub fn resource_ids(&self) -> &[ResourceId] {
481 &self.resource_ids
482 }
483
484 pub const fn minimum_request_bytes(&self) -> u64 {
485 self.minimum_request_bytes
486 }
487
488 pub const fn minimum_sequence_bytes(&self) -> u64 {
489 self.minimum_sequence_bytes
490 }
491
492 pub const fn minimum_step_bytes(&self) -> u64 {
493 self.minimum_step_bytes
494 }
495
496 pub const fn minimum_invocation_peak_bytes(&self) -> u64 {
497 self.minimum_invocation_peak_bytes
498 }
499
500 pub fn step_resource_slots(&self) -> &[StepResourceSlot] {
501 &self.step_resource_slots
502 }
503
504 pub fn theoretical_ceiling_bytes(&self) -> u128 {
505 self.theoretical_ceiling_bytes.get()
506 }
507
508 pub const fn reusable_workspace_ceiling_bytes(&self) -> u64 {
509 self.reusable_workspace_ceiling_bytes
510 }
511
512 pub const fn checkpoint_growth_ceiling_bytes(&self) -> u64 {
515 self.checkpoint_growth_ceiling_bytes
516 }
517
518 pub fn provisioning(&self) -> &DynamicPoolProvisioningPolicy {
519 &self.provisioning
520 }
521
522 pub const fn invocation_liveness_mode(&self) -> InvocationLivenessMode {
523 self.invocation_liveness_mode
524 }
525
526 pub fn invocation_liveness(&self) -> &[InvocationResourceLiveness] {
527 &self.invocation_liveness
528 }
529}
530
531#[derive(Deserialize)]
532#[serde(deny_unknown_fields)]
533pub(super) struct DynamicBackingPoolSpecWire {
534 pub(super) pool_id: DynamicBackingPoolId,
535 pub(super) compatibility: PoolCompatibilityKey,
536 pub(super) resource_ids: Vec<ResourceId>,
537 pub(super) minimum_request_bytes: u64,
538 pub(super) minimum_sequence_bytes: u64,
539 pub(super) minimum_step_bytes: u64,
540 pub(super) minimum_invocation_peak_bytes: u64,
541 pub(super) step_resource_slots: Vec<StepResourceSlot>,
542 pub(super) theoretical_ceiling_bytes: CanonicalU128,
543 pub(super) reusable_workspace_ceiling_bytes: u64,
544 #[serde(default)]
545 pub(super) checkpoint_growth_ceiling_bytes: u64,
546 pub(super) provisioning: DynamicPoolProvisioningPolicy,
547 pub(super) invocation_liveness_mode: InvocationLivenessMode,
548 pub(super) invocation_liveness: Vec<InvocationResourceLiveness>,
549}
550
551impl<'de> Deserialize<'de> for DynamicBackingPoolSpec {
552 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
553 where
554 D: Deserializer<'de>,
555 {
556 let wire = DynamicBackingPoolSpecWire::deserialize(deserializer)?;
557 let spec = Self {
558 pool_id: wire.pool_id,
559 compatibility: wire.compatibility,
560 resource_ids: wire.resource_ids,
561 minimum_request_bytes: wire.minimum_request_bytes,
562 minimum_sequence_bytes: wire.minimum_sequence_bytes,
563 minimum_step_bytes: wire.minimum_step_bytes,
564 minimum_invocation_peak_bytes: wire.minimum_invocation_peak_bytes,
565 step_resource_slots: wire.step_resource_slots,
566 theoretical_ceiling_bytes: wire.theoretical_ceiling_bytes,
567 reusable_workspace_ceiling_bytes: wire.reusable_workspace_ceiling_bytes,
568 checkpoint_growth_ceiling_bytes: wire.checkpoint_growth_ceiling_bytes,
569 provisioning: wire.provisioning,
570 invocation_liveness_mode: wire.invocation_liveness_mode,
571 invocation_liveness: wire.invocation_liveness,
572 };
573 spec.validate_local().map_err(serde::de::Error::custom)?;
574 Ok(spec)
575 }
576}
577
578fn is_zero_bytes(bytes: &u64) -> bool {
579 *bytes == 0
580}
581
582#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
583pub struct DynamicResourceDescriptor {
584 pub(super) base_resource_id: ResourceId,
585 pub(super) demand: DynamicResourceDemand,
586 pub(super) alignment_bytes: u64,
587 pub(super) usage: BufferUsage,
588 pub(super) element_type: ElementType,
589 pub(super) lifetime: AllocationLifetime,
590 pub(super) kind: AllocationKind,
591 pub(super) storage: DynamicStorageContract,
592 pub(super) pool_id: DynamicBackingPoolId,
593 pub(super) initialization: StateInitialization,
594 pub(super) theoretical_maximum_instances: u32,
597}
598
599impl DynamicResourceDescriptor {
600 #[allow(clippy::too_many_arguments)]
601 pub(super) fn new(
602 base_resource_id: ResourceId,
603 demand: DynamicResourceDemand,
604 alignment_bytes: u64,
605 usage: BufferUsage,
606 element_type: ElementType,
607 lifetime: AllocationLifetime,
608 kind: AllocationKind,
609 storage: DynamicStorageContract,
610 initialization: StateInitialization,
611 theoretical_maximum_instances: u32,
612 ) -> Result<Self, VNextError> {
613 validate_active_sequence_ceiling(theoretical_maximum_instances)?;
614 if alignment_bytes == 0
615 || !alignment_bytes.is_power_of_two()
616 || lifetime == AllocationLifetime::Plan
617 {
618 return Err(invalid_plan(
619 "dynamic resource descriptor has invalid alignment or static lifetime",
620 ));
621 }
622 let kind_valid = match &kind {
623 AllocationKind::InitializationScratch => false,
624 AllocationKind::Scratch { .. } => {
625 lifetime == AllocationLifetime::Invocation
626 && usage == BufferUsage::Scratch
627 && element_type == ElementType::U8
628 }
629 AllocationKind::Binding { .. } => {
630 lifetime == AllocationLifetime::Invocation
631 && usage == BufferUsage::Binding
632 && element_type == ElementType::U8
633 }
634 AllocationKind::Persistent { .. } => {
635 matches!(
636 lifetime,
637 AllocationLifetime::Request
638 | AllocationLifetime::Sequence
639 | AllocationLifetime::Step
640 ) && usage == BufferUsage::Persistent
641 && element_type == ElementType::U8
642 }
643 AllocationKind::Value => usage != BufferUsage::Weights,
644 };
645 if !kind_valid {
646 return Err(invalid_plan(
647 "dynamic resource kind, lifetime, usage, or element type is inconsistent",
648 ));
649 }
650 if initialization == StateInitialization::Zero
651 && (kind != AllocationKind::Value
652 || usage != BufferUsage::State
653 || lifetime != AllocationLifetime::Sequence)
654 {
655 return Err(invalid_plan(
656 "zero initialization requires semantic Sequence state backing",
657 ));
658 }
659 demand.validate()?;
660 if matches!(
661 &demand,
662 DynamicResourceDemand::ActualSequences {
663 maximum_sequences,
664 ..
665 } if *maximum_sequences != theoretical_maximum_instances
666 ) {
667 return Err(invalid_plan(
668 "actual-sequence demand and descriptor instance ceilings differ",
669 ));
670 }
671 let pool_id = DynamicBackingPoolId::from_compatibility(&PoolCompatibilityKey::new(
672 &storage,
673 usage,
674 element_type,
675 alignment_bytes,
676 )?)?;
677 let descriptor = Self {
678 base_resource_id,
679 demand,
680 alignment_bytes,
681 usage,
682 element_type,
683 lifetime,
684 kind,
685 storage,
686 pool_id,
687 initialization,
688 theoretical_maximum_instances,
689 };
690 descriptor.evaluate_request_bytes_for_shape(descriptor.demand.minimum_shape())?;
691 descriptor
692 .evaluate_request_bytes_for_shape(descriptor.demand.theoretical_maximum_shape())?;
693 Ok(descriptor)
694 }
695
696 #[cfg(test)]
697 pub(crate) fn resource_test_binding(
698 base_resource_id: ResourceId,
699 demand: DynamicResourceDemand,
700 alignment_bytes: u64,
701 node_id: NodeId,
702 storage: DynamicStorageContract,
703 theoretical_maximum_instances: u32,
704 ) -> Result<Self, VNextError> {
705 Self::new(
706 base_resource_id,
707 demand,
708 alignment_bytes,
709 BufferUsage::Binding,
710 ElementType::U8,
711 AllocationLifetime::Invocation,
712 AllocationKind::Binding { node_id },
713 storage,
714 StateInitialization::None,
715 theoretical_maximum_instances,
716 )
717 }
718
719 pub fn base_resource_id(&self) -> &ResourceId {
720 &self.base_resource_id
721 }
722
723 pub fn demand(&self) -> &DynamicResourceDemand {
724 &self.demand
725 }
726
727 pub const fn theoretical_maximum_instances(&self) -> u32 {
728 self.theoretical_maximum_instances
729 }
730
731 pub fn evaluate_logical_request_bytes(
732 &self,
733 work: &ResourceWorkShape,
734 ) -> Result<u64, VNextError> {
735 self.demand.evaluate_bytes(work)
736 }
737
738 pub(crate) fn evaluate_logical_request_bytes_for_shape(
739 &self,
740 shape: DynamicResourceShape,
741 ) -> Result<u64, VNextError> {
742 self.demand.evaluate_shape_bytes(shape)
743 }
744
745 pub fn physical_allocation_quantum_bytes(&self) -> u64 {
746 match self.storage.profile().allocator() {
747 super::DynamicStorageAllocator::LinearArena => self.alignment_bytes,
748 super::DynamicStorageAllocator::FixedBlockArena { block_bytes } => {
749 block_bytes.max(self.alignment_bytes)
750 }
751 }
752 }
753
754 pub fn evaluate_request_bytes(&self, work: &ResourceWorkShape) -> Result<u64, VNextError> {
758 self.evaluate_request_bytes_for_shape(work.immediate_shape())
759 }
760
761 pub fn evaluate_fit_request_bytes(&self, work: &ResourceWorkShape) -> Result<u64, VNextError> {
762 self.evaluate_request_bytes_for_shape(work.fit_shape())
763 }
764
765 pub(crate) fn evaluate_request_bytes_for_shape(
766 &self,
767 shape: DynamicResourceShape,
768 ) -> Result<u64, VNextError> {
769 quantize_storage_bytes(
770 self.evaluate_logical_request_bytes_for_shape(shape)?,
771 self.alignment_bytes,
772 self.storage.profile(),
773 )
774 }
775
776 pub fn minimum_request_bytes(&self) -> Result<u64, VNextError> {
777 self.evaluate_request_bytes_for_shape(self.demand.minimum_shape())
778 }
779
780 pub fn theoretical_maximum_request_bytes(&self) -> Result<u64, VNextError> {
781 self.evaluate_request_bytes_for_shape(self.demand.theoretical_maximum_shape())
782 }
783
784 pub(super) fn theoretical_maximum_resident_bytes(&self) -> Result<u128, VNextError> {
790 let per_instance_bytes = match self.demand {
791 DynamicResourceDemand::ActualSequences { .. } => self.minimum_request_bytes()?,
792 _ => self.theoretical_maximum_request_bytes()?,
793 };
794 Ok(u128::from(per_instance_bytes) * u128::from(self.theoretical_maximum_instances))
795 }
796
797 pub const fn alignment_bytes(&self) -> u64 {
798 self.alignment_bytes
799 }
800
801 pub const fn usage(&self) -> BufferUsage {
802 self.usage
803 }
804
805 pub const fn element_type(&self) -> ElementType {
806 self.element_type
807 }
808
809 pub const fn lifetime(&self) -> AllocationLifetime {
810 self.lifetime
811 }
812
813 pub fn kind(&self) -> &AllocationKind {
814 &self.kind
815 }
816
817 pub fn storage(&self) -> &DynamicStorageContract {
818 &self.storage
819 }
820
821 pub fn pool_id(&self) -> &DynamicBackingPoolId {
822 &self.pool_id
823 }
824
825 pub const fn initialization(&self) -> StateInitialization {
826 self.initialization
827 }
828}
829
830#[derive(Deserialize)]
831#[serde(deny_unknown_fields)]
832pub(super) struct DynamicResourceDescriptorWire {
833 pub(super) base_resource_id: ResourceId,
834 pub(super) demand: DynamicResourceDemand,
835 pub(super) alignment_bytes: u64,
836 pub(super) usage: BufferUsage,
837 pub(super) element_type: ElementType,
838 pub(super) lifetime: AllocationLifetime,
839 pub(super) kind: AllocationKind,
840 pub(super) storage: DynamicStorageContract,
841 pub(super) pool_id: DynamicBackingPoolId,
842 pub(super) initialization: StateInitialization,
843 pub(super) theoretical_maximum_instances: u32,
844}
845
846impl<'de> Deserialize<'de> for DynamicResourceDescriptor {
847 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
848 where
849 D: Deserializer<'de>,
850 {
851 let wire = DynamicResourceDescriptorWire::deserialize(deserializer)?;
852 let descriptor = Self::new(
853 wire.base_resource_id,
854 wire.demand,
855 wire.alignment_bytes,
856 wire.usage,
857 wire.element_type,
858 wire.lifetime,
859 wire.kind,
860 wire.storage,
861 wire.initialization,
862 wire.theoretical_maximum_instances,
863 )
864 .map_err(serde::de::Error::custom)?;
865 if descriptor.pool_id != wire.pool_id {
866 return Err(serde::de::Error::custom(
867 "dynamic resource pool id is not core-derived from compatibility",
868 ));
869 }
870 Ok(descriptor)
871 }
872}
873
874#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
875#[serde(transparent)]
876pub(super) struct CanonicalU128(String);
877
878impl CanonicalU128 {
879 pub(super) fn new(value: u128) -> Self {
880 Self(value.to_string())
881 }
882
883 pub(super) fn get(&self) -> u128 {
884 self.0
885 .parse()
886 .expect("canonical u128 is validated at construction or deserialization")
887 }
888}
889
890impl<'de> Deserialize<'de> for CanonicalU128 {
891 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
892 where
893 D: Deserializer<'de>,
894 {
895 let value = String::deserialize(deserializer)?;
896 let parsed = value.parse::<u128>().map_err(serde::de::Error::custom)?;
897 if parsed.to_string() != value {
898 return Err(serde::de::Error::custom(
899 "u128 evidence must be a canonical unsigned decimal string",
900 ));
901 }
902 Ok(Self(value))
903 }
904}
905
906#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
907#[serde(rename_all = "snake_case")]
908pub enum InvocationLivenessMode {
909 NoInvocationResources,
910 TotalOrderReuse,
914 ConservativeConcurrent,
917}
918
919#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
923#[serde(deny_unknown_fields)]
924pub struct StepResourceSlot {
925 pub(super) kind: StepResourceSlotKind,
926 pub(super) resource_ids: Vec<ResourceId>,
927}
928
929impl StepResourceSlot {
930 pub(super) fn dedicated(resource_id: ResourceId) -> Self {
931 Self {
932 kind: StepResourceSlotKind::Dedicated,
933 resource_ids: vec![resource_id],
934 }
935 }
936
937 pub(super) fn ordered_single_fence_wave(
938 mut resource_ids: Vec<ResourceId>,
939 ) -> Result<Self, VNextError> {
940 resource_ids.sort();
941 if resource_ids.len() < 2 || resource_ids.windows(2).any(|pair| pair[0] == pair[1]) {
942 return Err(invalid_plan(
943 "ordered single-fence step slot requires at least two unique resources",
944 ));
945 }
946 Ok(Self {
947 kind: StepResourceSlotKind::OrderedSingleFenceStepWave,
948 resource_ids,
949 })
950 }
951
952 pub(super) fn validate(&self) -> Result<(), VNextError> {
953 if self.resource_ids.is_empty()
954 || self.resource_ids.windows(2).any(|pair| pair[0] >= pair[1])
955 || match self.kind {
956 StepResourceSlotKind::Dedicated => self.resource_ids.len() != 1,
957 StepResourceSlotKind::OrderedSingleFenceStepWave => self.resource_ids.len() < 2,
958 }
959 {
960 return Err(invalid_plan(
961 "step resource slot kind or members are invalid",
962 ));
963 }
964 Ok(())
965 }
966
967 pub const fn kind(&self) -> StepResourceSlotKind {
968 self.kind
969 }
970
971 pub fn resource_ids(&self) -> &[ResourceId] {
972 &self.resource_ids
973 }
974}
975
976#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
977#[serde(rename_all = "snake_case")]
978pub enum StepResourceSlotKind {
979 Dedicated,
980 OrderedSingleFenceStepWave,
984}
985
986#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
987#[serde(deny_unknown_fields)]
988pub struct InvocationResourceLiveness {
989 pub(super) node_id: NodeId,
990 pub(super) resource_ids: Vec<ResourceId>,
991}
992
993impl InvocationResourceLiveness {
994 pub fn node_id(&self) -> &NodeId {
995 &self.node_id
996 }
997
998 pub fn resource_ids(&self) -> &[ResourceId] {
999 &self.resource_ids
1000 }
1001}