1use std::collections::HashMap;
2use std::fmt;
3use std::sync::Arc;
4
5use computegraph::compile::{compile, CompiledProgram};
6use computegraph::materialize::{
7 materialize_merge, MaterializedGraph, MaterializedOperation, MaterializedValue,
8};
9use computegraph::resolve::{resolve, ResolvedView, ValueDef};
10use computegraph::types::{OperationKey, ValueKey};
11use computegraph::GraphOperation;
12use num_complex::{Complex32, Complex64};
13use tenferro_ops::dim_expr::{DimExpr, DimExprEvalError};
14use tenferro_ops::input_key::TensorInputKey;
15use tenferro_ops::std_tensor_op::StdTensorOp;
16use tenferro_ops::{ShapeExtent, ShapeRelation, SymDim};
17#[cfg(test)]
18use tenferro_tensor::Tensor;
19use tenferro_tensor::{CacheStats, DType, SliceConfig, TensorScalar};
20
21use super::program::CompiledGraph;
22use crate::checkpoint::RetainedValue;
23#[cfg(test)]
24use crate::compiler::semantic_staging::stage_semantic_program;
25use crate::compiler::{lower_scoped_dim_expr, CompilerOptions};
26use crate::error::{Error, Result};
27use crate::extension_cache::{ExtensionCacheSelector, ExtensionCacheStore};
28use crate::metadata::registered_meta;
29use crate::program::{
30 CoreSemanticOp, FrozenProgram, ProgramInputSpec, ProgramShapeRelation, ProgramValueMetadata,
31 SemanticOpRef, SemanticProgramBuilder, ShapeGuard as ProgramShapeGuard,
32};
33use crate::shape_constraint::{discharge, LocalShapeConstraint, SlotScopedShapeConstraint};
34use crate::shape_infer::{infer_extension_output_meta, infer_output_shapes};
35use crate::trace::TracedGraph;
36use crate::traced::{try_concrete_shape, TracedTensor};
37
38#[derive(Clone)]
39struct InputDescriptor {
40 dtype: DType,
41 shape: Vec<usize>,
42 extent_identity: InputExtentIdentity,
43 default_tensor: Option<Arc<RetainedValue>>,
44 scalar_identity: Option<&'static str>,
46}
47
48#[derive(Clone, Copy)]
49enum InputExtentIdentity {
50 Concrete,
51 Symbolic,
52}
53
54impl InputDescriptor {
55 fn semantic_shape(&self, input_idx: usize) -> Vec<DimExpr> {
56 match self.extent_identity {
57 InputExtentIdentity::Concrete => DimExpr::from_concrete(&self.shape),
58 InputExtentIdentity::Symbolic => (0..self.shape.len())
59 .map(|axis| DimExpr::InputDim { input_idx, axis })
60 .collect(),
61 }
62 }
63
64 fn constraint_guard_shape(&self, input_idx: usize) -> Vec<DimExpr> {
65 if self.default_tensor.is_some() {
66 return input_dim_shape(input_idx, self.shape.len());
67 }
68 self.semantic_shape(input_idx)
69 }
70}
71
72fn input_dim_shape(input_idx: usize, rank: usize) -> Vec<DimExpr> {
73 (0..rank)
74 .map(|axis| DimExpr::InputDim { input_idx, axis })
75 .collect()
76}
77
78pub struct GraphCompiler {
95 extension_cache: ExtensionCacheStore,
96 compiler_options: CompilerOptions,
97}
98
99impl fmt::Debug for GraphCompiler {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 f.debug_struct("GraphCompiler")
102 .field("extension_cache_stats", &self.cache_stats())
103 .field("compiler_options", &self.compiler_options)
104 .field("extension_cache", &self.extension_cache)
105 .finish_non_exhaustive()
106 }
107}
108
109impl GraphCompiler {
110 pub fn new() -> Self {
121 Self {
122 extension_cache: ExtensionCacheStore::new(),
123 compiler_options: CompilerOptions::default(),
124 }
125 }
126
127 pub fn with_compiler_options(compiler_options: CompilerOptions) -> Self {
144 Self {
145 extension_cache: ExtensionCacheStore::new(),
146 compiler_options,
147 }
148 }
149
150 pub fn compile(&mut self, output: &TracedTensor) -> Result<CompiledGraph> {
173 self.compile_many(&[output])
174 }
175
176 pub fn compile_traced_graph(&mut self, graph: &TracedGraph) -> Result<CompiledGraph> {
189 self.compile_frozen(graph.frozen())
190 }
191
192 pub fn compile_frozen_program(&mut self, frozen: &FrozenProgram) -> Result<CompiledGraph> {
226 self.compile_frozen(frozen)
227 }
228
229 fn compile_frozen(&mut self, frozen: &FrozenProgram) -> Result<CompiledGraph> {
230 validate_bound_shape_guards(frozen)?;
231 Ok(CompiledGraph::new(
232 frozen.clone(),
233 self.compiler_options,
234 [],
235 ))
236 }
237
238 pub fn compile_many(&mut self, outputs: &[&TracedTensor]) -> Result<CompiledGraph> {
261 let all_inputs = collect_default_inputs(outputs)?;
262 self.compile_many_with_descriptors(
263 outputs,
264 &HashMap::new(),
265 &all_inputs,
266 None,
267 false,
268 false,
269 )
270 }
271
272 pub(crate) fn compile_ad_source(&mut self, output: &TracedTensor) -> Result<CompiledGraph> {
273 self.compile_ad_source_many(&[output])
274 }
275
276 pub(crate) fn compile_ad_source_many(
277 &mut self,
278 outputs: &[&TracedTensor],
279 ) -> Result<CompiledGraph> {
280 let all_inputs = collect_default_inputs(outputs)?;
281 self.compile_many_with_descriptors(outputs, &HashMap::new(), &all_inputs, None, true, true)
282 }
283
284 pub fn compile_with_input_specs(
311 &mut self,
312 output: &TracedTensor,
313 bindings: &[(&TracedTensor, DType, &[usize])],
314 ) -> Result<CompiledGraph> {
315 let mut binding_specs = HashMap::new();
316 let mut input_order = Vec::with_capacity(bindings.len());
317 for (index, (placeholder, dtype, shape)) in bindings.iter().enumerate() {
318 validate_placeholder_spec(index, placeholder, *dtype, shape)?;
319 let key = placeholder.input_key().ok_or(Error::UnexpectedBinding {
320 binding_index: index,
321 })?;
322 if binding_specs
323 .insert(
324 key.clone(),
325 InputDescriptor {
326 dtype: *dtype,
327 shape: (*shape).to_vec(),
328 extent_identity: InputExtentIdentity::Concrete,
329 default_tensor: None,
330 scalar_identity: None,
333 },
334 )
335 .is_some()
336 {
337 return Err(Error::DuplicateBinding {
338 input_key: format!("{:?}", key),
339 });
340 }
341 input_order.push(key);
342 }
343
344 self.compile_many_with_descriptors(
345 &[output],
346 &binding_specs,
347 output.inputs_map.as_ref(),
348 Some(&input_order),
349 false,
350 false,
351 )
352 }
353
354 pub fn compiler_options(&self) -> CompilerOptions {
366 self.compiler_options
367 }
368
369 pub fn set_compiler_options(&mut self, compiler_options: CompilerOptions) {
389 if self.compiler_options == compiler_options {
390 return;
391 }
392 self.compiler_options = compiler_options;
393 self.clear_extension_caches();
394 }
395
396 pub fn clear_extension_caches(&mut self) {
408 self.extension_cache.clear();
409 }
410
411 pub fn clear_caches(&mut self) {
423 self.clear_extension_caches();
424 }
425
426 pub fn cache_stats(&self) -> CacheStats {
438 self.extension_cache.stats(ExtensionCacheSelector::All)
439 }
440
441 pub fn extension_caches(&self) -> &ExtensionCacheStore {
452 &self.extension_cache
453 }
454
455 pub fn extension_caches_mut(&mut self) -> &mut ExtensionCacheStore {
466 &mut self.extension_cache
467 }
468
469 fn compile_many_with_descriptors(
470 &mut self,
471 outputs: &[&TracedTensor],
472 binding_specs: &HashMap<TensorInputKey, InputDescriptor>,
473 default_inputs: &HashMap<TensorInputKey, Arc<RetainedValue>>,
474 explicit_input_order: Option<&[TensorInputKey]>,
475 include_checkpoint_aliases: bool,
476 allow_unbound_placeholders: bool,
477 ) -> Result<CompiledGraph> {
478 let mut constraint_scopes = Vec::new();
479 let mut seen_constraint_scopes = std::collections::HashSet::new();
480 for output in outputs {
481 for scope in output.constraint_scopes.as_slice() {
482 if seen_constraint_scopes.insert(Arc::as_ptr(scope)) {
483 #[cfg(test)]
484 test_support::record_constraint_scope_clones(1);
485 constraint_scopes.push(Arc::clone(scope));
486 }
487 }
488 }
489
490 let mut roots = Vec::new();
491 let mut checkpoint_aliases = HashMap::new();
492 let mut output_keys = Vec::with_capacity(outputs.len());
493 for output in outputs {
494 roots.extend(output.resolve_roots());
495 if include_checkpoint_aliases && let Some(chain) = &output.checkpoint_chain {
496 roots.extend(chain.collect_graphs());
497 for (alias_key, target_key) in chain.collect_aliases() {
498 insert_checkpoint_alias(&mut checkpoint_aliases, alias_key, target_key)?;
499 }
500 }
501 output_keys.push(output.graph.values()[output.val].key.clone());
502 }
503
504 let view = resolve(roots);
505 let graph = if checkpoint_aliases.is_empty() {
506 materialize_merge(&view, &output_keys)
507 } else {
508 let checkpoint_alias_shapes = checkpoint_aliases
509 .keys()
510 .filter_map(|key| {
511 default_inputs
512 .get(key)
513 .map(|tensor| (key.clone(), tensor.shape().to_vec()))
514 })
515 .collect::<HashMap<_, _>>();
516 materialize_merge_with_input_aliases(
517 &view,
518 &output_keys,
519 &checkpoint_aliases,
520 &checkpoint_alias_shapes,
521 )
522 };
523 let mut compiled = compile(&graph);
524 prune_compiled_extension_outputs(&mut compiled)?;
525 let slot_by_key: HashMap<_, _> = graph
526 .values
527 .iter()
528 .enumerate()
529 .map(|(slot, value)| (value.key.clone(), slot))
530 .collect();
531 let mut scoped_constraints = Vec::new();
532 for scope in constraint_scopes {
533 for scoped in scope.constraints() {
534 let origin_slots: Vec<_> = scoped
535 .origins
536 .iter()
537 .filter_map(|key| slot_by_key.get(key).copied())
538 .collect();
539 if origin_slots.is_empty() {
540 continue;
541 }
542 let origin_instruction = origin_slots.iter().find_map(|&slot| {
543 graph
544 .values
545 .get(slot)
546 .and_then(|value| value.producer.map(|p| p.0))
547 });
548 let mut local = scoped.local.clone();
549 if let Some(instruction_index) = origin_instruction {
550 local.source = local.source.with_instruction(instruction_index);
551 }
552 let mut input_slots = Vec::with_capacity(scoped.inputs.len());
553 for (input_idx, key) in scoped.inputs.iter().enumerate() {
554 let Some(slot) = slot_by_key.get(key).copied() else {
555 return Err(Error::ShapeConstraintEvaluation {
556 family: local.source.family_id,
557 instruction_index: local.source.instruction_index,
558 relation: local.relation,
559 expression: format!("{:?}", local.lhs),
560 cause: crate::ShapeConstraintEvalError::MissingInput {
561 input_idx,
562 input_count: scoped.inputs.len(),
563 },
564 });
565 };
566 input_slots.push(slot);
567 }
568 scoped_constraints.push(SlotScopedShapeConstraint {
569 origin_slots,
570 input_slots,
571 local,
572 });
573 }
574 }
575
576 let mut descriptors = Vec::with_capacity(graph.inputs.len());
577 let mut input_keys = Vec::with_capacity(graph.inputs.len());
578 for key in &graph.inputs {
579 let ValueKey::Input(input_key) = key else {
580 return Err(Error::Internal(
581 "expected Input key in graph inputs".to_string(),
582 ));
583 };
584 let descriptor = descriptor_for_input(
585 input_key,
586 binding_specs,
587 default_inputs,
588 allow_unbound_placeholders,
589 )?;
590 descriptors.push(descriptor);
591 input_keys.push(input_key.clone());
592 }
593 if let Some(explicit_input_order) = explicit_input_order {
594 let input_position_by_key: HashMap<_, _> = graph
595 .inputs
596 .iter()
597 .enumerate()
598 .filter_map(|(position, key)| match key {
599 ValueKey::Input(key) => Some((key.clone(), position)),
600 _ => None,
601 })
602 .collect();
603 let mut ordered_positions = Vec::with_capacity(graph.inputs.len());
604 let mut selected_positions = vec![false; graph.inputs.len()];
605 for key in explicit_input_order {
606 if let Some(&position) = input_position_by_key.get(key) {
607 ordered_positions.push(position);
608 selected_positions[position] = true;
609 }
610 }
611 for (position, selected) in selected_positions.iter().enumerate() {
612 if !selected {
613 ordered_positions.push(position);
614 }
615 }
616 compiled.input_slots = ordered_positions
617 .iter()
618 .map(|&position| compiled.input_slots[position])
619 .collect();
620 descriptors = ordered_positions
621 .iter()
622 .map(|&position| descriptors[position].clone())
623 .collect();
624 input_keys = ordered_positions
625 .into_iter()
626 .map(|position| input_keys[position].clone())
627 .collect();
628 }
629
630 let semantic =
631 compile_materialized_semantic_program(&compiled, &descriptors, &scoped_constraints)?;
632 validate_bound_shape_guards(&semantic)?;
633 Ok(CompiledGraph::new(
634 semantic,
635 self.compiler_options,
636 input_keys,
637 ))
638 }
639}
640
641fn validate_bound_shape_guards(frozen: &FrozenProgram) -> Result<()> {
642 let input_shapes = compile_time_input_shapes(frozen)?;
643 for operation in frozen.program.operations() {
644 let fallback_family = match operation.op() {
645 SemanticOpRef::Core(_) => "tenferro-runtime.core.v1",
646 SemanticOpRef::Extension(extension) => extension.family_id(),
647 };
648 for guard in operation.shape_guards() {
649 if guard.source_family().is_some() {
650 continue;
651 }
652 validate_bound_shape_guard(guard, fallback_family, &input_shapes)?;
653 }
654 }
655 Ok(())
656}
657
658fn compile_time_input_shapes(frozen: &FrozenProgram) -> Result<Vec<Option<Vec<usize>>>> {
659 let metadata = frozen.input_metadata_with_bound_shapes();
660 frozen
661 .program
662 .inputs()
663 .iter()
664 .enumerate()
665 .map(|(input_idx, &input)| {
666 if let Some(tensor) = frozen.bindings.tensor_ref_for_input(input) {
667 return Ok(Some(tensor.shape().to_vec()));
668 }
669 let Some(metadata) = metadata.get(input_idx) else {
670 return Err(invalid_compiled_graph(format!(
671 "semantic input metadata index {input_idx} is outside metadata table"
672 )));
673 };
674 concrete_shape_from_input_metadata(metadata)
675 })
676 .collect()
677}
678
679fn concrete_shape_from_input_metadata(
680 metadata: &ProgramValueMetadata,
681) -> Result<Option<Vec<usize>>> {
682 let mut shape = Vec::with_capacity(metadata.shape().len());
683 for extent in metadata.shape() {
684 let ShapeExtent::Exact(expression) = extent else {
685 return Ok(None);
686 };
687 let Some(value) = evaluate_static_dim_expr(expression)? else {
688 return Ok(None);
689 };
690 shape.push(value);
691 }
692 Ok(Some(shape))
693}
694
695fn validate_bound_shape_guard(
696 guard: &ProgramShapeGuard,
697 fallback_family: &'static str,
698 input_shapes: &[Option<Vec<usize>>],
699) -> Result<()> {
700 let ProgramShapeRelation::Equal = guard.relation() else {
701 return Ok(());
702 };
703 let family = guard.source_family().unwrap_or(fallback_family);
704 let relation = ShapeRelation::Equal;
705 let lhs = evaluate_bound_shape_guard_expression(family, relation, guard.lhs(), input_shapes)?;
706 let rhs = evaluate_bound_shape_guard_expression(family, relation, guard.rhs(), input_shapes)?;
707 let (Some(lhs), Some(rhs)) = (lhs, rhs) else {
708 return Ok(());
709 };
710 if lhs == rhs {
711 return Ok(());
712 }
713 Err(Error::ShapeConstraintViolation {
714 family,
715 instruction_index: None,
716 relation,
717 lhs_expr: format!("{:?}", guard.lhs()),
718 rhs_expr: format!("{:?}", guard.rhs()),
719 lhs_value: lhs,
720 rhs_value: rhs,
721 })
722}
723
724fn evaluate_bound_shape_guard_expression(
725 family: &'static str,
726 relation: ShapeRelation,
727 expression: &DimExpr,
728 input_shapes: &[Option<Vec<usize>>],
729) -> Result<Option<usize>> {
730 evaluate_static_dim_expr_with_inputs(expression, input_shapes).map_err(|cause| {
731 Error::ShapeConstraintEvaluation {
732 family,
733 instruction_index: None,
734 relation,
735 expression: format!("{expression:?}"),
736 cause: cause.into(),
737 }
738 })
739}
740
741fn evaluate_static_dim_expr(expression: &DimExpr) -> Result<Option<usize>> {
742 evaluate_static_dim_expr_without_inputs(expression).map_err(|cause| {
743 Error::ShapeConstraintEvaluation {
744 family: "tenferro-runtime.input.v1",
745 instruction_index: None,
746 relation: ShapeRelation::Equal,
747 expression: format!("{expression:?}"),
748 cause: cause.into(),
749 }
750 })
751}
752
753fn evaluate_static_dim_expr_without_inputs(
754 expression: &DimExpr,
755) -> std::result::Result<Option<usize>, DimExprEvalError> {
756 match expression {
757 DimExpr::Const(value) => Ok(Some(*value)),
758 DimExpr::InputDim { .. } => Ok(None),
759 DimExpr::Add(a, b) => {
760 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
761 return Ok(None);
762 };
763 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
764 return Ok(None);
765 };
766 lhs.checked_add(rhs)
767 .map(Some)
768 .ok_or(DimExprEvalError::AddOverflow { lhs, rhs })
769 }
770 DimExpr::Sub(a, b) => {
771 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
772 return Ok(None);
773 };
774 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
775 return Ok(None);
776 };
777 lhs.checked_sub(rhs)
778 .map(Some)
779 .ok_or(DimExprEvalError::SubUnderflow { lhs, rhs })
780 }
781 DimExpr::Mul(a, b) => {
782 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
783 return Ok(None);
784 };
785 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
786 return Ok(None);
787 };
788 lhs.checked_mul(rhs)
789 .map(Some)
790 .ok_or(DimExprEvalError::MulOverflow { lhs, rhs })
791 }
792 DimExpr::FloorDiv(a, b) => {
793 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
794 return Ok(None);
795 };
796 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
797 return Ok(None);
798 };
799 if rhs == 0 {
800 return Err(DimExprEvalError::FloorDivByZero { lhs, rhs });
801 }
802 Ok(Some(lhs / rhs))
803 }
804 DimExpr::Min(a, b) => {
805 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
806 return Ok(None);
807 };
808 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
809 return Ok(None);
810 };
811 Ok(Some(lhs.min(rhs)))
812 }
813 DimExpr::Max(a, b) => {
814 let Some(lhs) = evaluate_static_dim_expr_without_inputs(a)? else {
815 return Ok(None);
816 };
817 let Some(rhs) = evaluate_static_dim_expr_without_inputs(b)? else {
818 return Ok(None);
819 };
820 Ok(Some(lhs.max(rhs)))
821 }
822 }
823}
824
825fn evaluate_static_dim_expr_with_inputs(
826 expression: &DimExpr,
827 input_shapes: &[Option<Vec<usize>>],
828) -> std::result::Result<Option<usize>, DimExprEvalError> {
829 match expression {
830 DimExpr::Const(value) => Ok(Some(*value)),
831 DimExpr::InputDim { input_idx, axis } => match input_shapes.get(*input_idx) {
832 Some(Some(shape)) => {
833 shape
834 .get(*axis)
835 .copied()
836 .map(Some)
837 .ok_or(DimExprEvalError::AxisOutOfBounds {
838 input_idx: *input_idx,
839 axis: *axis,
840 rank: shape.len(),
841 })
842 }
843 Some(None) => Ok(None),
844 None => Err(DimExprEvalError::InputOutOfBounds {
845 input_idx: *input_idx,
846 input_count: input_shapes.len(),
847 }),
848 },
849 DimExpr::Add(a, b) => {
850 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
851 return Ok(None);
852 };
853 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
854 return Ok(None);
855 };
856 lhs.checked_add(rhs)
857 .map(Some)
858 .ok_or(DimExprEvalError::AddOverflow { lhs, rhs })
859 }
860 DimExpr::Sub(a, b) => {
861 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
862 return Ok(None);
863 };
864 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
865 return Ok(None);
866 };
867 lhs.checked_sub(rhs)
868 .map(Some)
869 .ok_or(DimExprEvalError::SubUnderflow { lhs, rhs })
870 }
871 DimExpr::Mul(a, b) => {
872 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
873 return Ok(None);
874 };
875 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
876 return Ok(None);
877 };
878 lhs.checked_mul(rhs)
879 .map(Some)
880 .ok_or(DimExprEvalError::MulOverflow { lhs, rhs })
881 }
882 DimExpr::FloorDiv(a, b) => {
883 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
884 return Ok(None);
885 };
886 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
887 return Ok(None);
888 };
889 if rhs == 0 {
890 return Err(DimExprEvalError::FloorDivByZero { lhs, rhs });
891 }
892 Ok(Some(lhs / rhs))
893 }
894 DimExpr::Min(a, b) => {
895 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
896 return Ok(None);
897 };
898 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
899 return Ok(None);
900 };
901 Ok(Some(lhs.min(rhs)))
902 }
903 DimExpr::Max(a, b) => {
904 let Some(lhs) = evaluate_static_dim_expr_with_inputs(a, input_shapes)? else {
905 return Ok(None);
906 };
907 let Some(rhs) = evaluate_static_dim_expr_with_inputs(b, input_shapes)? else {
908 return Ok(None);
909 };
910 Ok(Some(lhs.max(rhs)))
911 }
912 }
913}
914
915fn compile_materialized_semantic_program(
916 compiled: &CompiledProgram<StdTensorOp>,
917 descriptors: &[InputDescriptor],
918 scoped_constraints: &[SlotScopedShapeConstraint],
919) -> Result<FrozenProgram> {
920 if compiled.input_slots.len() != descriptors.len() {
921 return Err(Error::runtime_state(
922 "graph_compile_semantic",
923 crate::ErrorPhase::Compile,
924 "materialized input count does not match semantic descriptors",
925 ));
926 }
927
928 let mut builder = SemanticProgramBuilder::new();
929 let mut values = vec![None; compiled.n_slots];
930 let mut slot_shapes = vec![None; compiled.n_slots];
931 let mut guard_slot_shapes = vec![None; compiled.n_slots];
932 let mut slot_dtypes = vec![None; compiled.n_slots];
933 for (input_idx, (&slot, descriptor)) in compiled.input_slots.iter().zip(descriptors).enumerate()
934 {
935 let Some(value_slot) = values.get_mut(slot) else {
936 return Err(invalid_compiled_graph(format!(
937 "semantic input slot {slot} is outside slot table of length {}",
938 compiled.n_slots
939 )));
940 };
941 let semantic_shape = descriptor.semantic_shape(input_idx);
942 let spec = match descriptor.scalar_identity {
943 Some(identity) => ProgramInputSpec::new(descriptor.dtype, semantic_shape.clone())
944 .with_scalar_identity(identity),
945 None => ProgramInputSpec::new(descriptor.dtype, semantic_shape.clone()),
946 };
947 let value = builder.input(spec).map_err(semantic_build_error)?;
948 if let Some(tensor) = &descriptor.default_tensor {
949 builder
950 .bind_input_retained(value, Arc::clone(tensor))
951 .map_err(semantic_build_error)?;
952 }
953 *value_slot = Some(value);
954 slot_shapes[slot] = Some(semantic_shape);
955 guard_slot_shapes[slot] = Some(descriptor.constraint_guard_shape(input_idx));
956 slot_dtypes[slot] = Some(descriptor.dtype);
957 }
958
959 for instruction in &compiled.instructions {
960 let inputs = instruction
961 .inputs
962 .iter()
963 .map(|&slot| {
964 values.get(slot).and_then(|value| *value).ok_or_else(|| {
965 invalid_compiled_graph(format!(
966 "semantic operation input slot {slot} is unavailable"
967 ))
968 })
969 })
970 .collect::<Result<Vec<_>>>()?;
971 let guard_input_shapes = instruction
972 .inputs
973 .iter()
974 .map(|&slot| {
975 guard_slot_shapes
976 .get(slot)
977 .and_then(|shape| shape.as_deref())
978 .ok_or_else(|| {
979 invalid_compiled_graph(format!(
980 "semantic operation guard-shape input slot {slot} is unavailable"
981 ))
982 })
983 })
984 .collect::<Result<Vec<_>>>()?;
985 let guard_output_shapes = match &instruction.operation {
986 StdTensorOp::Extension(extension) => {
987 let input_dtypes = instruction
988 .inputs
989 .iter()
990 .map(|&slot| {
991 slot_dtypes
992 .get(slot)
993 .and_then(|dtype| *dtype)
994 .ok_or_else(|| {
995 invalid_compiled_graph(format!(
996 "semantic operation dtype input slot {slot} is unavailable"
997 ))
998 })
999 })
1000 .collect::<Result<Vec<_>>>()?;
1001 infer_extension_output_meta(extension.as_ref(), &input_dtypes, &guard_input_shapes)?
1002 .into_iter()
1003 .map(|(_dtype, shape)| shape)
1004 .collect::<Vec<_>>()
1005 }
1006 operation => infer_output_shapes(operation, &guard_input_shapes)?,
1007 };
1008 let outputs = match &instruction.operation {
1009 StdTensorOp::Extension(extension) => builder
1010 .add_extension(Arc::clone(extension), &inputs)
1011 .map_err(semantic_build_error)?,
1012 operation => builder
1013 .add_op(
1014 CoreSemanticOp::try_from(operation).map_err(|source| {
1015 Error::runtime_state_source(
1016 "graph_compile_semantic",
1017 crate::ErrorPhase::Compile,
1018 source,
1019 )
1020 })?,
1021 &inputs,
1022 )
1023 .map_err(semantic_build_error)?,
1024 };
1025 if outputs.len() != instruction.outputs.len() {
1026 return Err(invalid_compiled_graph(format!(
1027 "semantic operation produced {} outputs for {} materialized slots",
1028 outputs.len(),
1029 instruction.outputs.len()
1030 )));
1031 }
1032 if guard_output_shapes.len() != instruction.outputs.len() {
1033 return Err(invalid_compiled_graph(format!(
1034 "semantic operation inferred {} guard shapes for {} materialized slots",
1035 guard_output_shapes.len(),
1036 instruction.outputs.len()
1037 )));
1038 }
1039 for ((&slot, &value), guard_shape) in instruction
1040 .outputs
1041 .iter()
1042 .zip(outputs.iter())
1043 .zip(guard_output_shapes.iter())
1044 {
1045 let Some(value_slot) = values.get_mut(slot) else {
1046 return Err(invalid_compiled_graph(format!(
1047 "semantic output slot {slot} is outside slot table of length {}",
1048 compiled.n_slots
1049 )));
1050 };
1051 if value_slot.replace(value).is_some() {
1052 return Err(invalid_compiled_graph(format!(
1053 "semantic output slot {slot} has multiple producers"
1054 )));
1055 }
1056 let metadata = builder
1057 .value_metadata(value)
1058 .map_err(semantic_build_error)?;
1059 slot_shapes[slot] = Some(
1060 metadata
1061 .shape()
1062 .iter()
1063 .enumerate()
1064 .map(|(axis, extent)| match extent {
1065 tenferro_ops::ShapeExtent::Exact(expression)
1066 | tenferro_ops::ShapeExtent::UpperBound(expression) => expression.clone(),
1067 tenferro_ops::ShapeExtent::Unknown => DimExpr::InputDim {
1068 input_idx: slot,
1069 axis,
1070 },
1071 })
1072 .collect(),
1073 );
1074 guard_slot_shapes[slot] = Some(guard_shape.clone());
1075 slot_dtypes[slot] = Some(metadata.dtype());
1076 }
1077 }
1078
1079 for scoped in scoped_constraints {
1080 let target = scoped
1081 .origin_slots
1082 .iter()
1083 .find_map(|&slot| values.get(slot).and_then(|value| *value))
1084 .ok_or_else(|| {
1085 invalid_compiled_graph(
1086 "semantic shape constraint has no available origin".to_string(),
1087 )
1088 })?;
1089 let lhs = lower_scoped_dim_expr(
1090 &scoped.local.lhs,
1091 &scoped.input_slots,
1092 &guard_slot_shapes,
1093 &scoped.local,
1094 )?;
1095 let rhs = lower_scoped_dim_expr(
1096 &scoped.local.rhs,
1097 &scoped.input_slots,
1098 &guard_slot_shapes,
1099 &scoped.local,
1100 )?;
1101 let relation = match scoped.local.relation {
1102 tenferro_ops::ShapeRelation::Equal => ProgramShapeRelation::Equal,
1103 };
1104 let lowered = LocalShapeConstraint {
1105 source: scoped.local.source.clone(),
1106 relation: scoped.local.relation,
1107 lhs,
1108 rhs,
1109 };
1110 let retained_guards = discharge(vec![lowered])?;
1111 if retained_guards.is_empty() {
1112 continue;
1113 }
1114 let guards = retained_guards.into_iter().map(|guard| {
1115 ProgramShapeGuard::new(relation, guard.lhs, guard.rhs)
1116 .with_source_family(guard.source.family_id)
1117 });
1118 builder
1119 .add_shape_guards_to_output(target, guards)
1120 .map_err(semantic_build_error)?;
1121 }
1122
1123 let outputs = compiled
1124 .output_slots
1125 .iter()
1126 .map(|&slot| {
1127 values.get(slot).and_then(|value| *value).ok_or_else(|| {
1128 invalid_compiled_graph(format!(
1129 "semantic program output slot {slot} is unavailable"
1130 ))
1131 })
1132 })
1133 .collect::<Result<Vec<_>>>()?;
1134 builder.finish(&outputs).map_err(|source| {
1135 Error::runtime_state_source("graph_compile_semantic", crate::ErrorPhase::Compile, source)
1136 })
1137}
1138
1139fn semantic_build_error(source: crate::program::ProgramBuildError) -> Error {
1140 Error::runtime_state_source("graph_compile_semantic", crate::ErrorPhase::Compile, source)
1141}
1142
1143impl Default for GraphCompiler {
1144 fn default() -> Self {
1145 Self::new()
1146 }
1147}
1148
1149fn collect_default_inputs(
1150 outputs: &[&TracedTensor],
1151) -> Result<HashMap<TensorInputKey, Arc<RetainedValue>>> {
1152 let mut all_inputs = HashMap::new();
1153 for output in outputs {
1154 for (key, tensor) in output.inputs_map.iter() {
1155 if let Some(existing) = all_inputs.get(key) {
1156 if !default_tensors_equivalent(existing, tensor) {
1157 return Err(Error::DuplicateBinding {
1158 input_key: format!("{:?}", key),
1159 });
1160 }
1161 continue;
1162 }
1163 all_inputs.insert(key.clone(), tensor.clone());
1164 }
1165 }
1166 Ok(all_inputs)
1167}
1168
1169fn insert_checkpoint_alias(
1170 aliases: &mut HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1171 alias_key: TensorInputKey,
1172 target_key: ValueKey<StdTensorOp>,
1173) -> Result<()> {
1174 if let Some(existing) = aliases.get(&alias_key) {
1175 if existing != &target_key {
1176 return Err(Error::Internal(format!(
1177 "checkpoint alias {alias_key:?} targets both {existing:?} and {target_key:?}"
1178 )));
1179 }
1180 return Ok(());
1181 }
1182 aliases.insert(alias_key, target_key);
1183 Ok(())
1184}
1185
1186struct AliasAwareMaterializer<'a> {
1187 view: &'a ResolvedView<StdTensorOp>,
1188 aliases: &'a HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1189 alias_shapes: &'a HashMap<TensorInputKey, Vec<usize>>,
1190 val_map: HashMap<ValueKey<StdTensorOp>, usize>,
1191 op_map: HashMap<Arc<OperationKey<StdTensorOp>>, usize>,
1192 values: Vec<MaterializedValue<StdTensorOp>>,
1193 operations: Vec<MaterializedOperation<StdTensorOp>>,
1194 input_keys: Vec<ValueKey<StdTensorOp>>,
1195}
1196
1197impl<'a> AliasAwareMaterializer<'a> {
1198 fn new(
1199 view: &'a ResolvedView<StdTensorOp>,
1200 aliases: &'a HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1201 alias_shapes: &'a HashMap<TensorInputKey, Vec<usize>>,
1202 ) -> Self {
1203 Self {
1204 view,
1205 aliases,
1206 alias_shapes,
1207 val_map: HashMap::new(),
1208 op_map: HashMap::new(),
1209 values: Vec::new(),
1210 operations: Vec::new(),
1211 input_keys: Vec::new(),
1212 }
1213 }
1214
1215 fn visit(&mut self, key: &ValueKey<StdTensorOp>) -> usize {
1216 if let Some(&index) = self.val_map.get(key) {
1217 return index;
1218 }
1219 if let Some(target) = self.alias_target(key).cloned() {
1220 let index = self.visit(&target);
1221 let index = self.refine_alias_to_checkpoint_shape(key, index);
1222 self.val_map.insert(key.clone(), index);
1223 return index;
1224 }
1225
1226 let resolved = self.view.resolve_value(key);
1227 assert!(
1228 resolved.is_some(),
1229 "key not found in resolved view: {:?}",
1230 key
1231 );
1232 match resolved {
1233 Some(ValueDef::Input { .. }) => self.materialize_input(key),
1234 Some(ValueDef::Produced {
1235 operation,
1236 input_keys,
1237 role,
1238 output_slot,
1239 }) => self.materialize_produced(operation, input_keys, role, output_slot),
1240 None => unreachable!("asserted above"),
1241 }
1242 }
1243
1244 fn alias_target(&self, key: &ValueKey<StdTensorOp>) -> Option<&ValueKey<StdTensorOp>> {
1245 let ValueKey::Input(input_key) = key else {
1246 return None;
1247 };
1248 self.aliases.get(input_key)
1249 }
1250
1251 fn refine_alias_to_checkpoint_shape(
1252 &mut self,
1253 key: &ValueKey<StdTensorOp>,
1254 target_index: usize,
1255 ) -> usize {
1256 let ValueKey::Input(input_key) = key else {
1257 return target_index;
1258 };
1259 let Some(shape) = self.alias_shapes.get(input_key) else {
1260 return target_index;
1261 };
1262 let target_key = self.values[target_index].key.clone();
1263 let rank = shape.len();
1264 self.materialize_produced(
1265 StdTensorOp::Slice(SliceConfig {
1266 starts: vec![0; rank],
1267 limits: shape.clone(),
1268 strides: vec![1; rank],
1269 }),
1270 vec![target_key],
1271 computegraph::types::OperationRole::Primary,
1272 0,
1273 )
1274 }
1275
1276 fn materialize_input(&mut self, key: &ValueKey<StdTensorOp>) -> usize {
1277 let index = self.values.len();
1278 self.values.push(MaterializedValue {
1279 key: key.clone(),
1280 producer: None,
1281 });
1282 self.val_map.insert(key.clone(), index);
1283 self.input_keys.push(key.clone());
1284 index
1285 }
1286
1287 fn materialize_produced(
1288 &mut self,
1289 operation: StdTensorOp,
1290 input_keys: Vec<ValueKey<StdTensorOp>>,
1291 role: computegraph::types::OperationRole,
1292 output_slot: usize,
1293 ) -> usize {
1294 let op_key = Arc::new(OperationKey::new(
1295 operation.clone(),
1296 input_keys.clone(),
1297 role.clone(),
1298 ));
1299
1300 if self.op_map.contains_key(&op_key) {
1301 let output_key = ValueKey::Derived {
1302 operation: op_key,
1303 output_slot: output_slot as u8,
1304 };
1305 let val_index = self.val_map.get(&output_key).copied();
1306 assert!(
1307 val_index.is_some(),
1308 "materialized op {:?} is missing output slot {}",
1309 operation,
1310 output_slot
1311 );
1312 return match val_index {
1313 Some(index) => index,
1314 None => unreachable!("asserted above"),
1315 };
1316 }
1317
1318 let materialized_inputs = input_keys.iter().map(|input| self.visit(input)).collect();
1319 let op_index = self.operations.len();
1320 self.op_map.insert(Arc::clone(&op_key), op_index);
1321 self.operations.push(MaterializedOperation {
1322 operation: operation.clone(),
1323 inputs: materialized_inputs,
1324 outputs: Vec::with_capacity(operation.output_count()),
1325 role,
1326 });
1327
1328 for slot in 0..operation.output_count() {
1329 let output_key = ValueKey::Derived {
1330 operation: Arc::clone(&op_key),
1331 output_slot: slot as u8,
1332 };
1333 let val_index = self.values.len();
1334 self.values.push(MaterializedValue {
1335 key: output_key.clone(),
1336 producer: Some((op_index, slot)),
1337 });
1338 self.val_map.insert(output_key, val_index);
1339 self.operations[op_index].outputs.push(val_index);
1340 }
1341
1342 self.operations[op_index].outputs[output_slot]
1343 }
1344}
1345
1346fn materialize_merge_with_input_aliases(
1347 view: &ResolvedView<StdTensorOp>,
1348 outputs: &[ValueKey<StdTensorOp>],
1349 aliases: &HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
1350 alias_shapes: &HashMap<TensorInputKey, Vec<usize>>,
1351) -> MaterializedGraph<StdTensorOp> {
1352 let mut materializer = AliasAwareMaterializer::new(view, aliases, alias_shapes);
1353 let mut materialized_outputs = Vec::with_capacity(outputs.len());
1354
1355 for output in outputs {
1356 let output_slot = materializer.visit(output);
1357 materialized_outputs.push(materializer.values[output_slot].key.clone());
1358 }
1359
1360 MaterializedGraph {
1361 values: materializer.values,
1362 operations: materializer.operations,
1363 inputs: materializer.input_keys,
1364 outputs: materialized_outputs,
1365 }
1366}
1367
1368fn validate_placeholder_spec(
1369 index: usize,
1370 placeholder: &TracedTensor,
1371 dtype: DType,
1372 shape: &[usize],
1373) -> Result<()> {
1374 if placeholder.data.is_some() {
1375 return Err(Error::UnexpectedBinding {
1376 binding_index: index,
1377 });
1378 }
1379 placeholder.input_key().ok_or(Error::UnexpectedBinding {
1380 binding_index: index,
1381 })?;
1382
1383 if placeholder.dtype != dtype {
1384 return Err(Error::PlaceholderDtypeMismatch {
1385 expected: placeholder.dtype,
1386 actual: dtype,
1387 });
1388 }
1389 validate_placeholder_shape(placeholder, shape)
1390}
1391
1392fn validate_placeholder_shape(placeholder: &TracedTensor, shape: &[usize]) -> Result<()> {
1393 match try_concrete_shape(placeholder) {
1394 Some(expected_shape) => {
1395 if expected_shape.as_slice() != shape {
1396 return Err(Error::PlaceholderShapeMismatch {
1397 expected: expected_shape,
1398 actual: shape.to_vec(),
1399 });
1400 }
1401 }
1402 None => {
1403 if placeholder.rank != shape.len() {
1404 return Err(Error::PlaceholderRankMismatch {
1405 expected: placeholder.rank,
1406 actual: shape.len(),
1407 });
1408 }
1409 }
1410 }
1411 Ok(())
1412}
1413
1414fn descriptor_for_input(
1415 key: &TensorInputKey,
1416 binding_specs: &HashMap<TensorInputKey, InputDescriptor>,
1417 default_inputs: &HashMap<TensorInputKey, Arc<RetainedValue>>,
1418 allow_unbound_placeholders: bool,
1419) -> Result<InputDescriptor> {
1420 if let Some(tensor) = default_inputs.get(key) {
1421 return Ok(InputDescriptor {
1422 dtype: tensor.dtype(),
1423 shape: tensor.shape().to_vec(),
1424 extent_identity: default_input_extent_identity(key, tensor)?,
1425 default_tensor: Some(tensor.clone()),
1426 scalar_identity: registered_meta(&ValueKey::Input(key.clone()))
1429 .ok()
1430 .and_then(|metadata| metadata.scalar_identity()),
1431 });
1432 }
1433 if let Some(spec) = binding_specs.get(key) {
1434 return Ok(spec.clone());
1435 }
1436 if allow_unbound_placeholders {
1437 return descriptor_for_unbound_input(key);
1438 }
1439 Err(Error::UnboundPlaceholder {
1440 input_key: format!("{:?}", key),
1441 })
1442}
1443
1444fn descriptor_for_unbound_input(key: &TensorInputKey) -> Result<InputDescriptor> {
1445 let metadata = registered_meta(&ValueKey::Input(key.clone()))?;
1446 if let Some(shape) = metadata
1447 .exact_shape()
1448 .as_deref()
1449 .and_then(concrete_shape_from_sym_dims)
1450 {
1451 return Ok(InputDescriptor {
1452 dtype: metadata.dtype,
1453 shape,
1454 extent_identity: InputExtentIdentity::Concrete,
1455 default_tensor: None,
1456 scalar_identity: metadata.scalar_identity(),
1457 });
1458 }
1459 Ok(InputDescriptor {
1460 dtype: metadata.dtype,
1461 shape: vec![0; metadata.rank()],
1462 extent_identity: InputExtentIdentity::Symbolic,
1463 default_tensor: None,
1464 scalar_identity: metadata.scalar_identity(),
1465 })
1466}
1467
1468fn concrete_shape_from_sym_dims(shape: &[SymDim]) -> Option<Vec<usize>> {
1469 shape.iter().map(SymDim::constant_value).collect()
1470}
1471
1472fn default_input_extent_identity(
1473 key: &TensorInputKey,
1474 tensor: &RetainedValue,
1475) -> Result<InputExtentIdentity> {
1476 let metadata = registered_meta(&ValueKey::Input(key.clone()))?;
1477 let exact_shape = metadata.exact_shape();
1478 if metadata.dtype == tensor.dtype()
1479 && exact_shape_matches_tensor_shape(exact_shape.as_deref(), tensor.shape())
1480 {
1481 Ok(InputExtentIdentity::Concrete)
1482 } else {
1483 Ok(InputExtentIdentity::Symbolic)
1484 }
1485}
1486
1487fn exact_shape_matches_tensor_shape(
1488 shape: Option<&[tenferro_ops::SymDim]>,
1489 tensor_shape: &[usize],
1490) -> bool {
1491 let Some(shape) = shape else {
1492 return false;
1493 };
1494 shape.len() == tensor_shape.len()
1495 && shape
1496 .iter()
1497 .zip(tensor_shape)
1498 .all(|(dim, &extent)| dim.constant_value() == Some(extent))
1499}
1500
1501fn prune_compiled_extension_outputs(prog: &mut CompiledProgram<StdTensorOp>) -> Result<()> {
1502 let mut live_slots = vec![false; prog.n_slots];
1503 for &slot in &prog.output_slots {
1504 let Some(live) = live_slots.get_mut(slot) else {
1505 return Err(invalid_compiled_graph(format!(
1506 "program output slot {slot} is outside slot table of length {}",
1507 prog.n_slots
1508 )));
1509 };
1510 *live = true;
1511 }
1512
1513 for instr in prog.instructions.iter_mut().rev() {
1514 let live_outputs = instr
1515 .outputs
1516 .iter()
1517 .map(|&slot| {
1518 live_slots.get(slot).copied().ok_or_else(|| {
1519 invalid_compiled_graph(format!(
1520 "instruction output slot {slot} is outside slot table of length {}",
1521 prog.n_slots
1522 ))
1523 })
1524 })
1525 .collect::<Result<Vec<_>>>()?;
1526
1527 if let StdTensorOp::Extension(ext) = &instr.operation
1528 && let Some(pruned) = ext.prune_outputs(&live_outputs)
1529 {
1530 let kept_outputs = instr
1531 .outputs
1532 .iter()
1533 .zip(live_outputs.iter())
1534 .filter_map(|(&slot, &live)| live.then_some(slot))
1535 .collect::<Vec<_>>();
1536 if pruned.output_count() != kept_outputs.len() {
1537 return Err(invalid_compiled_graph(format!(
1538 "extension family_id={:?} pruned to {} outputs for {} live slots",
1539 ext.family_id(),
1540 pruned.output_count(),
1541 kept_outputs.len()
1542 )));
1543 }
1544 instr.operation = StdTensorOp::Extension(pruned);
1545 instr.outputs = kept_outputs;
1546 }
1547
1548 if live_outputs.iter().any(|&live| live) {
1549 for &slot in &instr.inputs {
1550 let Some(live) = live_slots.get_mut(slot) else {
1551 return Err(invalid_compiled_graph(format!(
1552 "instruction input slot {slot} is outside slot table of length {}",
1553 prog.n_slots
1554 )));
1555 };
1556 *live = true;
1557 }
1558 }
1559 }
1560
1561 Ok(())
1562}
1563
1564fn invalid_compiled_graph(message: impl Into<String>) -> Error {
1565 Error::Internal(message.into())
1566}
1567
1568fn default_tensors_equivalent(lhs: &Arc<RetainedValue>, rhs: &Arc<RetainedValue>) -> bool {
1569 if Arc::ptr_eq(lhs, rhs) {
1570 return true;
1571 }
1572 if lhs.dtype() != rhs.dtype() || lhs.shape() != rhs.shape() {
1573 return false;
1574 }
1575 match lhs.dtype() {
1576 DType::F32 => default_slices_equivalent::<f32>(lhs, rhs),
1577 DType::F64 => default_slices_equivalent::<f64>(lhs, rhs),
1578 DType::I32 => default_slices_equivalent::<i32>(lhs, rhs),
1579 DType::I64 => default_slices_equivalent::<i64>(lhs, rhs),
1580 DType::Bool => default_slices_equivalent::<bool>(lhs, rhs),
1581 DType::C32 => default_slices_equivalent::<Complex32>(lhs, rhs),
1582 DType::C64 => default_slices_equivalent::<Complex64>(lhs, rhs),
1583 DType::External(_) => false,
1586 }
1587}
1588
1589fn default_slices_equivalent<T: TensorScalar + PartialEq>(
1590 lhs: &RetainedValue,
1591 rhs: &RetainedValue,
1592) -> bool {
1593 let (Ok(lhs), Ok(rhs)) = (lhs.tensor_read(), rhs.tensor_read()) else {
1594 return false;
1595 };
1596 let lhs = lhs.tensor_view();
1597 let rhs = rhs.tensor_view();
1598 match (lhs.as_slice::<T>(), rhs.as_slice::<T>()) {
1599 (Ok(lhs), Ok(rhs)) => lhs == rhs,
1600 _ => false,
1603 }
1604}
1605
1606#[cfg(test)]
1607mod constraint_scope_tests;
1608
1609#[cfg(test)]
1610mod test_support;
1611
1612#[cfg(test)]
1613mod tests {
1614 use super::*;
1615 use std::any::Any;
1616 use std::hash::Hasher;
1617 use std::sync::Arc;
1618 use tenferro_ops::{
1619 ext_op::{ExtensionAliasDeclaration, ExtensionEffectDeclaration, ExtensionOp},
1620 SymDim,
1621 };
1622 use tenferro_tensor::{
1623 BackendStorageHandle, DeviceId, DeviceKind, GpuBackendKind, MemoryKind, Placement,
1624 StorageBuffer, TypedTensor,
1625 };
1626
1627 #[test]
1628 fn compile_publishes_semantic_program_and_separate_default_bindings() {
1629 let input = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1630 let output = input.neg().unwrap();
1631
1632 let program = GraphCompiler::new().compile(&output).unwrap();
1633
1634 assert_eq!(program.program().inputs().len(), 1);
1635 assert_eq!(program.program().outputs().len(), 1);
1636 assert_eq!(program.program().operations().count(), 1);
1637 assert_eq!(program.bindings().len(), 1);
1638 assert_eq!(
1639 program
1640 .program()
1641 .value_metadata(program.program().inputs()[0])
1642 .unwrap()
1643 .shape(),
1644 &[ShapeExtent::Exact(DimExpr::Const(2))]
1645 );
1646 assert_eq!(
1647 program
1648 .program()
1649 .value_metadata(program.program().outputs()[0])
1650 .unwrap()
1651 .dtype(),
1652 DType::F64
1653 );
1654 }
1655
1656 #[test]
1657 fn compile_preserves_symbolic_default_input_extent_identity() {
1658 let tensor = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1659 let input = TracedTensor::from_tensor_symbolic_shape(tensor).unwrap();
1660 let output = input.neg().unwrap();
1661
1662 let program = GraphCompiler::new().compile(&output).unwrap();
1663
1664 assert!(matches!(
1665 program
1666 .program()
1667 .value_metadata(program.program().inputs()[0])
1668 .unwrap()
1669 .shape(),
1670 [ShapeExtent::Exact(DimExpr::InputDim {
1671 input_idx: 0,
1672 axis: 0
1673 })]
1674 ));
1675 }
1676
1677 #[test]
1678 fn compile_many_rejects_conflicting_default_inputs_for_same_key() {
1679 let x = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
1680 let y1 = x.neg().unwrap();
1681 let mut y2 = x.neg().unwrap();
1682 let key = x.input_key().expect("concrete traced tensor has input key");
1683 let replacement = Arc::new(RetainedValue::from_tensor(
1684 Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap(),
1685 ));
1686 let mut inputs = (*y2.inputs_map).clone();
1687 inputs.insert(key.clone(), replacement);
1688 y2.inputs_map = Arc::new(inputs);
1689
1690 let err = GraphCompiler::new().compile_many(&[&y1, &y2]).unwrap_err();
1691
1692 assert!(matches!(
1693 err,
1694 Error::DuplicateBinding { ref input_key } if input_key.contains(&format!("{key:?}"))
1695 ));
1696 }
1697
1698 #[test]
1699 fn default_tensors_equivalent_rejects_distinct_backend_buffers() {
1700 let placement = Placement {
1701 memory_kind: MemoryKind::Device,
1702 device: Some(DeviceId {
1703 kind: DeviceKind::Gpu(GpuBackendKind::Cuda),
1704 ordinal: 0,
1705 }),
1706 cpu_affinity: None,
1707 };
1708 let lhs = Arc::new(RetainedValue::from_tensor(Tensor::from_typed::<f64>(
1709 TypedTensor::from_buffer_col_major(
1710 vec![2],
1711 StorageBuffer::Backend(Box::new(BackendStorageHandle::<f64>::new_with_len(1, 2))),
1712 placement.clone(),
1713 )
1714 .unwrap(),
1715 )));
1716 let rhs = Arc::new(RetainedValue::from_tensor(Tensor::from_typed::<f64>(
1717 TypedTensor::from_buffer_col_major(
1718 vec![2],
1719 StorageBuffer::Backend(Box::new(BackendStorageHandle::<f64>::new_with_len(2, 2))),
1720 placement,
1721 )
1722 .unwrap(),
1723 )));
1724
1725 assert!(
1726 !default_tensors_equivalent(&lhs, &rhs),
1727 "distinct backend-resident default tensors must not compare equal just because both are unreadable on host"
1728 );
1729 assert!(default_tensors_equivalent(&lhs, &lhs));
1730 }
1731
1732 #[test]
1733 fn compile_frozen_program_rejects_static_unbound_shape_guard_mismatch() {
1734 let mut builder = SemanticProgramBuilder::new();
1735 let lhs = builder
1736 .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
1737 .unwrap();
1738 let rhs = builder
1739 .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(3)]))
1740 .unwrap();
1741 let output = builder.add_op(CoreSemanticOp::Neg, &[lhs]).unwrap()[0];
1742 builder
1743 .add_shape_guards_to_output(
1744 output,
1745 [ProgramShapeGuard::new(
1746 ProgramShapeRelation::Equal,
1747 DimExpr::InputDim {
1748 input_idx: 0,
1749 axis: 0,
1750 },
1751 DimExpr::InputDim {
1752 input_idx: 1,
1753 axis: 0,
1754 },
1755 )],
1756 )
1757 .unwrap();
1758 let frozen = builder.finish(&[output]).unwrap();
1759
1760 let err = GraphCompiler::new()
1761 .compile_frozen_program(&frozen)
1762 .unwrap_err();
1763
1764 assert!(matches!(
1765 err,
1766 Error::ShapeConstraintViolation {
1767 lhs_value: 2,
1768 rhs_value: 3,
1769 ..
1770 }
1771 ));
1772 let _ = rhs;
1773 }
1774
1775 #[test]
1776 fn compile_frozen_program_defers_dynamic_unbound_shape_guard() {
1777 let mut builder = SemanticProgramBuilder::new();
1778 let lhs = builder
1779 .input(ProgramInputSpec::new(DType::F64, [DimExpr::Const(2)]))
1780 .unwrap();
1781 let rhs = builder
1782 .input(ProgramInputSpec::from_metadata(
1783 ProgramValueMetadata::from_extents(DType::F64, [ShapeExtent::Unknown]),
1784 ))
1785 .unwrap();
1786 let output = builder.add_op(CoreSemanticOp::Neg, &[lhs]).unwrap()[0];
1787 builder
1788 .add_shape_guards_to_output(
1789 output,
1790 [ProgramShapeGuard::new(
1791 ProgramShapeRelation::Equal,
1792 DimExpr::InputDim {
1793 input_idx: 0,
1794 axis: 0,
1795 },
1796 DimExpr::InputDim {
1797 input_idx: 1,
1798 axis: 0,
1799 },
1800 )],
1801 )
1802 .unwrap();
1803 let frozen = builder.finish(&[output]).unwrap();
1804
1805 GraphCompiler::new()
1806 .compile_frozen_program(&frozen)
1807 .unwrap();
1808 let _ = rhs;
1809 }
1810
1811 #[derive(Clone, Debug, PartialEq, Eq)]
1812 struct PrunableTestOp {
1813 pruned: bool,
1814 }
1815
1816 impl ExtensionOp for PrunableTestOp {
1817 fn family_id(&self) -> &'static str {
1818 "tenferro-runtime.test-prunable.v1"
1819 }
1820
1821 fn payload_hash(&self, hasher: &mut dyn Hasher) {
1822 hasher.write_u8(u8::from(self.pruned));
1823 }
1824
1825 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
1826 other
1827 .as_any()
1828 .downcast_ref::<Self>()
1829 .is_some_and(|that| self == that)
1830 }
1831
1832 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
1833 Arc::new(self.clone())
1834 }
1835
1836 fn as_any(&self) -> &dyn Any {
1837 self
1838 }
1839
1840 fn input_count(&self) -> usize {
1841 1
1842 }
1843
1844 fn output_count(&self) -> usize {
1845 if self.pruned {
1846 1
1847 } else {
1848 3
1849 }
1850 }
1851
1852 fn semantic_effects(&self) -> ExtensionEffectDeclaration<'_> {
1853 ExtensionEffectDeclaration::Declared(&[])
1854 }
1855
1856 fn semantic_aliases(&self) -> ExtensionAliasDeclaration<'_> {
1857 ExtensionAliasDeclaration::AllFresh
1858 }
1859
1860 fn infer_output_meta(
1861 &self,
1862 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
1863 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1864 let dtype = ctx.input_dtype(0)?;
1865 let shape = ctx.input_shape(0)?.to_vec();
1866 Ok((0..self.output_count())
1867 .map(|_| (dtype, shape.clone()))
1868 .collect())
1869 }
1870
1871 fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
1872 (!self.pruned && live_outputs == [false, true, false])
1873 .then(|| Arc::new(Self { pruned: true }) as Arc<dyn ExtensionOp>)
1874 }
1875 }
1876
1877 #[test]
1878 fn compile_prunes_extension_outputs_with_replacement_op() {
1879 let input = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1880 let outputs =
1881 crate::extension::apply(Arc::new(PrunableTestOp { pruned: false }), &[&input]).unwrap();
1882
1883 let program = GraphCompiler::new().compile(&outputs[1]).unwrap();
1884 let staging =
1885 stage_semantic_program(program.program(), program.compiler_options()).unwrap();
1886 let pruned_instruction = staging
1887 .instructions
1888 .iter()
1889 .find_map(|inst| match &inst.op {
1890 crate::exec::ExecOp::Extension(op)
1891 if op.family_id() == "tenferro-runtime.test-prunable.v1" =>
1892 {
1893 Some((
1894 inst.output_slots.clone(),
1895 format!("{op:?}"),
1896 op.output_count(),
1897 ))
1898 }
1899 _ => None,
1900 })
1901 .expect("compiled program should contain the test extension");
1902
1903 assert_eq!(pruned_instruction.0.len(), 1);
1904 assert!(pruned_instruction.1.contains("pruned: true"));
1905 assert_eq!(pruned_instruction.2, 1);
1906 }
1907
1908 #[test]
1909 fn compiled_graph_input_keys_preserve_order_for_binary_graph() {
1910 let a = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1911 let b = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1912 let a_key = a.input_key().expect("concrete traced tensor has input key");
1913 let b_key = b.input_key().expect("concrete traced tensor has input key");
1914 let c = a.mul(&b).unwrap();
1915
1916 let program = GraphCompiler::new().compile_many(&[&c]).unwrap();
1917
1918 assert_eq!(program.input_count(), 2);
1919 assert_eq!(program.input_keys().len(), 2);
1920 let keys: Vec<_> = program.input_keys().to_vec();
1921 assert!(keys.contains(&a_key), "input_keys must contain a's key");
1922 assert!(keys.contains(&b_key), "input_keys must contain b's key");
1923 assert_eq!(
1925 program.input_key_index(&a_key),
1926 keys.iter().position(|k| k == &a_key)
1927 );
1928 assert_eq!(
1929 program.input_key_index(&b_key),
1930 keys.iter().position(|k| k == &b_key)
1931 );
1932 }
1933
1934 #[test]
1935 fn compile_with_input_specs_reorders_inputs_by_explicit_order() {
1936 let a = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
1937 let b = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
1938 let a_key = a.input_key().expect("symbolic traced tensor has input key");
1939 let b_key = b.input_key().expect("symbolic traced tensor has input key");
1940 let c = a.mul(&b).unwrap();
1941
1942 let program = GraphCompiler::new()
1944 .compile_with_input_specs(&c, &[(&b, DType::F64, &[2]), (&a, DType::F64, &[2])])
1945 .unwrap();
1946
1947 assert_eq!(program.input_count(), 2);
1948 assert_eq!(program.input_keys().len(), 2);
1949 assert_eq!(
1951 program.input_key_index(&b_key),
1952 Some(0),
1953 "b must be first in explicit order"
1954 );
1955 assert_eq!(
1956 program.input_key_index(&a_key),
1957 Some(1),
1958 "a must be second in explicit order"
1959 );
1960 }
1961}