Skip to main content

ferrum_interfaces/vnext/operation/
descriptor.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5
6use super::super::{
7    CanonicalRational, CapabilityId, ContractVersion, OperationId, SemanticValue, VNextError,
8};
9use super::foundation::invalid_operation;
10use super::{
11    AliasPolicy, AttributeId, AttributeSchema, DimensionConstraint, ElementType, LayoutConstraint,
12    ResolvedTensorLayout, ResolvedTensorSpec, ResolvedValueBinding, ResolvedValueRole,
13    ResolvedValueStorage, StrideConstraint, TensorAccess, TensorContract,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ResourcePresenceRequirement {
19    Forbidden,
20    Optional,
21    Required,
22}
23
24impl ResourcePresenceRequirement {
25    pub const fn accepts(self, present: bool) -> bool {
26        matches!(
27            (self, present),
28            (Self::Forbidden, false) | (Self::Optional, _) | (Self::Required, true)
29        )
30    }
31}
32
33/// Shape-independent resource contract. Concrete byte counts, scopes, and
34/// alignment are produced by the selected provider's versioned estimator and
35/// bound into the immutable execution plan.
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct ResourceRequirements {
39    pub minimum_value_alignment_bytes: u64,
40    pub scratch: ResourcePresenceRequirement,
41    /// Small request-shaped control workspace whose contents are written in
42    /// the wave binding preamble and consumed by reusable compute.
43    pub binding: ResourcePresenceRequirement,
44    pub persistent: ResourcePresenceRequirement,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum OracleSpec {
50    Exact,
51    AbsoluteTolerance {
52        tolerance: CanonicalRational,
53    },
54    RelativeTolerance {
55        tolerance: CanonicalRational,
56    },
57    ReferenceOperation {
58        operation_id: OperationId,
59        version: ContractVersion,
60    },
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(rename_all = "snake_case")]
65pub enum ProfilePhase {
66    Load,
67    Prepare,
68    /// Backend operation shared by prefill and decode. The exact request phase
69    /// is derived from the bound work shape rather than changing operation
70    /// identity or selecting another provider in the hot path.
71    Forward,
72    Prefill,
73    Decode,
74    Transfer,
75    Synchronize,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct ProviderRequirement {
80    pub minimum_version: ContractVersion,
81    pub required_capabilities: BTreeSet<CapabilityId>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct OperationDescriptor {
86    pub id: OperationId,
87    pub version: ContractVersion,
88    pub inputs: Vec<TensorContract>,
89    pub outputs: Vec<TensorContract>,
90    pub attributes: AttributeSchema,
91    pub resources: ResourceRequirements,
92    pub oracle: OracleSpec,
93    pub provider: ProviderRequirement,
94    pub profile_phase: ProfilePhase,
95}
96
97impl OperationDescriptor {
98    pub fn validate(&self) -> Result<(), VNextError> {
99        if self.version.major == 0 {
100            return Err(VNextError::InvalidExecutionPlan {
101                reason: format!("operation `{}` has an unstable zero major version", self.id),
102            });
103        }
104        if self.outputs.is_empty() {
105            return Err(VNextError::InvalidExecutionPlan {
106                reason: format!("operation `{}` has no outputs", self.id),
107            });
108        }
109        for (index, input) in self.inputs.iter().enumerate() {
110            input.validate(&format!("operation.{}.inputs[{index}]", self.id))?;
111            if !matches!(input.access(), TensorAccess::Read | TensorAccess::ReadWrite)
112                || !matches!(input.alias(), AliasPolicy::NoAlias)
113            {
114                return Err(VNextError::InvalidExecutionPlan {
115                    reason: format!(
116                        "operation `{}` input {index} has invalid access or alias semantics",
117                        self.id
118                    ),
119                });
120            }
121        }
122        for (index, output) in self.outputs.iter().enumerate() {
123            output.validate(&format!("operation.{}.outputs[{index}]", self.id))?;
124            if !matches!(
125                output.access(),
126                TensorAccess::Write | TensorAccess::ReadWrite
127            ) {
128                return Err(VNextError::InvalidExecutionPlan {
129                    reason: format!("operation `{}` output {index} is not writable", self.id),
130                });
131            }
132            if let AliasPolicy::MayAlias { tensor_index }
133            | AliasPolicy::MustAlias { tensor_index } = output.alias()
134            {
135                if *tensor_index as usize >= self.inputs.len() {
136                    return Err(VNextError::InvalidExecutionPlan {
137                        reason: format!("operation `{}` output {index} aliases no input", self.id),
138                    });
139                }
140            }
141        }
142        if self.resources.minimum_value_alignment_bytes == 0
143            || !self
144                .resources
145                .minimum_value_alignment_bytes
146                .is_power_of_two()
147        {
148            return Err(VNextError::InvalidExecutionPlan {
149                reason: format!("operation `{}` has invalid resource requirements", self.id),
150            });
151        }
152        match self.oracle {
153            OracleSpec::AbsoluteTolerance { tolerance }
154            | OracleSpec::RelativeTolerance { tolerance }
155                if tolerance.numerator() < 0 =>
156            {
157                return Err(VNextError::InvalidExecutionPlan {
158                    reason: format!("operation `{}` has a negative oracle tolerance", self.id),
159                });
160            }
161            OracleSpec::AbsoluteTolerance { .. } | OracleSpec::RelativeTolerance { .. }
162                if self
163                    .outputs
164                    .iter()
165                    .any(|output| output.element_types().contains(&ElementType::Bool)) =>
166            {
167                return Err(VNextError::InvalidExecutionPlan {
168                    reason: format!(
169                        "operation `{}` applies numeric oracle tolerance to a possible boolean output",
170                        self.id
171                    ),
172                });
173            }
174            _ => {}
175        }
176        if self.provider.minimum_version.major == 0 {
177            return Err(VNextError::InvalidExecutionPlan {
178                reason: format!("operation `{}` has a zero provider major version", self.id),
179            });
180        }
181        if self.provider.minimum_version.major != self.version.major {
182            return Err(VNextError::InvalidExecutionPlan {
183                reason: format!(
184                    "operation `{}` version {} and provider minimum version {} have incompatible major versions",
185                    self.id, self.version, self.provider.minimum_version
186                ),
187            });
188        }
189        Ok(())
190    }
191
192    pub fn fingerprint(&self) -> Result<String, VNextError> {
193        self.validate()?;
194        let bytes = serde_json::to_vec(self).map_err(|error| VNextError::Serialization {
195            context: "serialize operation descriptor",
196            message: error.to_string(),
197        })?;
198        Ok(format!("{:x}", Sha256::digest(bytes)))
199    }
200
201    pub fn validate_attributes(
202        &self,
203        values: &BTreeMap<AttributeId, SemanticValue>,
204    ) -> Result<(), VNextError> {
205        self.attributes
206            .validate_values(values, &format!("operation.{}.attributes", self.id))
207    }
208
209    pub fn validate_resolved_bindings(
210        &self,
211        bindings: &[ResolvedValueBinding],
212    ) -> Result<(), VNextError> {
213        self.validate()?;
214        if bindings.len() != self.inputs.len() + self.outputs.len() {
215            return Err(invalid_operation(format!(
216                "operation `{}` expects {} value bindings, received {}",
217                self.id,
218                self.inputs.len() + self.outputs.len(),
219                bindings.len()
220            )));
221        }
222
223        let mut dimensions = BTreeMap::<String, u64>::new();
224        let mut strides = BTreeMap::<String, u64>::new();
225        let mut positions = BTreeSet::new();
226        for (index, binding) in bindings.iter().enumerate() {
227            let expected_position = if index < self.inputs.len() {
228                (ResolvedValueRole::Input, index as u32)
229            } else {
230                (
231                    ResolvedValueRole::Output,
232                    (index - self.inputs.len()) as u32,
233                )
234            };
235            if (binding.role(), binding.ordinal()) != expected_position {
236                return Err(invalid_operation(format!(
237                    "operation `{}` bindings are not in canonical input/output ordinal order",
238                    self.id
239                )));
240            }
241            if !positions.insert((binding.role(), binding.ordinal())) {
242                return Err(invalid_operation(format!(
243                    "operation `{}` contains duplicate ordinal bindings",
244                    self.id
245                )));
246            }
247            if let Some(previous) = bindings[..index]
248                .iter()
249                .find(|previous| previous.value_id() == binding.value_id())
250            {
251                let repeated_readonly_input = previous.role() == ResolvedValueRole::Input
252                    && binding.role() == ResolvedValueRole::Input
253                    && previous.access() == TensorAccess::Read
254                    && binding.access() == TensorAccess::Read
255                    && previous.tensor() == binding.tensor()
256                    && previous.storage() == binding.storage()
257                    && previous.usage() == binding.usage();
258                if !repeated_readonly_input {
259                    return Err(invalid_operation(format!(
260                        "operation `{}` repeats a value outside identical read-only input slots",
261                        self.id
262                    )));
263                }
264            }
265            let contract = match binding.role() {
266                ResolvedValueRole::Input => self.inputs.get(binding.ordinal() as usize),
267                ResolvedValueRole::Output => self.outputs.get(binding.ordinal() as usize),
268            }
269            .ok_or_else(|| {
270                invalid_operation(format!(
271                    "operation `{}` binding ordinal is out of range",
272                    self.id
273                ))
274            })?;
275            if binding.access() != contract.access() || binding.alias() != contract.alias() {
276                return Err(invalid_operation(format!(
277                    "operation `{}` binding access or alias differs from its contract",
278                    self.id
279                )));
280            }
281            Self::validate_resolved_tensor(
282                &self.id,
283                contract,
284                binding.tensor(),
285                &mut dimensions,
286                &mut strides,
287            )?;
288        }
289        let inputs = &bindings[..self.inputs.len()];
290        let outputs = &bindings[self.inputs.len()..];
291        for (index, input) in inputs.iter().enumerate() {
292            for previous in &inputs[..index] {
293                if storage_overlaps(input.storage(), previous.storage())
294                    && (input.value_id() != previous.value_id()
295                        || input.access() != TensorAccess::Read
296                        || previous.access() != TensorAccess::Read)
297                {
298                    return Err(invalid_operation(format!(
299                        "operation `{}` shares input storage between different or writable values",
300                        self.id
301                    )));
302                }
303            }
304        }
305        for (index, output) in outputs.iter().enumerate() {
306            let aliased_inputs = inputs
307                .iter()
308                .enumerate()
309                .filter(|(_, input)| storage_overlaps(output.storage(), input.storage()))
310                .map(|(ordinal, _)| ordinal as u32)
311                .collect::<Vec<_>>();
312            match output.alias() {
313                AliasPolicy::NoAlias if !aliased_inputs.is_empty() => {
314                    return Err(invalid_operation(format!(
315                        "operation `{}` output {index} aliases despite a no-alias contract",
316                        self.id
317                    )));
318                }
319                AliasPolicy::MayAlias { tensor_index } => {
320                    if aliased_inputs.iter().any(|ordinal| ordinal != tensor_index)
321                        || (aliased_inputs.contains(tensor_index)
322                            && output.storage() != inputs[*tensor_index as usize].storage())
323                    {
324                        return Err(invalid_operation(format!(
325                            "operation `{}` output {index} partially aliases or aliases the wrong input",
326                            self.id
327                        )));
328                    }
329                }
330                AliasPolicy::MustAlias { tensor_index }
331                    if aliased_inputs != [*tensor_index]
332                        || output.storage() != inputs[*tensor_index as usize].storage() =>
333                {
334                    return Err(invalid_operation(format!(
335                        "operation `{}` output {index} does not exactly alias its declared input",
336                        self.id
337                    )));
338                }
339                _ => {}
340            }
341            if outputs[..index]
342                .iter()
343                .any(|previous| storage_overlaps(output.storage(), previous.storage()))
344            {
345                return Err(invalid_operation(format!(
346                    "operation `{}` output resources overlap",
347                    self.id
348                )));
349            }
350        }
351        Ok(())
352    }
353
354    fn validate_resolved_tensor(
355        operation_id: &OperationId,
356        contract: &TensorContract,
357        tensor: &ResolvedTensorSpec,
358        dimensions: &mut BTreeMap<String, u64>,
359        strides: &mut BTreeMap<String, u64>,
360    ) -> Result<(), VNextError> {
361        if tensor.dimensions().len() != contract.dimensions().len()
362            || !contract.element_types().contains(&tensor.element_type())
363        {
364            return Err(invalid_operation(format!(
365                "operation `{operation_id}` resolved tensor rank or element type is incompatible"
366            )));
367        }
368        for (constraint, extent) in contract.dimensions().iter().zip(tensor.dimensions()) {
369            let compatible = match constraint {
370                DimensionConstraint::Exact(expected) => expected == extent,
371                DimensionConstraint::Range { minimum, maximum } => {
372                    minimum <= extent && extent <= maximum
373                }
374                DimensionConstraint::Symbol(symbol) => match dimensions.get(symbol) {
375                    Some(expected) => expected == extent,
376                    None => {
377                        dimensions.insert(symbol.clone(), *extent);
378                        true
379                    }
380                },
381            };
382            if !compatible {
383                return Err(invalid_operation(format!(
384                    "operation `{operation_id}` resolved tensor violates a dimension constraint"
385                )));
386            }
387        }
388
389        let mut matched_strides = None;
390        let layout_matches =
391            contract
392                .layouts()
393                .iter()
394                .any(|layout| match (layout, tensor.layout()) {
395                    (LayoutConstraint::Contiguous, ResolvedTensorLayout::Contiguous) => true,
396                    (
397                        LayoutConstraint::Blocked {
398                            block: expected_block,
399                            axis_order: expected_axis_order,
400                        },
401                        ResolvedTensorLayout::Blocked {
402                            block: actual_block,
403                            axis_order: actual_axis_order,
404                            ..
405                        },
406                    ) => expected_block == actual_block && expected_axis_order == actual_axis_order,
407                    (
408                        LayoutConstraint::Strided {
409                            strides: constraints,
410                        },
411                        ResolvedTensorLayout::Strided { byte_strides },
412                    ) if constraints.len() == byte_strides.len() => {
413                        let mut candidate = strides.clone();
414                        let matches =
415                            constraints
416                                .iter()
417                                .zip(byte_strides)
418                                .all(|(constraint, actual)| match constraint {
419                                    StrideConstraint::ExactBytes(expected) => expected == actual,
420                                    StrideConstraint::Symbol(symbol) => match candidate.get(symbol)
421                                    {
422                                        Some(expected) => expected == actual,
423                                        None => {
424                                            candidate.insert(symbol.clone(), *actual);
425                                            true
426                                        }
427                                    },
428                                });
429                        if matches {
430                            matched_strides = Some(candidate);
431                        }
432                        matches
433                    }
434                    _ => false,
435                });
436        if !layout_matches {
437            return Err(invalid_operation(format!(
438                "operation `{operation_id}` resolved tensor layout is incompatible"
439            )));
440        }
441        if let Some(candidate) = matched_strides {
442            *strides = candidate;
443        }
444        Ok(())
445    }
446}
447
448fn storage_overlaps(left: &ResolvedValueStorage, right: &ResolvedValueStorage) -> bool {
449    left.components().iter().any(|left| {
450        right.components().iter().any(|right| {
451            left.resource_id() == right.resource_id()
452                && left.offset_bytes() < right.offset_bytes().saturating_add(right.length_bytes())
453                && right.offset_bytes() < left.offset_bytes().saturating_add(left.length_bytes())
454        })
455    })
456}
457
458/// Object-safe semantic operation contract used while building a plan.
459pub trait OperationContract: Send + Sync {
460    fn descriptor(&self) -> &OperationDescriptor;
461
462    fn validate_signature(
463        &self,
464        inputs: &[TensorContract],
465        outputs: &[TensorContract],
466    ) -> Result<(), VNextError>;
467}