Skip to main content

ferrum_interfaces/vnext/execution/
contracts.rs

1use super::{
2    invalid_plan, is_canonical_sha256, AttributeId, BTreeMap, BTreeSet, CapabilityId,
3    ContractVersion, Deserialize, NodeId, OperationId, ProgramValueId,
4    ProviderCompatibilityRejectReason, ProviderId, ProviderResourcePlan, ResolvedValueBinding,
5    ResolvedValueRole, ResourceId, SemanticValue, Serialize, StateId, TensorAccess, VNextError,
6};
7use crate::vnext::ProviderExecutionSemantics;
8
9pub const EXECUTION_PLAN_SCHEMA: PlanSchemaVersion = PlanSchemaVersion::new(8, 1);
10pub const MAX_EXECUTION_PLAN_WIRE_BYTES: usize = 16 * 1024 * 1024;
11/// Maximum number of O(graph) static allocations plus dynamic descriptors.
12/// This limit is independent of the concurrency ceiling.
13pub const MAX_EXECUTION_PLAN_RESOURCE_ROWS: usize = 65_536;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16pub struct PlanSchemaVersion {
17    pub major: u16,
18    pub minor: u16,
19}
20
21impl PlanSchemaVersion {
22    pub const fn new(major: u16, minor: u16) -> Self {
23        Self { major, minor }
24    }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
28#[serde(transparent)]
29pub struct PlanHash(String);
30
31impl PlanHash {
32    pub(super) fn new(value: String) -> Result<Self, VNextError> {
33        if !is_canonical_sha256(&value) {
34            return Err(invalid_plan("plan hash must be a lowercase SHA256"));
35        }
36        Ok(Self(value))
37    }
38
39    pub fn as_str(&self) -> &str {
40        &self.0
41    }
42}
43
44impl<'de> Deserialize<'de> for PlanHash {
45    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46    where
47        D: serde::Deserializer<'de>,
48    {
49        Self::new(String::deserialize(deserializer)?).map_err(serde::de::Error::custom)
50    }
51}
52
53impl std::fmt::Display for PlanHash {
54    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        formatter.write_str(&self.0)
56    }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum PlanProviderRejectReason {
62    NotRegistered,
63    Incompatible(Vec<ProviderCompatibilityRejectReason>),
64    StorageIncompatible { resource_ids: Vec<ResourceId> },
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct RejectedProvider {
69    pub(super) provider_id: ProviderId,
70    pub(super) reasons: PlanProviderRejectReason,
71}
72
73impl RejectedProvider {
74    pub fn provider_id(&self) -> &ProviderId {
75        &self.provider_id
76    }
77
78    pub fn reasons(&self) -> &PlanProviderRejectReason {
79        &self.reasons
80    }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum ProviderSelectionReason {
86    PreferredCompatible,
87    CanonicalCompatible,
88    FallbackFromPreferred,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct ProviderSelection {
93    pub(super) requested_provider: Option<ProviderId>,
94    pub(super) selected_provider: ProviderId,
95    pub(super) selection_reason: ProviderSelectionReason,
96    pub(super) rejected_providers: Vec<RejectedProvider>,
97}
98
99impl ProviderSelection {
100    pub fn requested_provider(&self) -> Option<&ProviderId> {
101        self.requested_provider.as_ref()
102    }
103
104    pub fn selected_provider(&self) -> &ProviderId {
105        &self.selected_provider
106    }
107
108    pub const fn selection_reason(&self) -> ProviderSelectionReason {
109        self.selection_reason
110    }
111
112    pub fn rejected_providers(&self) -> &[RejectedProvider] {
113        &self.rejected_providers
114    }
115}
116
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
118#[serde(rename_all = "snake_case")]
119pub enum PlanExactAliasKind {
120    MayAlias,
121    MustAlias,
122}
123
124/// Core-proven exact storage equality between one output and its declared
125/// input. A MayAlias contract with distinct storage deliberately emits no
126/// edge.
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct PlanExactAlias {
129    pub(super) output_value_id: ProgramValueId,
130    pub(super) output_ordinal: u32,
131    pub(super) input_value_id: ProgramValueId,
132    pub(super) input_ordinal: u32,
133    pub(super) kind: PlanExactAliasKind,
134}
135
136impl PlanExactAlias {
137    pub fn output_value_id(&self) -> &ProgramValueId {
138        &self.output_value_id
139    }
140
141    pub const fn output_ordinal(&self) -> u32 {
142        self.output_ordinal
143    }
144
145    pub fn input_value_id(&self) -> &ProgramValueId {
146        &self.input_value_id
147    }
148
149    pub const fn input_ordinal(&self) -> u32 {
150        self.input_ordinal
151    }
152
153    pub const fn kind(&self) -> PlanExactAliasKind {
154        self.kind
155    }
156}
157
158/// Typed state effect derived from the operation access contract for a
159/// declared ModelProgram state binding.
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161pub struct PlanStateEffect {
162    pub(super) state_id: StateId,
163    pub(super) state_value_id: ProgramValueId,
164    pub(super) lifetime: AllocationLifetime,
165    pub(super) access: TensorAccess,
166    pub(super) resource_ids: Vec<ResourceId>,
167}
168
169/// Exact resolved binding projection whose one logical axis advances with the
170/// core-issued packed token work shape.
171#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
172pub struct NodeTokenBindingProjection {
173    pub(super) value_id: ProgramValueId,
174    pub(super) role: ResolvedValueRole,
175    pub(super) ordinal: u32,
176    pub(super) axis: u32,
177    pub(super) rank: u32,
178    pub(super) canonical_extent: u64,
179}
180
181impl NodeTokenBindingProjection {
182    pub fn value_id(&self) -> &ProgramValueId {
183        &self.value_id
184    }
185
186    pub const fn role(&self) -> ResolvedValueRole {
187        self.role
188    }
189
190    pub const fn ordinal(&self) -> u32 {
191        self.ordinal
192    }
193
194    pub const fn axis(&self) -> u32 {
195        self.axis
196    }
197
198    pub const fn rank(&self) -> u32 {
199        self.rank
200    }
201
202    pub const fn canonical_extent(&self) -> u64 {
203        self.canonical_extent
204    }
205}
206
207/// Core-derived work mapping stored in the immutable execution plan. Token
208/// projections are resolved from one model-declared source dimension through
209/// the operation's symbolic signature, so providers never infer work from a
210/// tensor element count or model family.
211#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
212#[serde(rename_all = "snake_case")]
213pub enum NodeWorkContract {
214    Fixed,
215    Tokens {
216        source: NodeTokenBindingProjection,
217        projections: Vec<NodeTokenBindingProjection>,
218    },
219}
220
221impl NodeWorkContract {
222    pub fn token_source(&self) -> Option<&NodeTokenBindingProjection> {
223        match self {
224            Self::Fixed => None,
225            Self::Tokens { source, .. } => Some(source),
226        }
227    }
228
229    pub fn token_projections(&self) -> &[NodeTokenBindingProjection] {
230        match self {
231            Self::Fixed => &[],
232            Self::Tokens { projections, .. } => projections,
233        }
234    }
235
236    pub fn token_projection(
237        &self,
238        role: ResolvedValueRole,
239        ordinal: u32,
240    ) -> Option<&NodeTokenBindingProjection> {
241        self.token_projections()
242            .iter()
243            .find(|projection| projection.role == role && projection.ordinal == ordinal)
244    }
245}
246
247impl PlanStateEffect {
248    pub fn state_id(&self) -> &StateId {
249        &self.state_id
250    }
251
252    pub fn state_value_id(&self) -> &ProgramValueId {
253        &self.state_value_id
254    }
255
256    pub const fn lifetime(&self) -> AllocationLifetime {
257        self.lifetime
258    }
259
260    pub const fn access(&self) -> TensorAccess {
261        self.access
262    }
263
264    pub fn resource_ids(&self) -> &[ResourceId] {
265        &self.resource_ids
266    }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
270pub struct PlanNode {
271    pub(super) id: NodeId,
272    pub(super) dependencies: Vec<NodeId>,
273    pub(super) operation_id: OperationId,
274    pub(super) operation_version: ContractVersion,
275    pub(super) operation_fingerprint: String,
276    pub(super) provider_implementation_fingerprint: String,
277    pub(super) provider_execution_semantics: ProviderExecutionSemantics,
278    pub(super) required_capabilities: BTreeSet<CapabilityId>,
279    pub(super) attributes: BTreeMap<AttributeId, SemanticValue>,
280    pub(super) work: NodeWorkContract,
281    pub(super) selection: ProviderSelection,
282    pub(super) provider_resources: ProviderResourcePlan,
283    pub(super) values: Vec<ResolvedValueBinding>,
284    pub(super) exact_aliases: Vec<PlanExactAlias>,
285    pub(super) state_effects: Vec<PlanStateEffect>,
286    pub(super) scratch_resource: Option<ResourceId>,
287    pub(super) binding_resource: Option<ResourceId>,
288    pub(super) persistent_resource: Option<ResourceId>,
289    pub(super) resources: Vec<ResourceId>,
290}
291
292impl PlanNode {
293    #[cfg(test)]
294    pub(crate) fn resource_test_node(id: NodeId) -> Self {
295        let provider_resources = ProviderResourcePlan {
296            provider_id: ProviderId::new("provider/resource-test").expect("valid provider id"),
297            estimator_id: "resource-test-estimator".to_owned(),
298            estimator_version: ContractVersion::new(1, 0),
299            estimator_implementation_fingerprint: "1".repeat(64),
300            estimator_input_fingerprint: "2".repeat(64),
301            estimate_fingerprint: "3".repeat(64),
302            value_alignment_bytes: 16,
303            scratch: None,
304            binding: None,
305            persistent: None,
306        };
307        let selected_provider = provider_resources.provider_id().clone();
308        Self {
309            id,
310            dependencies: Vec::new(),
311            operation_id: OperationId::new("operation/resource-test").expect("valid operation id"),
312            operation_version: ContractVersion::new(1, 0),
313            operation_fingerprint: "4".repeat(64),
314            provider_implementation_fingerprint: "5".repeat(64),
315            provider_execution_semantics: ProviderExecutionSemantics::bitwise_eager_and_replay(),
316            required_capabilities: BTreeSet::new(),
317            attributes: BTreeMap::new(),
318            work: super::NodeWorkContract::Fixed,
319            selection: super::ProviderSelection {
320                requested_provider: None,
321                selected_provider,
322                selection_reason: super::ProviderSelectionReason::CanonicalCompatible,
323                rejected_providers: Vec::new(),
324            },
325            provider_resources,
326            values: Vec::new(),
327            exact_aliases: Vec::new(),
328            state_effects: Vec::new(),
329            scratch_resource: None,
330            binding_resource: None,
331            persistent_resource: None,
332            resources: Vec::new(),
333        }
334    }
335
336    #[cfg(test)]
337    pub(crate) fn resource_test_node_with_binding(
338        id: NodeId,
339        binding_resource: ResourceId,
340    ) -> Self {
341        let mut node = Self::resource_test_node(id);
342        node.binding_resource = Some(binding_resource.clone());
343        node.resources = vec![binding_resource];
344        node
345    }
346
347    #[cfg(test)]
348    pub(crate) fn resource_test_node_with_state_effect(
349        id: NodeId,
350        state_id: StateId,
351        state_value_id: ProgramValueId,
352        lifetime: AllocationLifetime,
353        access: TensorAccess,
354        resource_ids: Vec<ResourceId>,
355    ) -> Self {
356        let mut node = Self::resource_test_node(id);
357        node.state_effects = vec![PlanStateEffect {
358            state_id,
359            state_value_id,
360            lifetime,
361            access,
362            resource_ids,
363        }];
364        node
365    }
366
367    pub fn id(&self) -> &NodeId {
368        &self.id
369    }
370
371    pub fn dependencies(&self) -> &[NodeId] {
372        &self.dependencies
373    }
374
375    pub fn operation_id(&self) -> &OperationId {
376        &self.operation_id
377    }
378
379    pub const fn operation_version(&self) -> ContractVersion {
380        self.operation_version
381    }
382
383    pub fn operation_fingerprint(&self) -> &str {
384        &self.operation_fingerprint
385    }
386
387    pub fn provider_implementation_fingerprint(&self) -> &str {
388        &self.provider_implementation_fingerprint
389    }
390
391    pub const fn provider_execution_semantics(&self) -> ProviderExecutionSemantics {
392        self.provider_execution_semantics
393    }
394
395    pub fn required_capabilities(&self) -> &BTreeSet<CapabilityId> {
396        &self.required_capabilities
397    }
398
399    pub fn attributes(&self) -> &BTreeMap<AttributeId, SemanticValue> {
400        &self.attributes
401    }
402
403    pub fn work(&self) -> &NodeWorkContract {
404        &self.work
405    }
406
407    pub fn selection(&self) -> &ProviderSelection {
408        &self.selection
409    }
410
411    pub fn provider_resources(&self) -> &ProviderResourcePlan {
412        &self.provider_resources
413    }
414
415    pub fn values(&self) -> &[ResolvedValueBinding] {
416        &self.values
417    }
418
419    pub fn exact_aliases(&self) -> &[PlanExactAlias] {
420        &self.exact_aliases
421    }
422
423    pub fn state_effects(&self) -> &[PlanStateEffect] {
424        &self.state_effects
425    }
426
427    pub fn scratch_resource(&self) -> Option<&ResourceId> {
428        self.scratch_resource.as_ref()
429    }
430
431    pub fn binding_resource(&self) -> Option<&ResourceId> {
432        self.binding_resource.as_ref()
433    }
434
435    pub fn persistent_resource(&self) -> Option<&ResourceId> {
436        self.persistent_resource.as_ref()
437    }
438
439    pub fn resources(&self) -> &[ResourceId] {
440        &self.resources
441    }
442}
443
444#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
445#[serde(rename_all = "snake_case")]
446pub enum AllocationLifetime {
447    Plan,
448    Request,
449    Sequence,
450    Step,
451    Invocation,
452}
453
454#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455#[serde(rename_all = "snake_case")]
456pub enum AllocationKind {
457    Value,
458    Scratch { node_id: NodeId },
459    Binding { node_id: NodeId },
460    Persistent { node_id: NodeId },
461}