1use std::collections::{HashMap, HashSet};
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::Arc;
4
5use crate::ad_rule_error::ad_rule_error;
6use computegraph::graph::Graph;
7use computegraph::resolve::resolve;
8use computegraph::resolve::{ResolvedView, ValueDef};
9use computegraph::types::ValueKey;
10use tenferro_ops::input_key::TensorInputKey;
11use tenferro_ops::std_tensor_op::StdTensorOp;
12use tenferro_ops::ExtensionRuleSet;
13use tenferro_ops::ShapeGuardContext;
14use tenferro_runtime::ad_support::{
15 checkpoint_chain as tensor_checkpoint_chain, checkpoint_tensor,
16 extra_roots as tensor_extra_roots, inputs_map as tensor_inputs_map, leaf_input_key,
17 linear_input_key, metadata_scopes as tensor_metadata_scopes, metadata_scopes_with_new,
18 ones_tensor, push_metadata_scope, register_scoped_graph_metadata, registered_meta,
19 resolve_roots as tensor_resolve_roots, shape_hint as tensor_shape_hint, tensor_from_parts,
20 tensor_meta_from_tensor, GlobalMetadataScope, TracedTensorParts,
21};
22use tenferro_runtime::{Error, GraphCompiler, GraphExecutor, Result, TracedTensor};
23use tenferro_tensor::TensorBackend;
24use tidu::{linear_transpose, linearize, ADRuleError};
25
26#[path = "traced/optimizer.rs"]
27mod optimizer;
28#[path = "traced/primal_transpose.rs"]
29mod primal_transpose;
30
31use optimizer::OptimizedLinearGraph;
32use primal_transpose::{try_primal_transpose, PrimalTransposeGraph};
33
34use crate::transform_cache::{
35 AdTransformCache, CachedTracedVjpTransform, TracedAdTransformCacheKey, TracedAdTransformKind,
36};
37
38static NEXT_DIFF_PASS_ID: AtomicU64 = AtomicU64::new(0);
39
40fn next_pass_id() -> u64 {
41 NEXT_DIFF_PASS_ID.fetch_add(1, Ordering::Relaxed)
42}
43
44pub(crate) fn next_input_key() -> TensorInputKey {
45 tenferro_runtime::ad_support::allocate_input_key()
46}
47
48fn error_shape_hint(tensor: &TracedTensor) -> Vec<usize> {
49 tensor
50 .try_concrete_shape()
51 .unwrap_or_else(|| vec![0; tensor.rank])
52}
53
54fn shape_guard_context(
55 extension_rules: Option<&ExtensionRuleSet>,
56 active_values: Option<Arc<HashSet<ValueKey<StdTensorOp>>>>,
57 roots: &[Arc<Graph<StdTensorOp>>],
58) -> ShapeGuardContext {
59 let mut ctx = ShapeGuardContext::with_global_metadata();
60 register_shape_sources(&mut ctx, roots);
61 let ctx = match extension_rules {
62 Some(rules) => ctx.with_extension_rules(rules.clone()),
63 None => ctx,
64 };
65 match active_values {
66 Some(keys) => ctx.with_linearize_active_values(keys),
67 None => ctx,
68 }
69}
70
71fn register_shape_sources(ctx: &mut ShapeGuardContext, roots: &[Arc<Graph<StdTensorOp>>]) {
72 let mut seen = HashSet::new();
73 for graph in roots {
74 register_graph_shape_sources(ctx, graph, &mut seen);
75 }
76}
77
78fn register_graph_shape_sources(
79 ctx: &mut ShapeGuardContext,
80 graph: &Arc<Graph<StdTensorOp>>,
81 seen: &mut HashSet<*const Graph<StdTensorOp>>,
82) {
83 if !seen.insert(Arc::as_ptr(graph)) {
84 return;
85 }
86 for parent in graph.parents() {
87 register_graph_shape_sources(ctx, parent, seen);
88 }
89 for &input_id in graph.inputs() {
90 let key = graph.values()[input_id].key.clone();
91 let Ok(meta) = registered_meta(&key) else {
92 continue;
93 };
94 let Some(shape) = meta.bound_shape() else {
95 continue;
96 };
97 for tensor_id in shape
98 .iter()
99 .flat_map(|dim| dim.referenced_tensor_ids().into_iter())
100 {
101 ctx.insert_shape_source(tensor_id, key.clone());
102 }
103 }
104}
105
106fn linearize_active_value_keys(
107 view: &ResolvedView<StdTensorOp>,
108 outputs: &[ValueKey<StdTensorOp>],
109 aliases: &std::collections::HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
110) -> Arc<HashSet<ValueKey<StdTensorOp>>> {
111 let mut active = HashSet::new();
112 let mut stack: Vec<ValueKey<StdTensorOp>> = outputs.to_vec();
113 while let Some(key) = stack.pop() {
114 if !active.insert(key.clone()) {
115 continue;
116 }
117 let Some(val_def) = view.resolve_value(&key) else {
118 continue;
119 };
120 match val_def {
121 ValueDef::Produced { input_keys, .. } => {
122 for input_key in input_keys {
123 stack.push(input_key.clone());
124 }
125 }
126 ValueDef::Input { key: input_key } => {
127 if let Some(aliased) = aliases.get(&input_key) {
128 stack.push(aliased.clone());
129 }
130 }
131 }
132 }
133 Arc::new(active)
134}
135
136fn graph_has_registered_primal_vjp(
137 view: &ResolvedView<StdTensorOp>,
138 outputs: &[ValueKey<StdTensorOp>],
139 aliases: &HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
140 extension_rules: Option<&ExtensionRuleSet>,
141) -> bool {
142 let Some(extension_rules) = extension_rules else {
143 return false;
144 };
145 let mut seen = HashSet::new();
146 let mut stack = outputs.to_vec();
147 while let Some(key) = stack.pop() {
148 if !seen.insert(key.clone()) {
149 continue;
150 }
151 if let ValueKey::Derived { operation, .. } = &key {
152 if let StdTensorOp::Extension(ext) = operation.operation() {
153 if extension_rules.lookup_primal_vjp(ext.family_id()).is_some() {
154 return true;
155 }
156 }
157 }
158 let Some(val_def) = view.resolve_value(&key) else {
159 continue;
160 };
161 match val_def {
162 ValueDef::Produced { input_keys, .. } => {
163 for input_key in input_keys {
164 stack.push(input_key);
165 }
166 }
167 ValueDef::Input { key: input_key } => {
168 if let Some(aliased) = aliases.get(&input_key) {
169 stack.push(aliased.clone());
170 }
171 }
172 }
173 }
174 false
175}
176
177fn is_not_applicable_custom_vjp(err: &ADRuleError) -> bool {
178 matches!(err, ADRuleError::Unsupported { .. })
179}
180
181pub(crate) fn grad_with_rules_and_cache(
182 output: &TracedTensor,
183 wrt: &TracedTensor,
184 extension_rules: &ExtensionRuleSet,
185 ad_transform_cache: Option<&AdTransformCache>,
186) -> Result<TracedTensor> {
187 grad_with_optional_rules(output, wrt, Some(extension_rules), ad_transform_cache)
188}
189
190pub(crate) fn jvp_with_rules_and_cache(
191 output: &TracedTensor,
192 wrt: &TracedTensor,
193 tangent: &TracedTensor,
194 extension_rules: &ExtensionRuleSet,
195 ad_transform_cache: Option<&AdTransformCache>,
196) -> Result<TracedTensor> {
197 let wrt_input_key = leaf_input_key(wrt)?;
198 jvp_optional_impl(
199 output,
200 wrt,
201 tangent,
202 Some(extension_rules),
203 ad_transform_cache,
204 )?
205 .ok_or_else(|| Error::Internal(format!("jvp output is inactive for {:?}", wrt_input_key)))
206}
207
208pub(crate) fn grad_optional_with_rules_and_cache(
209 output: &TracedTensor,
210 wrt: &TracedTensor,
211 extension_rules: &ExtensionRuleSet,
212 ad_transform_cache: Option<&AdTransformCache>,
213) -> Result<Option<TracedTensor>> {
214 if output.rank != 0 {
215 return Err(Error::NonScalarGrad {
216 shape: error_shape_hint(output),
217 });
218 }
219
220 let ones = ones_tensor(output.dtype, vec![])?;
221 let seed = TracedTensor::from_tensor_concrete_shape(ones)?;
222 vjp_optional_impl(
223 output,
224 wrt,
225 &seed,
226 Some(extension_rules),
227 "grad",
228 ad_transform_cache,
229 )
230}
231
232pub(crate) fn jvp_optional_with_rules_and_cache(
233 output: &TracedTensor,
234 wrt: &TracedTensor,
235 tangent: &TracedTensor,
236 extension_rules: &ExtensionRuleSet,
237 ad_transform_cache: Option<&AdTransformCache>,
238) -> Result<Option<TracedTensor>> {
239 jvp_optional_impl(
240 output,
241 wrt,
242 tangent,
243 Some(extension_rules),
244 ad_transform_cache,
245 )
246}
247
248pub(crate) fn vjp_with_rules_and_cache(
249 output: &TracedTensor,
250 wrt: &TracedTensor,
251 cotangent: &TracedTensor,
252 extension_rules: &ExtensionRuleSet,
253 ad_transform_cache: Option<&AdTransformCache>,
254) -> Result<TracedTensor> {
255 let wrt_input_key = leaf_input_key(wrt)?;
256 vjp_optional_impl(
257 output,
258 wrt,
259 cotangent,
260 Some(extension_rules),
261 "vjp",
262 ad_transform_cache,
263 )?
264 .ok_or_else(|| Error::Internal(format!("vjp output is inactive for {:?}", wrt_input_key)))
265}
266
267pub(crate) fn vjp_optional_with_rules_and_cache(
268 output: &TracedTensor,
269 wrt: &TracedTensor,
270 cotangent: &TracedTensor,
271 extension_rules: &ExtensionRuleSet,
272 ad_transform_cache: Option<&AdTransformCache>,
273) -> Result<Option<TracedTensor>> {
274 vjp_optional_impl(
275 output,
276 wrt,
277 cotangent,
278 Some(extension_rules),
279 "vjp",
280 ad_transform_cache,
281 )
282}
283
284fn grad_with_optional_rules(
285 output: &TracedTensor,
286 wrt: &TracedTensor,
287 extension_rules: Option<&ExtensionRuleSet>,
288 ad_transform_cache: Option<&AdTransformCache>,
289) -> Result<TracedTensor> {
290 if output.rank != 0 {
291 return Err(Error::NonScalarGrad {
292 shape: error_shape_hint(output),
293 });
294 }
295
296 let ones = ones_tensor(output.dtype, vec![])?;
297 let seed = TracedTensor::from_tensor_concrete_shape(ones)?;
298 let wrt_input_key = leaf_input_key(wrt)?;
299 vjp_optional_impl(
300 output,
301 wrt,
302 &seed,
303 extension_rules,
304 "grad",
305 ad_transform_cache,
306 )?
307 .ok_or_else(|| Error::Internal(format!("grad output is inactive for {:?}", wrt_input_key)))
308}
309
310pub trait TracedTensorAdExt {
324 fn grad(&self, wrt: &TracedTensor) -> Result<TracedTensor>;
352
353 fn grad_optional(&self, wrt: &TracedTensor) -> Result<Option<TracedTensor>>;
368
369 fn checkpoint<B: TensorBackend>(
390 &mut self,
391 compiler: &mut GraphCompiler,
392 executor: &mut GraphExecutor<B>,
393 ) -> Result<()>;
394
395 fn jvp(&self, wrt: &TracedTensor, tangent: &TracedTensor) -> Result<TracedTensor>;
419
420 fn jvp_optional(
436 &self,
437 wrt: &TracedTensor,
438 tangent: &TracedTensor,
439 ) -> Result<Option<TracedTensor>>;
440
441 fn vjp(&self, wrt: &TracedTensor, cotangent: &TracedTensor) -> Result<TracedTensor>;
470
471 fn vjp_optional(
487 &self,
488 wrt: &TracedTensor,
489 cotangent: &TracedTensor,
490 ) -> Result<Option<TracedTensor>>;
491}
492
493impl TracedTensorAdExt for TracedTensor {
494 fn grad(&self, wrt: &TracedTensor) -> Result<TracedTensor> {
495 grad_with_optional_rules(self, wrt, None, None)
496 }
497
498 fn grad_optional(&self, wrt: &TracedTensor) -> Result<Option<TracedTensor>> {
499 if self.rank != 0 {
500 return Err(Error::NonScalarGrad {
501 shape: error_shape_hint(self),
502 });
503 }
504
505 let ones = ones_tensor(self.dtype, vec![])?;
506 let seed = TracedTensor::from_tensor_concrete_shape(ones)?;
507 vjp_optional_impl(self, wrt, &seed, None, "grad", None)
508 }
509
510 fn checkpoint<B: TensorBackend>(
511 &mut self,
512 compiler: &mut GraphCompiler,
513 executor: &mut GraphExecutor<B>,
514 ) -> Result<()> {
515 let data = if let Some(data) = self.attached_data() {
516 Arc::clone(data)
517 } else {
518 let program = compiler.compile(self)?;
519 Arc::new(executor.run(&program)?)
520 };
521 checkpoint_tensor(self, data)?;
522 Ok(())
523 }
524
525 fn jvp(&self, wrt: &TracedTensor, tangent: &TracedTensor) -> Result<TracedTensor> {
526 let wrt_input_key = leaf_input_key(wrt)?;
527 self.jvp_optional(wrt, tangent)?.ok_or_else(|| {
528 Error::Internal(format!("jvp output is inactive for {:?}", wrt_input_key))
529 })
530 }
531
532 fn jvp_optional(
533 &self,
534 wrt: &TracedTensor,
535 tangent: &TracedTensor,
536 ) -> Result<Option<TracedTensor>> {
537 jvp_optional_impl(self, wrt, tangent, None, None)
538 }
539
540 fn vjp(&self, wrt: &TracedTensor, cotangent: &TracedTensor) -> Result<TracedTensor> {
541 let wrt_input_key = leaf_input_key(wrt)?;
542 self.vjp_optional(wrt, cotangent)?.ok_or_else(|| {
543 Error::Internal(format!("vjp output is inactive for {:?}", wrt_input_key))
544 })
545 }
546
547 fn vjp_optional(
548 &self,
549 wrt: &TracedTensor,
550 cotangent: &TracedTensor,
551 ) -> Result<Option<TracedTensor>> {
552 vjp_optional_impl(self, wrt, cotangent, None, "vjp", None)
553 }
554}
555
556fn jvp_optional_impl(
557 output: &TracedTensor,
558 wrt: &TracedTensor,
559 tangent: &TracedTensor,
560 extension_rules: Option<&ExtensionRuleSet>,
561 ad_transform_cache: Option<&AdTransformCache>,
562) -> Result<Option<TracedTensor>> {
563 let wrt_input_key = leaf_input_key(wrt)?;
564 let output_key = output.graph().values()[output.val].key.clone();
565 let checkpoint_chain = tensor_checkpoint_chain(output);
566 let aliases = checkpoint_chain
567 .as_ref()
568 .map(|chain| chain.collect_aliases())
569 .unwrap_or_default();
570 let checkpoint_graphs = checkpoint_chain
571 .as_ref()
572 .map(|chain| chain.collect_graphs())
573 .unwrap_or_default();
574 let mut roots = tensor_resolve_roots(output);
575 roots.extend(checkpoint_graphs.iter().cloned());
576 let view = resolve(roots);
577 let active_values =
578 linearize_active_value_keys(&view, std::slice::from_ref(&output_key), &aliases);
579 let cache_key = ad_transform_cache.map(|_| {
580 TracedAdTransformCacheKey::new(
581 TracedAdTransformKind::Jvp,
582 &view.roots,
583 &output_key,
584 &wrt_input_key,
585 &aliases,
586 )
587 });
588 let linear = match (ad_transform_cache, cache_key) {
589 (Some(cache), Some(key)) => {
590 if let Some(linear) = cache.get_traced_linearized(&key)? {
591 linear
592 } else {
593 let mut ad_ctx =
594 shape_guard_context(extension_rules, Some(active_values), &view.roots);
595 let linear = linearize(
596 &view,
597 std::slice::from_ref(&output_key),
598 std::slice::from_ref(&wrt_input_key),
599 next_pass_id(),
600 &mut ad_ctx,
601 &aliases,
602 )
603 .map_err(|err| ad_rule_error("jvp", err))?;
604 let linear = Arc::new(OptimizedLinearGraph::from_tidu(linear).into_cached());
605 cache.put_traced_linearized(key, Arc::clone(&linear))?;
606 linear
607 }
608 }
609 _ => {
610 let mut ad_ctx = shape_guard_context(extension_rules, Some(active_values), &view.roots);
611 let linear = linearize(
612 &view,
613 std::slice::from_ref(&output_key),
614 std::slice::from_ref(&wrt_input_key),
615 next_pass_id(),
616 &mut ad_ctx,
617 &aliases,
618 )
619 .map_err(|err| ad_rule_error("jvp", err))?;
620 Arc::new(OptimizedLinearGraph::from_tidu(linear).into_cached())
621 }
622 };
623 let Some(tangent_output) = linear.tangent_outputs()[0] else {
624 return Ok(None);
625 };
626 let tangent_input_key = linear_input_key(linear.as_graph(), linear.tangent_inputs()[0].1)?;
627 let tangent_data =
628 tangent
629 .attached_data()
630 .cloned()
631 .ok_or_else(|| Error::InvalidGraphBuild {
632 op: "jvp",
633 message: "jvp tangent must have concrete tensor data".to_string(),
634 })?;
635 let metadata_scope = register_scoped_graph_metadata(
636 linear.as_graph(),
637 vec![(
638 ValueKey::Input(tangent_input_key.clone()),
639 tensor_meta_from_tensor(tangent_data.as_ref()),
640 )],
641 )?;
642
643 let mut inputs_map = (*tensor_inputs_map(output)).clone();
644 if let Some(chain) = &checkpoint_chain {
645 inputs_map.extend(chain.collect_inputs());
646 }
647 inputs_map.insert(tangent_input_key, tangent_data);
648
649 let mut extra_roots = vec![Arc::clone(output.graph())];
650 extra_roots.extend(checkpoint_graphs);
651 extra_roots.extend(tensor_extra_roots(output));
652
653 Ok(Some(tensor_from_parts(TracedTensorParts {
654 rank: output.rank,
655 dtype: output.dtype,
656 graph: Arc::clone(linear.graph()),
657 val: tangent_output,
658 data: None,
659 shape_hint: tensor_shape_hint(output),
660 inputs_map: Arc::new(inputs_map),
661 extra_roots,
662 checkpoint_chain,
663 metadata_scopes: metadata_scopes_with_new(
664 metadata_scope,
665 [
666 tensor_metadata_scopes(output),
667 tensor_metadata_scopes(wrt),
668 tensor_metadata_scopes(tangent),
669 ],
670 ),
671 })))
672}
673
674enum VjpTransposeGraph {
675 Primal(PrimalTransposeGraph),
676 Linear(Arc<CachedTracedVjpTransform>),
677}
678
679struct ActiveLinearVjp {
680 transposed: Arc<CachedTracedVjpTransform>,
681 metadata_scope: GlobalMetadataScope,
682}
683
684impl VjpTransposeGraph {
685 fn as_graph(&self) -> &computegraph::graph::Graph<StdTensorOp> {
686 match self {
687 Self::Primal(graph) => graph.as_graph(),
688 Self::Linear(graph) => graph.transposed().as_graph(),
689 }
690 }
691
692 fn tangent_inputs(&self) -> &[(TensorInputKey, computegraph::LocalValueId)] {
693 match self {
694 Self::Primal(graph) => graph.tangent_inputs(),
695 Self::Linear(graph) => graph.transposed().tangent_inputs(),
696 }
697 }
698
699 fn tangent_outputs(&self) -> &[Option<computegraph::LocalValueId>] {
700 match self {
701 Self::Primal(graph) => graph.tangent_outputs(),
702 Self::Linear(graph) => graph.transposed().tangent_outputs(),
703 }
704 }
705
706 fn into_graph_arc(self) -> Arc<computegraph::graph::Graph<StdTensorOp>> {
707 match self {
708 Self::Primal(graph) => Arc::new(graph.into_graph()),
709 Self::Linear(graph) => Arc::clone(graph.transposed().graph()),
710 }
711 }
712}
713
714fn compute_linear_vjp_transform(
715 view: &ResolvedView<StdTensorOp>,
716 output_key: &ValueKey<StdTensorOp>,
717 wrt_input_key: &TensorInputKey,
718 aliases: &HashMap<TensorInputKey, ValueKey<StdTensorOp>>,
719 extension_rules: Option<&ExtensionRuleSet>,
720 active_values: Arc<HashSet<ValueKey<StdTensorOp>>>,
721 wrt: &TracedTensor,
722) -> Result<tidu::ADRuleResult<Option<ActiveLinearVjp>>> {
723 let mut linear_ad_ctx = shape_guard_context(extension_rules, Some(active_values), &view.roots);
724 let linear = match linearize(
725 view,
726 std::slice::from_ref(output_key),
727 std::slice::from_ref(wrt_input_key),
728 next_pass_id(),
729 &mut linear_ad_ctx,
730 aliases,
731 ) {
732 Ok(linear) => linear,
733 Err(err) => return Ok(Err(err)),
734 };
735 if linear.tangent_outputs()[0].is_none() {
736 return Ok(Ok(None));
737 }
738
739 let linear_seed_key = linear_input_key(linear.as_graph(), linear.tangent_inputs()[0].1)?;
740 let linear_metadata_scope = register_scoped_graph_metadata(
741 linear.as_graph(),
742 vec![(
743 ValueKey::Input(linear_seed_key),
744 registered_meta(&wrt.graph().values()[wrt.val].key)?,
745 )],
746 )?;
747 linear_ad_ctx.refresh_global_metadata();
748 let transposed = match linear_transpose(&linear, &mut linear_ad_ctx) {
749 Ok(transposed) => OptimizedLinearGraph::from_tidu(transposed).into_cached(),
750 Err(err) => return Ok(Err(err)),
751 };
752 let (_linear_graph, residual_graph) = linear.into_graphs();
753 let residual_metadata_scope =
754 register_scoped_graph_metadata(residual_graph.as_ref(), std::iter::empty())?;
755 drop(linear_metadata_scope);
756 Ok(Ok(Some(ActiveLinearVjp {
757 transposed: Arc::new(CachedTracedVjpTransform::new(residual_graph, transposed)),
758 metadata_scope: residual_metadata_scope,
759 })))
760}
761
762fn vjp_optional_impl(
763 output: &TracedTensor,
764 wrt: &TracedTensor,
765 cotangent: &TracedTensor,
766 extension_rules: Option<&ExtensionRuleSet>,
767 transform: &'static str,
768 ad_transform_cache: Option<&AdTransformCache>,
769) -> Result<Option<TracedTensor>> {
770 let wrt_input_key = leaf_input_key(wrt)?;
771 let output_key = output.graph().values()[output.val].key.clone();
772 let checkpoint_chain = tensor_checkpoint_chain(output);
773 let aliases = checkpoint_chain
774 .as_ref()
775 .map(|chain| chain.collect_aliases())
776 .unwrap_or_default();
777 let checkpoint_graphs = checkpoint_chain
778 .as_ref()
779 .map(|chain| chain.collect_graphs())
780 .unwrap_or_default();
781 let mut roots = tensor_resolve_roots(output);
782 roots.extend(checkpoint_graphs.iter().cloned());
783 let view = resolve(roots);
784
785 let active_values =
786 linearize_active_value_keys(&view, std::slice::from_ref(&output_key), &aliases);
787 let cache_key = ad_transform_cache.map(|_| {
788 TracedAdTransformCacheKey::new(
789 TracedAdTransformKind::Vjp,
790 &view.roots,
791 &output_key,
792 &wrt_input_key,
793 &aliases,
794 )
795 });
796 if graph_has_registered_primal_vjp(
797 &view,
798 std::slice::from_ref(&output_key),
799 &aliases,
800 extension_rules,
801 ) {
802 let mut primal_ad_ctx = shape_guard_context(extension_rules, None, &view.roots);
803 primal_ad_ctx.refresh_global_metadata();
804 match try_primal_transpose(
805 &view,
806 std::slice::from_ref(&output_key),
807 std::slice::from_ref(&wrt_input_key),
808 &aliases,
809 &mut primal_ad_ctx,
810 next_pass_id(),
811 ) {
812 Ok(transposed) => {
813 if transposed
814 .tangent_outputs()
815 .first()
816 .and_then(|slot| *slot)
817 .is_some()
818 {
819 let transposed = VjpTransposeGraph::Primal(transposed);
820 return build_vjp_tensor(
821 output,
822 wrt,
823 cotangent,
824 transposed,
825 None,
826 checkpoint_chain,
827 checkpoint_graphs,
828 );
829 }
830 return Ok(None);
831 }
832 Err(err) if !is_not_applicable_custom_vjp(&err) => {
833 return Err(ad_rule_error(transform, err));
834 }
835 Err(_) => {}
836 }
837 }
838
839 let linear_attempt = match (ad_transform_cache, cache_key) {
840 (Some(cache), Some(key)) => {
841 if let Some(cached) = cache.get_traced_vjp(&key)? {
842 let residual_metadata_scope =
843 register_scoped_graph_metadata(cached.residual_graph(), std::iter::empty())?;
844 Ok(Some(ActiveLinearVjp {
845 transposed: cached,
846 metadata_scope: residual_metadata_scope,
847 }))
848 } else {
849 let computed = compute_linear_vjp_transform(
850 &view,
851 &output_key,
852 &wrt_input_key,
853 &aliases,
854 extension_rules,
855 active_values,
856 wrt,
857 )?;
858 if let Ok(Some(active)) = &computed {
859 cache.put_traced_vjp(key, Arc::clone(&active.transposed))?;
860 }
861 computed
862 }
863 }
864 _ => compute_linear_vjp_transform(
865 &view,
866 &output_key,
867 &wrt_input_key,
868 &aliases,
869 extension_rules,
870 active_values,
871 wrt,
872 )?,
873 };
874
875 let (transposed, linear_metadata_scope) = match linear_attempt {
876 Ok(None) => return Ok(None),
877 Ok(Some(active)) => (
878 VjpTransposeGraph::Linear(active.transposed),
879 Some(active.metadata_scope),
880 ),
881 Err(linear_err) => return Err(ad_rule_error(transform, linear_err)),
882 };
883
884 build_vjp_tensor(
885 output,
886 wrt,
887 cotangent,
888 transposed,
889 linear_metadata_scope,
890 checkpoint_chain,
891 checkpoint_graphs,
892 )
893}
894
895fn build_vjp_tensor(
896 output: &TracedTensor,
897 wrt: &TracedTensor,
898 cotangent: &TracedTensor,
899 transposed: VjpTransposeGraph,
900 linear_metadata_scope: Option<GlobalMetadataScope>,
901 checkpoint_chain: Option<Arc<tenferro_runtime::ad_support::CheckpointNode>>,
902 checkpoint_graphs: Vec<Arc<Graph<StdTensorOp>>>,
903) -> Result<Option<TracedTensor>> {
904 let cotangent_input_key =
905 linear_input_key(transposed.as_graph(), transposed.tangent_inputs()[0].1)?;
906 let cotangent_data =
907 cotangent
908 .attached_data()
909 .cloned()
910 .ok_or_else(|| Error::InvalidGraphBuild {
911 op: "vjp",
912 message: "vjp cotangent must have concrete tensor data".to_string(),
913 })?;
914 let transposed_metadata_scope = register_scoped_graph_metadata(
915 transposed.as_graph(),
916 vec![(
917 ValueKey::Input(cotangent_input_key.clone()),
918 tensor_meta_from_tensor(cotangent_data.as_ref()),
919 )],
920 )?;
921 let Some(cotangent_output) = transposed.tangent_outputs()[0] else {
922 return Ok(None);
923 };
924
925 let mut inputs_map = (*tensor_inputs_map(output)).clone();
926 if let Some(chain) = &checkpoint_chain {
927 inputs_map.extend(chain.collect_inputs());
928 }
929 inputs_map.insert(cotangent_input_key.clone(), cotangent_data);
930
931 let mut extra_roots = vec![Arc::clone(output.graph())];
932 if let VjpTransposeGraph::Linear(cached) = &transposed {
933 extra_roots.push(Arc::clone(cached.residual_graph()));
934 }
935 extra_roots.extend(checkpoint_graphs);
936 extra_roots.extend(tensor_extra_roots(output));
937
938 Ok(Some(tensor_from_parts(TracedTensorParts {
939 rank: wrt.rank,
940 dtype: wrt.dtype,
941 graph: transposed.into_graph_arc(),
942 val: cotangent_output,
943 data: None,
944 shape_hint: tensor_shape_hint(wrt),
945 inputs_map: Arc::new(inputs_map),
946 extra_roots,
947 checkpoint_chain,
948 metadata_scopes: {
949 let mut scopes = if let Some(scope) = linear_metadata_scope {
950 metadata_scopes_with_new(
951 scope,
952 [
953 tensor_metadata_scopes(output),
954 tensor_metadata_scopes(wrt),
955 tensor_metadata_scopes(cotangent),
956 ],
957 )
958 } else {
959 let mut scopes: Vec<Arc<crate::metadata::GlobalMetadataScope>> = Vec::new();
960 for inherited in [
961 tensor_metadata_scopes(output),
962 tensor_metadata_scopes(wrt),
963 tensor_metadata_scopes(cotangent),
964 ] {
965 for scope in inherited {
966 scopes.push(Arc::clone(scope));
967 }
968 }
969 scopes
970 };
971 push_metadata_scope(&mut scopes, Arc::new(transposed_metadata_scope));
972 scopes
973 },
974 })))
975}