1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use computegraph::graph::GraphBuilder;
5use computegraph::{LocalValueId, OperationRole, ValueRef};
6use tenferro_ops::input_key::TensorInputKey;
7use tenferro_ops::std_tensor_op::StdTensorOp;
8use tenferro_ops::{SymDim, TensorMeta};
9use tenferro_runtime::ad_support::{
10 allocate_input_key, allocate_shape_tensor_id, checkpoint_tensor, compile_ad_source,
11 frozen_input_value, inputs_map as tensor_inputs_map, leaf_input_key, merge_traced_leaf_metas,
12 metadata_scopes as tensor_metadata_scopes, metadata_scopes_with_new, ones_tensor,
13 register_scoped_graph_analysis, shape_hint as tensor_shape_hint, tensor_from_parts,
14 ConstraintScopeTransfer, RetainedValue, TracedTensorParts,
15};
16use tenferro_runtime::program::{FrozenProgram, ProgramValue, ProgramValueMetadata, SemanticOpRef};
17use tenferro_runtime::{
18 CompiledGraph, Error, ErrorPhase, ExecutionEngine, GraphCompiler, Result, Runtime, Tensor,
19 TracedTensor,
20};
21
22use crate::semantic_extension::{SemanticAdError, SemanticExtensionRuleSet};
23use crate::semantic_transform::{
24 semantic_jvp, semantic_vjp, SemanticAdProgram, SemanticAdTransformError,
25};
26use crate::transform_cache::{AdTransformCache, SemanticAdTransformCacheKey};
27
28pub(crate) fn next_input_key() -> TensorInputKey {
29 tenferro_runtime::ad_support::allocate_input_key()
30}
31
32fn error_shape_hint(tensor: &TracedTensor) -> Vec<usize> {
33 tensor
34 .try_concrete_shape()
35 .unwrap_or_else(|| vec![0; tensor.rank])
36}
37
38pub(crate) fn inactive_wrt_error(op: &'static str, wrt_key: impl std::fmt::Debug) -> Error {
43 Error::invalid_argument(
44 op,
45 ErrorPhase::GraphBuild,
46 "wrt",
47 format!(
48 "{op} is inactive for {wrt_key:?}; use {op}_optional to observe the inactive state"
49 ),
50 )
51}
52
53pub(crate) fn grad_with_rules_and_cache(
54 output: &TracedTensor,
55 wrt: &TracedTensor,
56 rules: &SemanticExtensionRuleSet,
57 ad_transform_cache: Option<&AdTransformCache>,
58) -> Result<TracedTensor> {
59 grad_with_optional_rules(output, wrt, rules, ad_transform_cache)
60}
61
62pub(crate) fn jvp_with_rules_and_cache(
63 output: &TracedTensor,
64 wrt: &TracedTensor,
65 tangent: &TracedTensor,
66 rules: &SemanticExtensionRuleSet,
67 ad_transform_cache: Option<&AdTransformCache>,
68) -> Result<TracedTensor> {
69 let wrt_input_key = leaf_input_key(wrt)?;
70 jvp_many_with_rules_and_cache(output, &[(wrt, tangent)], rules, ad_transform_cache)?
71 .ok_or_else(|| inactive_wrt_error("jvp", &wrt_input_key))
72}
73
74pub(crate) fn jvp_many_with_rules_and_cache(
75 output: &TracedTensor,
76 wrt_tangents: &[(&TracedTensor, &TracedTensor)],
77 rules: &SemanticExtensionRuleSet,
78 ad_transform_cache: Option<&AdTransformCache>,
79) -> Result<Option<TracedTensor>> {
80 jvp_many_optional_impl(output, wrt_tangents, rules, ad_transform_cache)
81}
82
83pub(crate) fn grad_optional_with_rules_and_cache(
84 output: &TracedTensor,
85 wrt: &TracedTensor,
86 rules: &SemanticExtensionRuleSet,
87 ad_transform_cache: Option<&AdTransformCache>,
88) -> Result<Option<TracedTensor>> {
89 if output.rank != 0 {
90 return Err(Error::NonScalarGrad {
91 shape: error_shape_hint(output),
92 });
93 }
94
95 let ones = ones_tensor(output.dtype, vec![])?;
96 let seed = TracedTensor::from_tensor_concrete_shape(ones)?;
97 vjp_many_with_transform_and_cache(output, &[wrt], &seed, rules, "grad", ad_transform_cache)
98 .map(|mut results| results.pop().flatten())
99}
100
101pub(crate) fn jvp_optional_with_rules_and_cache(
102 output: &TracedTensor,
103 wrt: &TracedTensor,
104 tangent: &TracedTensor,
105 rules: &SemanticExtensionRuleSet,
106 ad_transform_cache: Option<&AdTransformCache>,
107) -> Result<Option<TracedTensor>> {
108 jvp_many_with_rules_and_cache(output, &[(wrt, tangent)], rules, ad_transform_cache)
109}
110
111pub(crate) fn vjp_with_rules_and_cache(
112 output: &TracedTensor,
113 wrt: &TracedTensor,
114 cotangent: &TracedTensor,
115 rules: &SemanticExtensionRuleSet,
116 ad_transform_cache: Option<&AdTransformCache>,
117) -> Result<TracedTensor> {
118 let wrt_input_key = leaf_input_key(wrt)?;
119 vjp_many_with_rules_and_cache(output, &[wrt], cotangent, rules, ad_transform_cache)?
120 .into_iter()
121 .next()
122 .flatten()
123 .ok_or_else(|| inactive_wrt_error("vjp", &wrt_input_key))
124}
125
126pub(crate) fn vjp_many_with_rules_and_cache(
127 output: &TracedTensor,
128 wrts: &[&TracedTensor],
129 cotangent: &TracedTensor,
130 rules: &SemanticExtensionRuleSet,
131 ad_transform_cache: Option<&AdTransformCache>,
132) -> Result<Vec<Option<TracedTensor>>> {
133 vjp_many_with_transform_and_cache(output, wrts, cotangent, rules, "vjp", ad_transform_cache)
134}
135
136fn vjp_many_with_transform_and_cache(
137 output: &TracedTensor,
138 wrts: &[&TracedTensor],
139 cotangent: &TracedTensor,
140 rules: &SemanticExtensionRuleSet,
141 transform: &'static str,
142 ad_transform_cache: Option<&AdTransformCache>,
143) -> Result<Vec<Option<TracedTensor>>> {
144 vjp_many_optional_impl(
145 output,
146 wrts,
147 cotangent,
148 rules,
149 transform,
150 ad_transform_cache,
151 )
152}
153
154pub(crate) fn vjp_optional_with_rules_and_cache(
155 output: &TracedTensor,
156 wrt: &TracedTensor,
157 cotangent: &TracedTensor,
158 rules: &SemanticExtensionRuleSet,
159 ad_transform_cache: Option<&AdTransformCache>,
160) -> Result<Option<TracedTensor>> {
161 vjp_many_with_rules_and_cache(output, &[wrt], cotangent, rules, ad_transform_cache)
162 .map(|mut results| results.pop().flatten())
163}
164
165fn grad_with_optional_rules(
166 output: &TracedTensor,
167 wrt: &TracedTensor,
168 rules: &SemanticExtensionRuleSet,
169 ad_transform_cache: Option<&AdTransformCache>,
170) -> Result<TracedTensor> {
171 if output.rank != 0 {
172 return Err(Error::NonScalarGrad {
173 shape: error_shape_hint(output),
174 });
175 }
176
177 let ones = ones_tensor(output.dtype, vec![])?;
178 let seed = TracedTensor::from_tensor_concrete_shape(ones)?;
179 let wrt_input_key = leaf_input_key(wrt)?;
180 vjp_many_with_transform_and_cache(output, &[wrt], &seed, rules, "grad", ad_transform_cache)?
181 .pop()
182 .flatten()
183 .ok_or_else(|| inactive_wrt_error("grad", &wrt_input_key))
184}
185
186fn single_runtime_output(mut outputs: Vec<Tensor>, op: &'static str) -> Result<Tensor> {
187 let actual = outputs.len();
188 if actual != 1 {
189 return Err(Error::runtime_state(
190 op,
191 ErrorPhase::Execution,
192 format!("expected one runtime output, got {actual}"),
193 ));
194 }
195 outputs.pop().ok_or_else(|| {
196 Error::runtime_state(
197 op,
198 ErrorPhase::Execution,
199 "runtime returned no output after successful output-count validation",
200 )
201 })
202}
203
204pub trait TracedTensorAdExt {
218 fn grad(&self, wrt: &TracedTensor) -> Result<TracedTensor>;
268
269 fn grad_optional(&self, wrt: &TracedTensor) -> Result<Option<TracedTensor>>;
297
298 fn checkpoint(
331 &mut self,
332 compiler: &mut GraphCompiler,
333 runtime: &Runtime,
334 engine: &mut dyn ExecutionEngine,
335 ) -> Result<()>;
336
337 fn jvp(&self, wrt: &TracedTensor, tangent: &TracedTensor) -> Result<TracedTensor>;
383
384 fn jvp_optional(
413 &self,
414 wrt: &TracedTensor,
415 tangent: &TracedTensor,
416 ) -> Result<Option<TracedTensor>>;
417
418 fn vjp(&self, wrt: &TracedTensor, cotangent: &TracedTensor) -> Result<TracedTensor>;
469
470 fn vjp_optional(
499 &self,
500 wrt: &TracedTensor,
501 cotangent: &TracedTensor,
502 ) -> Result<Option<TracedTensor>>;
503}
504
505impl TracedTensorAdExt for TracedTensor {
506 fn grad(&self, wrt: &TracedTensor) -> Result<TracedTensor> {
507 let rules = SemanticExtensionRuleSet::default();
508 grad_with_optional_rules(self, wrt, &rules, None)
509 }
510
511 fn grad_optional(&self, wrt: &TracedTensor) -> Result<Option<TracedTensor>> {
512 if self.rank != 0 {
513 return Err(Error::NonScalarGrad {
514 shape: error_shape_hint(self),
515 });
516 }
517
518 let ones = ones_tensor(self.dtype, vec![])?;
519 let seed = TracedTensor::from_tensor_concrete_shape(ones)?;
520 let rules = SemanticExtensionRuleSet::default();
521 vjp_many_with_transform_and_cache(self, &[wrt], &seed, &rules, "grad", None)
522 .map(|mut results| results.pop().flatten())
523 }
524
525 fn checkpoint(
526 &mut self,
527 compiler: &mut GraphCompiler,
528 runtime: &Runtime,
529 engine: &mut dyn ExecutionEngine,
530 ) -> Result<()> {
531 let data = if let Some(data) = self.attached_value() {
532 Arc::clone(data)
533 } else {
534 let program = compiler.compile(self)?;
535 let target = engine.target();
536 let prepared = runtime.prepare(&program, &[], std::slice::from_ref(&target))?;
537 let outputs = crate::eager::run_prepared_on(runtime, engine, &prepared, &[])?;
538 Arc::new(RetainedValue::from_tensor(single_runtime_output(
539 outputs,
540 "TracedTensorAdExt::checkpoint",
541 )?))
542 };
543 checkpoint_tensor(self, data)?;
544 Ok(())
545 }
546
547 fn jvp(&self, wrt: &TracedTensor, tangent: &TracedTensor) -> Result<TracedTensor> {
548 let wrt_input_key = leaf_input_key(wrt)?;
549 self.jvp_optional(wrt, tangent)?
550 .ok_or_else(|| inactive_wrt_error("jvp", &wrt_input_key))
551 }
552
553 fn jvp_optional(
554 &self,
555 wrt: &TracedTensor,
556 tangent: &TracedTensor,
557 ) -> Result<Option<TracedTensor>> {
558 let rules = SemanticExtensionRuleSet::default();
559 jvp_many_optional_impl(self, &[(wrt, tangent)], &rules, None)
560 }
561
562 fn vjp(&self, wrt: &TracedTensor, cotangent: &TracedTensor) -> Result<TracedTensor> {
563 let wrt_input_key = leaf_input_key(wrt)?;
564 self.vjp_optional(wrt, cotangent)?
565 .ok_or_else(|| inactive_wrt_error("vjp", &wrt_input_key))
566 }
567
568 fn vjp_optional(
569 &self,
570 wrt: &TracedTensor,
571 cotangent: &TracedTensor,
572 ) -> Result<Option<TracedTensor>> {
573 let rules = SemanticExtensionRuleSet::default();
574 vjp_many_with_transform_and_cache(self, &[wrt], cotangent, &rules, "vjp", None)
575 .map(|mut results| results.pop().flatten())
576 }
577}
578
579fn jvp_many_optional_impl(
580 output: &TracedTensor,
581 wrt_tangents: &[(&TracedTensor, &TracedTensor)],
582 rules: &SemanticExtensionRuleSet,
583 ad_transform_cache: Option<&AdTransformCache>,
584) -> Result<Option<TracedTensor>> {
585 if wrt_tangents.is_empty() {
586 return Ok(None);
587 }
588
589 let mut seen = HashSet::with_capacity(wrt_tangents.len());
590 let mut requested = Vec::with_capacity(wrt_tangents.len());
591 for (wrt, tangent) in wrt_tangents {
592 let key = leaf_input_key(wrt)?;
593 if !seen.insert(key.clone()) {
594 return Err(Error::invalid_argument(
595 "jvp",
596 ErrorPhase::GraphBuild,
597 "wrt_tangents",
598 "wrt_tangents contains duplicate wrt leaves",
599 ));
600 }
601 let tangent_data = tangent.attached_value().cloned().ok_or_else(|| {
602 Error::invalid_argument(
603 "jvp",
604 ErrorPhase::GraphBuild,
605 "tangent",
606 "jvp tangent must have concrete tensor data",
607 )
608 })?;
609 requested.push((key, tangent_data));
610 }
611
612 let mut compiler = GraphCompiler::new();
613 let source = compile_ad_source(&mut compiler, output)?;
614 let mut active_inputs = vec![false; source.input_count()];
615 let mut source_indices = Vec::with_capacity(requested.len());
616 for (key, _) in &requested {
617 let source_index = source.input_key_index(key);
618 if let Some(index) = source_index {
619 active_inputs[index] = true;
620 }
621 source_indices.push(source_index);
622 }
623 if !active_inputs.iter().any(|active| *active) {
624 return Ok(None);
625 }
626
627 let derivative = semantic_jvp_with_cache(
628 source.frozen_program(),
629 &active_inputs,
630 rules,
631 ad_transform_cache,
632 )?;
633 let Some(derivative_output_index) = derivative
634 .derivative_output_indices()
635 .first()
636 .copied()
637 .flatten()
638 else {
639 return Ok(None);
640 };
641
642 let mut seed_tensors = Vec::with_capacity(requested.len());
643 for ((_, tangent_data), source_index) in requested.iter().zip(source_indices) {
644 let Some(source_index) = source_index else {
645 continue;
646 };
647 let Some(seed_input_index) = derivative
648 .derivative_input_indices()
649 .get(source_index)
650 .copied()
651 .flatten()
652 else {
653 continue;
654 };
655 seed_tensors.push((seed_input_index, Arc::clone(tangent_data)));
656 }
657 let mut inherited_tensors = Vec::with_capacity(1 + 2 * wrt_tangents.len());
658 inherited_tensors.push(output);
659 for (wrt, tangent) in wrt_tangents {
660 inherited_tensors.push(*wrt);
661 inherited_tensors.push(*tangent);
662 }
663 let mut traces = derivative_tensors_from_program(
664 &source,
665 &derivative,
666 &[derivative_output_index],
667 &seed_tensors,
668 &inherited_tensors,
669 &[tensor_shape_hint(output)],
670 "jvp",
671 )?;
672 traces.pop().map(Some).ok_or_else(|| {
673 Error::runtime_state(
674 "jvp",
675 ErrorPhase::GraphBuild,
676 "derivative program returned no requested output",
677 )
678 })
679}
680
681fn vjp_many_optional_impl(
682 output: &TracedTensor,
683 wrts: &[&TracedTensor],
684 cotangent: &TracedTensor,
685 rules: &SemanticExtensionRuleSet,
686 transform: &'static str,
687 ad_transform_cache: Option<&AdTransformCache>,
688) -> Result<Vec<Option<TracedTensor>>> {
689 let cotangent_data = cotangent.attached_value().cloned().ok_or_else(|| {
690 Error::invalid_argument(
691 transform,
692 ErrorPhase::GraphBuild,
693 "cotangent",
694 "vjp cotangent must have concrete tensor data",
695 )
696 })?;
697 if wrts.is_empty() {
698 return Ok(Vec::new());
699 }
700
701 let requested_keys = wrts
702 .iter()
703 .map(|wrt| leaf_input_key(wrt))
704 .collect::<Result<Vec<_>>>()?;
705 let mut compiler = GraphCompiler::new();
706 let source = compile_ad_source(&mut compiler, output)?;
707 let source_indices: Vec<_> = requested_keys
708 .iter()
709 .map(|key| source.input_key_index(key))
710 .collect();
711 let mut active_inputs = vec![false; source.input_count()];
712 for source_index in source_indices.iter().flatten().copied() {
713 active_inputs[source_index] = true;
714 }
715 if !active_inputs.iter().any(|active| *active) {
716 return Ok(vec![None; wrts.len()]);
717 }
718
719 let active_outputs = vec![true; source.output_count()];
720 let derivative = semantic_vjp_with_cache(
721 source.frozen_program(),
722 &active_inputs,
723 &active_outputs,
724 rules,
725 ad_transform_cache,
726 )?;
727 let Some(seed_input_index) = derivative
728 .derivative_input_indices()
729 .first()
730 .copied()
731 .flatten()
732 else {
733 return Ok(vec![None; wrts.len()]);
734 };
735
736 let mut output_slots = vec![None; source.input_count()];
737 let mut derivative_output_indices = Vec::new();
738 let mut fallback_shape_hints = Vec::new();
739 for (request_index, source_index) in source_indices.iter().enumerate() {
740 let Some(source_index) = source_index else {
741 continue;
742 };
743 if output_slots[*source_index].is_some() {
744 continue;
745 }
746 let Some(derivative_output_index) = derivative
747 .derivative_output_indices()
748 .get(*source_index)
749 .copied()
750 .flatten()
751 else {
752 continue;
753 };
754 output_slots[*source_index] = Some(derivative_output_indices.len());
755 derivative_output_indices.push(derivative_output_index);
756 fallback_shape_hints.push(tensor_shape_hint(wrts[request_index]));
757 }
758 if derivative_output_indices.is_empty() {
759 return Ok(vec![None; wrts.len()]);
760 }
761
762 let inherited_tensors: Vec<&TracedTensor> = std::iter::once(output)
763 .chain(wrts.iter().copied())
764 .chain(std::iter::once(cotangent))
765 .collect();
766 let traces = derivative_tensors_from_program(
767 &source,
768 &derivative,
769 &derivative_output_indices,
770 &[(seed_input_index, cotangent_data)],
771 &inherited_tensors,
772 &fallback_shape_hints,
773 transform,
774 )?;
775 Ok(source_indices
776 .into_iter()
777 .map(|source_index| {
778 source_index
779 .and_then(|index| output_slots[index])
780 .map(|slot| traces[slot].clone())
781 })
782 .collect())
783}
784
785fn semantic_jvp_with_cache(
786 source: &FrozenProgram,
787 active_inputs: &[bool],
788 rules: &SemanticExtensionRuleSet,
789 ad_transform_cache: Option<&AdTransformCache>,
790) -> Result<SemanticAdProgram> {
791 let key = SemanticAdTransformCacheKey::jvp(source, active_inputs);
792 if let Some(cache) = ad_transform_cache
793 && let Some(cached) = cache.get_semantic(&key, source)?
794 {
795 return cached
796 .as_ref()
797 .with_input_prefix_bindings_from(source)
798 .map_err(|source| {
799 Error::runtime_state_source(
800 "semantic traced jvp cache",
801 ErrorPhase::GraphBuild,
802 source,
803 )
804 });
805 }
806 let derivative =
807 semantic_jvp(source, active_inputs, rules).map_err(semantic_transform_error("jvp"))?;
808 if let Some(cache) = ad_transform_cache {
809 cache.put_semantic(key, source, Arc::new(derivative.clone()))?;
810 }
811 Ok(derivative)
812}
813
814fn semantic_vjp_with_cache(
815 source: &FrozenProgram,
816 active_inputs: &[bool],
817 active_outputs: &[bool],
818 rules: &SemanticExtensionRuleSet,
819 ad_transform_cache: Option<&AdTransformCache>,
820) -> Result<SemanticAdProgram> {
821 let key = SemanticAdTransformCacheKey::vjp(source, active_inputs, active_outputs);
822 if let Some(cache) = ad_transform_cache
823 && let Some(cached) = cache.get_semantic(&key, source)?
824 {
825 return cached
826 .as_ref()
827 .with_input_prefix_bindings_from(source)
828 .map_err(|source| {
829 Error::runtime_state_source(
830 "semantic traced vjp cache",
831 ErrorPhase::GraphBuild,
832 source,
833 )
834 });
835 }
836 let derivative = semantic_vjp(source, active_inputs, active_outputs, rules)
837 .map_err(semantic_transform_error("vjp"))?;
838 if let Some(cache) = ad_transform_cache {
839 cache.put_semantic(key, source, Arc::new(derivative.clone()))?;
840 }
841 Ok(derivative)
842}
843
844fn semantic_transform_error(
845 transform: &'static str,
846) -> impl FnOnce(SemanticAdTransformError) -> Error {
847 move |source| {
848 semantic_transform_validation_error(transform, &source).unwrap_or_else(|| {
849 Error::runtime_state_source(transform, ErrorPhase::GraphBuild, source)
850 })
851 }
852}
853
854fn semantic_transform_validation_error(
855 transform: &'static str,
856 source: &SemanticAdTransformError,
857) -> Option<Error> {
858 if let SemanticAdTransformError::Extension(
859 SemanticAdError::Unsupported { family_id, .. }
860 | SemanticAdError::MissingRule { family_id, .. },
861 ) = source
862 {
863 return Some(Error::UnsupportedAdRule {
864 transform,
865 op: (*family_id).to_owned(),
866 });
867 }
868
869 let SemanticAdTransformError::Extension(SemanticAdError::Rule { source, .. }) = source else {
870 return None;
871 };
872 let tenferro_ops::ad::ADRuleError::InvalidInput { op, message, .. } =
873 source.downcast_ref::<tenferro_ops::ad::ADRuleError>()?
874 else {
875 return None;
876 };
877 Some(Error::invalid_argument(
878 transform,
879 ErrorPhase::GraphBuild,
880 "semantic_ad_rule",
881 format!("{op}: {message}"),
882 ))
883}
884
885fn derivative_tensors_from_program(
886 source: &CompiledGraph,
887 derivative: &SemanticAdProgram,
888 derivative_output_indices: &[usize],
889 seed_tensors: &[(usize, Arc<RetainedValue>)],
890 inherited_tensors: &[&TracedTensor],
891 fallback_shape_hints: &[Option<Vec<SymDim>>],
892 transform: &'static str,
893) -> Result<Vec<TracedTensor>> {
894 derivative_traces_from_frozen_program(
895 source,
896 derivative.frozen(),
897 derivative_output_indices,
898 seed_tensors,
899 inherited_tensors,
900 fallback_shape_hints,
901 transform,
902 )
903}
904
905pub(crate) fn derivative_trace_from_frozen_program(
906 source: &CompiledGraph,
907 frozen: &FrozenProgram,
908 derivative_output_index: usize,
909 seed_tensors: &[(usize, Arc<RetainedValue>)],
910 inherited_tensors: &[&TracedTensor],
911 fallback_shape_hint: Option<Vec<SymDim>>,
912 transform: &'static str,
913) -> Result<TracedTensor> {
914 let mut outputs = derivative_traces_from_frozen_program(
915 source,
916 frozen,
917 &[derivative_output_index],
918 seed_tensors,
919 inherited_tensors,
920 &[fallback_shape_hint],
921 transform,
922 )?;
923 outputs.pop().ok_or_else(|| {
924 Error::runtime_state(
925 transform,
926 ErrorPhase::GraphBuild,
927 "derivative program returned no requested output",
928 )
929 })
930}
931
932fn derivative_traces_from_frozen_program(
933 source: &CompiledGraph,
934 frozen: &FrozenProgram,
935 derivative_output_indices: &[usize],
936 seed_tensors: &[(usize, Arc<RetainedValue>)],
937 inherited_tensors: &[&TracedTensor],
938 fallback_shape_hints: &[Option<Vec<SymDim>>],
939 transform: &'static str,
940) -> Result<Vec<TracedTensor>> {
941 let input_shapes = symbolic_input_shapes(frozen)?;
942 let input_shape_refs: Vec<_> = input_shapes.iter().map(Vec::as_slice).collect();
943 let input_metas = frozen
944 .program
945 .inputs()
946 .iter()
947 .copied()
948 .map(|value| tensor_meta_for_value(frozen, value, &input_shape_refs, transform))
949 .collect::<Result<Vec<_>>>()?;
950
951 if derivative_output_indices.len() != fallback_shape_hints.len() {
952 return Err(Error::runtime_state(
953 transform,
954 ErrorPhase::GraphBuild,
955 "derivative output indices and fallback shape hints differ in length",
956 ));
957 }
958 let output_metas = derivative_output_indices
959 .iter()
960 .map(|&derivative_output_index| {
961 let output_value = *frozen
962 .program
963 .outputs()
964 .get(derivative_output_index)
965 .ok_or_else(|| {
966 Error::runtime_state(
967 transform,
968 ErrorPhase::GraphBuild,
969 format!(
970 "derivative output index {derivative_output_index} is outside {} outputs",
971 frozen.program.outputs().len()
972 ),
973 )
974 })?;
975 tensor_meta_for_value(frozen, output_value, &input_shape_refs, transform)
976 })
977 .collect::<Result<Vec<_>>>()?;
978
979 let mut builder = GraphBuilder::<StdTensorOp>::new();
980 let mut value_map = HashMap::<ProgramValue, LocalValueId>::new();
981 let mut input_keys = Vec::with_capacity(frozen.program.inputs().len());
982 for (input_index, input) in frozen.program.inputs().iter().copied().enumerate() {
983 let key = if input_index < source.input_keys().len() {
984 source.input_keys()[input_index].clone()
985 } else {
986 allocate_input_key()
987 };
988 let local = builder.add_input(key.clone());
989 value_map.insert(input, local);
990 input_keys.push(key);
991 }
992
993 for operation in frozen.program.operations() {
994 let inputs = operation
995 .inputs()
996 .iter()
997 .copied()
998 .map(|value| {
999 value_map
1000 .get(&value)
1001 .copied()
1002 .map(ValueRef::Local)
1003 .ok_or_else(|| missing_program_value(transform, "operation input"))
1004 })
1005 .collect::<Result<Vec<_>>>()?;
1006 let op = match operation.op() {
1007 SemanticOpRef::Core(op) => StdTensorOp::from(op),
1008 SemanticOpRef::Extension(op) => StdTensorOp::Extension(op.clone_arc()),
1009 _ => {
1010 return Err(Error::runtime_state(
1011 transform,
1012 ErrorPhase::GraphBuild,
1013 "unsupported semantic operation variant in derivative graph",
1014 ));
1015 }
1016 };
1017 let outputs = builder.add_operation(op, inputs, OperationRole::Primary);
1018 if outputs.len() != operation.outputs().len() {
1019 return Err(Error::runtime_state(
1020 transform,
1021 ErrorPhase::GraphBuild,
1022 format!(
1023 "semantic operation expected {} outputs, graph builder produced {}",
1024 operation.outputs().len(),
1025 outputs.len()
1026 ),
1027 ));
1028 }
1029 for (value, local) in operation.outputs().iter().copied().zip(outputs) {
1030 value_map.insert(value, local);
1031 }
1032 }
1033
1034 let graph_outputs = frozen
1035 .program
1036 .outputs()
1037 .iter()
1038 .copied()
1039 .map(|value| {
1040 value_map
1041 .get(&value)
1042 .copied()
1043 .ok_or_else(|| missing_program_value(transform, "program output"))
1044 })
1045 .collect::<Result<Vec<_>>>()?;
1046 let vals = derivative_output_indices
1047 .iter()
1048 .map(|&index| {
1049 graph_outputs.get(index).copied().ok_or_else(|| {
1050 Error::runtime_state(
1051 transform,
1052 ErrorPhase::GraphBuild,
1053 "derivative output index missing after graph conversion",
1054 )
1055 })
1056 })
1057 .collect::<Result<Vec<_>>>()?;
1058 builder.set_outputs(graph_outputs);
1059 let graph = Arc::new(builder.build());
1060
1061 let Some(primary_tensor) = inherited_tensors.first() else {
1062 return Err(Error::runtime_state(
1063 transform,
1064 ErrorPhase::GraphBuild,
1065 "derivative trace construction requires inherited source tensors",
1066 ));
1067 };
1068 let mut inputs_map = (*tensor_inputs_map(primary_tensor)).clone();
1069 for (input_index, key) in input_keys.iter().enumerate() {
1070 if let Some(tensor) = frozen_input_value(frozen, input_index) {
1071 inputs_map.insert(key.clone(), tensor);
1072 }
1073 }
1074 for (seed_input_index, tensor) in seed_tensors {
1075 let meta = input_metas.get(*seed_input_index).ok_or_else(|| {
1076 Error::runtime_state(
1077 transform,
1078 ErrorPhase::GraphBuild,
1079 format!("seed input index {seed_input_index} is outside derivative inputs"),
1080 )
1081 })?;
1082 validate_seed_tensor(transform, *seed_input_index, tensor.as_ref(), meta)?;
1083 let key = input_keys.get(*seed_input_index).ok_or_else(|| {
1084 Error::runtime_state(
1085 transform,
1086 ErrorPhase::GraphBuild,
1087 format!("seed input key {seed_input_index} is outside derivative inputs"),
1088 )
1089 })?;
1090 inputs_map.insert(key.clone(), Arc::clone(tensor));
1091 }
1092
1093 let source_input_count = source.input_keys().len();
1094 let graph_input_metadata = graph
1095 .inputs()
1096 .iter()
1097 .copied()
1098 .zip(input_metas.iter().cloned())
1099 .enumerate()
1100 .filter_map(|(input_index, (input, meta))| {
1101 if input_index < source_input_count {
1107 None
1108 } else {
1109 Some((graph.values()[input].key.clone(), meta))
1110 }
1111 });
1112 let analysis = register_scoped_graph_analysis(graph.as_ref(), graph_input_metadata)?;
1113 let inherited_constraint_scopes = inherited_tensors
1114 .iter()
1115 .map(|tensor| ConstraintScopeTransfer::from_tensor(tensor))
1116 .collect::<Vec<_>>();
1117
1118 let inputs_map = Arc::new(inputs_map);
1119 let leaf_metas = merge_traced_leaf_metas(inherited_tensors.iter().copied());
1123 let metadata_scopes = metadata_scopes_with_new(
1124 analysis.metadata,
1125 inherited_tensors
1126 .iter()
1127 .map(|tensor| tensor_metadata_scopes(tensor)),
1128 );
1129 let constraint_scope_transfer =
1130 ConstraintScopeTransfer::with_new(analysis.constraints, inherited_constraint_scopes.iter());
1131
1132 Ok(vals
1133 .into_iter()
1134 .zip(output_metas)
1135 .zip(fallback_shape_hints)
1136 .map(|((val, output_meta), fallback_shape_hint)| {
1137 tensor_from_parts(TracedTensorParts {
1138 rank: output_meta.rank(),
1139 dtype: output_meta.dtype,
1140 graph: Arc::clone(&graph),
1141 val,
1142 data: None,
1143 shape_hint: output_meta
1144 .exact_shape()
1145 .or_else(|| fallback_shape_hint.clone()),
1146 inputs_map: Arc::clone(&inputs_map),
1147 leaf_metas: Arc::clone(&leaf_metas),
1148 extra_roots: Vec::new(),
1149 checkpoint_chain: None,
1150 metadata_scopes: metadata_scopes.clone(),
1151 constraint_scope_transfer: constraint_scope_transfer.clone(),
1152 })
1153 })
1154 .collect())
1155}
1156
1157fn missing_program_value(transform: &'static str, role: &'static str) -> Error {
1158 Error::runtime_state(
1159 transform,
1160 ErrorPhase::GraphBuild,
1161 format!("semantic derivative graph references missing {role}"),
1162 )
1163}
1164
1165fn symbolic_input_shapes(frozen: &FrozenProgram) -> Result<Vec<Vec<SymDim>>> {
1166 frozen
1167 .program
1168 .inputs()
1169 .iter()
1170 .copied()
1171 .map(|value| {
1172 let meta = frozen.program.value_metadata(value).map_err(|source| {
1173 Error::runtime_state_source(
1174 "semantic traced AD input metadata",
1175 ErrorPhase::GraphBuild,
1176 source,
1177 )
1178 })?;
1179 let tensor_id = allocate_shape_tensor_id();
1180 Ok((0..meta.shape().len())
1181 .map(|axis| SymDim::tensor_axis(tensor_id, axis))
1182 .collect())
1183 })
1184 .collect()
1185}
1186
1187fn tensor_meta_for_value(
1188 frozen: &FrozenProgram,
1189 value: ProgramValue,
1190 input_shapes: &[&[SymDim]],
1191 transform: &'static str,
1192) -> Result<TensorMeta> {
1193 let meta = frozen
1194 .program
1195 .value_metadata(value)
1196 .map_err(|source| Error::runtime_state_source(transform, ErrorPhase::GraphBuild, source))?;
1197 Ok(program_metadata_to_tensor_meta(meta, input_shapes))
1198}
1199
1200fn program_metadata_to_tensor_meta(
1201 metadata: &ProgramValueMetadata,
1202 input_shapes: &[&[SymDim]],
1203) -> TensorMeta {
1204 let extents = metadata
1205 .shape()
1206 .iter()
1207 .cloned()
1208 .map(|extent| extent.map(|dim| SymDim::from_dim_expr(&dim, input_shapes)))
1209 .collect();
1210 let meta = TensorMeta::with_extents(metadata.dtype(), extents);
1213 match metadata.scalar_identity() {
1214 Some(identity) => meta.with_scalar_identity(identity),
1215 None => meta,
1216 }
1217}
1218
1219fn validate_seed_tensor(
1220 transform: &'static str,
1221 input_index: usize,
1222 tensor: &RetainedValue,
1223 expected: &TensorMeta,
1224) -> Result<()> {
1225 let actual_dtype = tensor.dtype();
1226 if actual_dtype != expected.dtype {
1227 return Err(Error::invalid_argument(
1228 transform,
1229 ErrorPhase::GraphBuild,
1230 "seed",
1231 format!(
1232 "seed input {input_index} dtype mismatch: expected {:?}, got {:?}",
1233 expected.dtype, actual_dtype
1234 ),
1235 ));
1236 }
1237 let actual_shape = tensor.shape();
1238 if actual_shape.len() != expected.rank() {
1239 return Err(Error::invalid_argument(
1240 transform,
1241 ErrorPhase::GraphBuild,
1242 "seed",
1243 format!(
1244 "seed input {input_index} rank mismatch: expected {}, got {}",
1245 expected.rank(),
1246 actual_shape.len()
1247 ),
1248 ));
1249 }
1250 if let Some(expected_shape) = expected
1251 .exact_shape()
1252 .filter(|shape| shape.iter().all(|dim| dim.constant_value().is_some()))
1253 .map(|shape| {
1254 shape
1255 .into_iter()
1256 .map(|dim| dim.constant_value().expect("filtered constant shape"))
1257 .collect::<Vec<_>>()
1258 })
1259 && expected_shape != actual_shape
1260 {
1261 return Err(Error::invalid_argument(
1262 transform,
1263 ErrorPhase::GraphBuild,
1264 "seed",
1265 format!(
1266 "seed input {input_index} shape mismatch: expected {:?}, got {:?}",
1267 expected_shape, actual_shape
1268 ),
1269 ));
1270 }
1271 Ok(())
1272}
1273
1274#[cfg(test)]
1275mod semantic_transform_error_tests {
1276 use super::*;
1277 use crate::semantic_extension::SemanticAdRuleRole;
1278
1279 #[test]
1280 fn unsupported_semantic_rule_maps_to_public_transform_error() {
1281 let source = SemanticAdTransformError::Extension(SemanticAdError::Unsupported {
1282 family_id: "tenferro-tests.unsupported.v1",
1283 role: SemanticAdRuleRole::LinearTranspose,
1284 message: "unsupported test payload".into(),
1285 });
1286
1287 let error = semantic_transform_validation_error("vjp", &source)
1288 .expect("semantic rejection must map to a public unsupported-rule error");
1289
1290 assert!(matches!(
1291 error,
1292 Error::UnsupportedAdRule { transform: "vjp", ref op }
1293 if op == "tenferro-tests.unsupported.v1"
1294 ));
1295 }
1296
1297 #[test]
1298 fn missing_semantic_rule_maps_to_public_transform_error() {
1299 let source = SemanticAdTransformError::Extension(SemanticAdError::MissingRule {
1300 family_id: "tenferro-tests.missing.v1",
1301 role: SemanticAdRuleRole::Linearize,
1302 });
1303
1304 let error = semantic_transform_validation_error("jvp", &source)
1305 .expect("missing semantic rule must map to a public unsupported-rule error");
1306
1307 assert!(matches!(
1308 error,
1309 Error::UnsupportedAdRule { transform: "jvp", ref op }
1310 if op == "tenferro-tests.missing.v1"
1311 ));
1312 }
1313}