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#[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 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 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
458pub 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}