Skip to main content

ferrum_interfaces/vnext/execution/
storage.rs

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/// Typed bounds for elastic residency. `minimum_resident_bytes` is the amount
274/// required to make one request runnable, not an initial reservation. Pools
275/// may grow on demand up to `maximum_resident_bytes`; the process-wide device
276/// account remains the authority when several pools compete for that memory.
277#[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/// One self-contained physical-compatibility class for demand-driven backing.
322/// Membership, runnable minima, completion-order reuse evidence, and elastic
323/// bounds are canonical plan data, so a runtime does not have to rediscover
324/// pool structure by scanning unrelated descriptors.
325#[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    /// Optional growth beyond ordinary live-resource demand. Each pool's
513    /// ceiling shares one aggregate checkpoint cap; these are not reservations.
514    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    /// Protocol-only ceiling used for checked evidence. No API may iterate,
595    /// reserve, allocate, or claim this many instances.
596    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    /// Exact physical claim for one logical shape. The semantic demand stays
755    /// unchanged in the plan; allocator geometry is applied only at this
756    /// boundary so admission and backing cannot under-count fixed blocks.
757    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    /// Conservative maximum physical residency for this resource across all
785    /// live instances. Sequence-shaped claims share the plan's global active
786    /// sequence ceiling, so multiplying a full-batch claim by that ceiling
787    /// would count the same participants twice. Splitting every participant
788    /// into its own claim is the physical-padding worst case.
789    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    /// Every invocation row in this pool is ordered by a transitive node
911    /// completion dependency, so one runnable request needs only the maximum
912    /// row size rather than their sum.
913    TotalOrderReuse,
914    /// The plan cannot prove that every invocation row completes before the
915    /// next one starts. The runnable minimum therefore sums member resources.
916    ConservativeConcurrent,
917}
918
919/// A set of Step-scoped logical resources that may project onto one physical
920/// extent. Multi-resource slots are emitted only when plan dependencies prove
921/// that every member's final user completes before the next member starts.
922#[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    /// Every member is an internal activation and the runtime must submit the
981    /// canonical plan order as one ordered command batch with one terminal
982    /// fence before it may consume this reuse proof.
983    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}