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