1use std::any::Any;
2use std::collections::hash_map::DefaultHasher;
3use std::collections::HashMap;
4#[cfg(feature = "autodiff")]
5use std::collections::HashSet;
6use std::hash::{Hash, Hasher};
7use std::sync::Arc;
8
9use computegraph::graph::GraphBuilder;
10use computegraph::types::ValueRef;
11#[cfg(feature = "autodiff")]
12use tenferro_ad::semantic_extension::{
13 AdValue, SemanticAdError, SemanticAdRuleRole, SemanticExtensionRegistryError,
14 SemanticExtensionRuleSet, SemanticLinearTransposeRequest, SemanticLinearTransposeRule,
15 SemanticLinearizeRequest, SemanticLinearizeResult, SemanticLinearizeRule,
16 SemanticPrimalVjpRequest, SemanticPrimalVjpRule,
17};
18use tenferro_extension_macros::define_extension_runtime;
19#[cfg(feature = "autodiff")]
20use tenferro_ops::dim_expr::DimExpr;
21use tenferro_ops::ext_op::{
22 ExtensionLoweringError, ExtensionLoweringResult, ExtensionOp, ExtensionStandardLowering,
23};
24use tenferro_ops::std_tensor_op::StdTensorOp;
25use tenferro_ops::sym_dim::SymDim;
26use tenferro_runtime::extension::{ExtensionCacheKey, ExtensionExecutionContext};
27#[cfg(feature = "autodiff")]
28use tenferro_runtime::program::{CoreSemanticOp, ProgramValue, SemanticProgramBuilder};
29use tenferro_tensor::{
30 BackendSession, DType, Error as TensorError, Tensor, TensorBackend, TensorRead,
31};
32
33use crate::builder::build_einsum_graph;
34use crate::cache::{
35 einsum_subscripts_retained_bytes, saturating_sum, vec_retained_bytes,
36 EINSUM_EXTENSION_FAMILY_ID, EINSUM_RUNTIME_PLANS_CACHE,
37};
38#[cfg(test)]
39use crate::optimize::default_auto_options;
40#[cfg(feature = "autodiff")]
41use crate::optimize::jax_path_to_v1_pairs;
42use crate::optimize::{hash_einsum_plan_spec, plan_specs_equal, resolve_plan_spec, EinsumPlanSpec};
43#[cfg(feature = "autodiff")]
44use crate::util::map_label_occurrences;
45use crate::{
46 ContractionTree, EinsumSubscripts, Error as EinsumError, Result as EinsumResult, Subscripts,
47};
48
49#[derive(Clone)]
54pub(crate) struct EinsumExtensionOp {
55 subscripts: EinsumSubscripts,
56 plan_spec: EinsumPlanSpec,
57 output_shape_hint: Option<Vec<SymDim>>,
58}
59
60impl std::fmt::Debug for EinsumExtensionOp {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("EinsumExtensionOp")
63 .field("subscripts", &self.subscripts)
64 .field("plan_spec", &self.plan_spec)
65 .field("output_shape_hint", &self.output_shape_hint)
66 .finish()
67 }
68}
69
70impl EinsumExtensionOp {
71 #[must_use]
73 #[cfg(test)]
74 pub(crate) fn new(subscripts: EinsumSubscripts) -> Self {
75 Self::with_plan_spec(subscripts, EinsumPlanSpec::Auto(default_auto_options()))
76 }
77
78 #[must_use]
79 pub(crate) fn with_plan_spec(subscripts: EinsumSubscripts, plan_spec: EinsumPlanSpec) -> Self {
80 Self {
81 subscripts,
82 plan_spec,
83 output_shape_hint: None,
84 }
85 }
86
87 #[must_use]
89 #[cfg(any(feature = "autodiff", test))]
90 pub(crate) fn with_output_shape_hint(
91 subscripts: EinsumSubscripts,
92 output_shape_hint: Vec<SymDim>,
93 plan_spec: EinsumPlanSpec,
94 ) -> Self {
95 let mut op = Self::with_plan_spec(subscripts, plan_spec);
96 op.output_shape_hint = Some(output_shape_hint);
97 op
98 }
99
100 #[must_use]
102 pub(crate) fn subscripts(&self) -> &EinsumSubscripts {
103 &self.subscripts
104 }
105
106 #[must_use]
108 pub(crate) fn plan_spec(&self) -> &EinsumPlanSpec {
109 &self.plan_spec
110 }
111}
112
113impl ExtensionOp for EinsumExtensionOp {
114 fn family_id(&self) -> &'static str {
115 EINSUM_EXTENSION_FAMILY_ID
116 }
117
118 fn payload_hash(&self, hasher: &mut dyn Hasher) {
119 hasher.write_usize(self.subscripts.inputs.len());
120 for input in &self.subscripts.inputs {
121 hasher.write_usize(input.len());
122 for label in input {
123 hasher.write_u32(*label);
124 }
125 }
126 hasher.write_usize(self.subscripts.output.len());
127 for label in &self.subscripts.output {
128 hasher.write_u32(*label);
129 }
130 hash_einsum_plan_spec(self.plan_spec(), hasher);
131 if let Some(shape) = &self.output_shape_hint {
132 hasher.write_usize(shape.len());
133 for dim in shape {
134 match dim.constant_value() {
135 Some(value) => {
136 hasher.write_u8(1);
137 hasher.write_usize(value);
138 }
139 None => hasher.write_u8(0),
140 }
141 }
142 } else {
143 hasher.write_usize(usize::MAX);
144 }
145 }
146
147 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
148 other.as_any().downcast_ref::<Self>().is_some_and(|that| {
149 self.subscripts == that.subscripts
150 && plan_specs_equal(self.plan_spec(), that.plan_spec())
151 && self.output_shape_hint == that.output_shape_hint
152 })
153 }
154
155 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
156 Arc::new(self.clone())
157 }
158
159 fn as_any(&self) -> &dyn Any {
160 self
161 }
162
163 fn input_count(&self) -> usize {
164 self.subscripts.inputs.len()
165 }
166
167 fn output_count(&self) -> usize {
168 1
169 }
170
171 fn semantic_effects(&self) -> tenferro_ops::ext_op::ExtensionEffectDeclaration<'_> {
172 tenferro_ops::ext_op::ExtensionEffectDeclaration::Declared(&[])
173 }
174
175 fn semantic_aliases(&self) -> tenferro_ops::ext_op::ExtensionAliasDeclaration<'_> {
176 tenferro_ops::ext_op::ExtensionAliasDeclaration::AllFresh
177 }
178
179 fn infer_output_meta(
180 &self,
181 ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
182 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
183 let input_dtypes = (0..self.input_count())
184 .map(|input| ctx.input_dtype(input))
185 .collect::<Result<Vec<_>, _>>()?;
186 let input_shapes = (0..self.input_count())
187 .map(|input| ctx.input_shape(input).map(<[_]>::to_vec))
188 .collect::<Result<Vec<_>, _>>()?;
189
190 let mut label_dims: HashMap<u32, SymDim> = HashMap::new();
191 for (labels, shape) in self.subscripts.inputs.iter().zip(input_shapes.iter()) {
192 if labels.len() != shape.len() {
193 return Err(TensorError::rank_mismatch(
194 "einsum",
195 labels.len(),
196 shape.len(),
197 ));
198 }
199 for (&label, dim) in labels.iter().zip(shape.iter()) {
200 if let Some(existing) = label_dims.get(&label) {
201 ctx.require_equal(existing.clone(), dim.clone())?;
202 } else {
203 label_dims.insert(label, dim.clone());
204 }
205 }
206 }
207
208 let output_shape = match &self.output_shape_hint {
209 Some(shape) if shape.iter().all(|dim| dim.constant_value().is_some()) => shape.clone(),
210 _ => self
211 .subscripts
212 .output
213 .iter()
214 .map(|label| label_dims.get(label).cloned())
215 .collect::<Option<Vec<_>>>()
216 .ok_or_else(|| {
217 TensorError::invalid_argument(
218 "einsum",
219 "output labels",
220 "must be present in input metadata",
221 )
222 })?,
223 };
224 if output_shape.len() != self.subscripts.output.len() {
225 return Err(TensorError::rank_mismatch(
226 "einsum",
227 self.subscripts.output.len(),
228 output_shape.len(),
229 ));
230 }
231 Ok(vec![(
232 promote_dtypes(input_dtypes.iter().copied()),
233 output_shape,
234 )])
235 }
236
237 fn lower_to_standard_ops(
238 &self,
239 builder: &mut GraphBuilder<StdTensorOp>,
240 inputs: &[ValueRef<StdTensorOp>],
241 input_dtypes: &[DType],
242 input_shapes: &[&[SymDim]],
243 ) -> ExtensionLoweringResult {
244 if inputs.len() != self.input_count()
245 || input_dtypes.len() != self.input_count()
246 || input_shapes.len() != self.input_count()
247 {
248 return Err(ExtensionLoweringError::new(format!(
249 "einsum extension expects {} inputs, got values={}, dtypes={}, shapes={}",
250 self.input_count(),
251 inputs.len(),
252 input_dtypes.len(),
253 input_shapes.len()
254 )));
255 }
256
257 let Some(shapes) = concrete_sym_shape_slices(input_shapes) else {
258 return Ok(ExtensionStandardLowering::Unsupported);
259 };
260 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
261 let subs = Subscripts::from(&self.subscripts);
262 let tree = resolve_plan_spec(self.plan_spec(), &subs, &shape_refs).map_err(|source| {
263 ExtensionLoweringError::from_source_with_kind(source.kind(), source)
264 })?;
265 let output = build_einsum_graph(builder, &tree, inputs, &shapes).map_err(|source| {
266 ExtensionLoweringError::from_source_with_kind(source.kind(), source)
267 })?;
268 Ok(ExtensionStandardLowering::Lowered(vec![output]))
269 }
270}
271
272fn concrete_sym_shape_slices(input_shapes: &[&[SymDim]]) -> Option<Vec<Vec<usize>>> {
273 input_shapes
274 .iter()
275 .map(|shape| {
276 shape
277 .iter()
278 .map(SymDim::constant_value)
279 .collect::<Option<Vec<_>>>()
280 })
281 .collect()
282}
283
284#[cfg(feature = "autodiff")]
286pub fn semantic_ad_rules(
293) -> std::result::Result<SemanticExtensionRuleSet, SemanticExtensionRegistryError> {
294 SemanticExtensionRuleSet::new()
295 .with_linearize(Arc::new(EinsumAdRule))?
296 .with_linear_transpose(Arc::new(EinsumAdRule))?
297 .with_primal_vjp(Arc::new(EinsumAdRule))
298}
299
300#[derive(Debug)]
301#[cfg(feature = "autodiff")]
302struct EinsumAdRule;
303
304#[cfg(feature = "autodiff")]
305impl SemanticLinearizeRule for EinsumAdRule {
306 fn family_id(&self) -> &'static str {
307 EINSUM_EXTENSION_FAMILY_ID
308 }
309
310 fn linearize(
311 &self,
312 request: SemanticLinearizeRequest<'_>,
313 builder: &mut SemanticProgramBuilder,
314 ) -> std::result::Result<SemanticLinearizeResult, SemanticAdError> {
315 let op = semantic_einsum_payload(request.op(), SemanticAdRuleRole::Linearize)?;
316 if !request.active_outputs()[0] {
317 return Ok(SemanticLinearizeResult::new([AdValue::Absent], []));
318 }
319 let mut terms = Vec::new();
320 for (active_idx, tangent) in request.tangent_inputs().iter().copied().enumerate() {
321 let AdValue::Value(tangent) = tangent else {
322 continue;
323 };
324 let inputs: Vec<_> = request
325 .primal_inputs()
326 .iter()
327 .copied()
328 .enumerate()
329 .map(|(input_idx, primal)| {
330 if input_idx == active_idx {
331 tangent
332 } else {
333 primal
334 }
335 })
336 .collect();
337 terms.push(builder.add_extension(Arc::new(op.clone()), &inputs)?[0]);
338 }
339 let tangent = semantic_sum_terms(builder, terms)?;
340 Ok(SemanticLinearizeResult::new([tangent], []))
341 }
342}
343
344#[cfg(feature = "autodiff")]
345impl SemanticLinearTransposeRule for EinsumAdRule {
346 fn family_id(&self) -> &'static str {
347 EINSUM_EXTENSION_FAMILY_ID
348 }
349
350 fn linear_transpose(
351 &self,
352 request: SemanticLinearTransposeRequest<'_>,
353 builder: &mut SemanticProgramBuilder,
354 ) -> std::result::Result<Box<[AdValue]>, SemanticAdError> {
355 semantic_einsum_vjp(
356 request.op(),
357 request.primal_inputs(),
358 request.primal_outputs(),
359 request.cotangent_outputs(),
360 request.active_inputs(),
361 builder,
362 )
363 }
364}
365
366#[cfg(feature = "autodiff")]
367impl SemanticPrimalVjpRule for EinsumAdRule {
368 fn family_id(&self) -> &'static str {
369 EINSUM_EXTENSION_FAMILY_ID
370 }
371
372 fn primal_vjp(
373 &self,
374 request: SemanticPrimalVjpRequest<'_>,
375 builder: &mut SemanticProgramBuilder,
376 ) -> std::result::Result<Box<[AdValue]>, SemanticAdError> {
377 semantic_einsum_vjp(
378 request.op(),
379 request.primal_inputs(),
380 request.primal_outputs(),
381 request.cotangent_outputs(),
382 request.active_inputs(),
383 builder,
384 )
385 }
386}
387
388#[cfg(feature = "autodiff")]
389fn semantic_einsum_vjp(
390 payload: &dyn ExtensionOp,
391 primal_inputs: &[ProgramValue],
392 primal_outputs: &[ProgramValue],
393 cotangent_outputs: &[AdValue],
394 active_inputs: &[bool],
395 builder: &mut SemanticProgramBuilder,
396) -> std::result::Result<Box<[AdValue]>, SemanticAdError> {
397 let op = semantic_einsum_payload(payload, SemanticAdRuleRole::LinearTranspose)?;
398 let input_count = op.subscripts.inputs.len();
399 let AdValue::Value(cotangent) = cotangent_outputs[0] else {
400 return Ok(vec![AdValue::Absent; input_count].into_boxed_slice());
401 };
402 let primal_input_shapes = primal_inputs
403 .iter()
404 .copied()
405 .map(|value| semantic_value_shape(builder, value))
406 .collect::<std::result::Result<Vec<_>, _>>()?;
407 let cotangent_shape = semantic_value_shape(builder, primal_outputs[0])?;
408
409 let input_labels = &op.subscripts.inputs;
410 let output_labels = &op.subscripts.output;
411 let mut result = Vec::with_capacity(input_count);
412 for active_idx in 0..input_count {
413 if !active_inputs[active_idx] {
414 result.push(AdValue::Absent);
415 continue;
416 }
417 let mut available_labels: HashSet<u32> = output_labels.iter().copied().collect();
418 for (input_idx, labels) in input_labels.iter().enumerate() {
419 if input_idx != active_idx {
420 available_labels.extend(labels.iter().copied());
421 }
422 }
423 let vjp_output_labels: Vec<u32> = input_labels[active_idx]
424 .iter()
425 .copied()
426 .filter(|label| available_labels.contains(label))
427 .collect();
428 let mut vjp_input_labels = vec![output_labels.clone()];
429 let mut vjp_inputs = vec![cotangent];
430 let mut vjp_input_shapes = vec![cotangent_shape.clone()];
431 for input_idx in 0..input_count {
432 if input_idx == active_idx {
433 continue;
434 }
435 vjp_input_labels.push(input_labels[input_idx].clone());
436 vjp_input_shapes.push(primal_input_shapes[input_idx].clone());
437 vjp_inputs.push(semantic_conjugate_if_complex(
438 builder,
439 primal_inputs[input_idx],
440 )?);
441 }
442 let vjp_op = semantic_vjp_einsum_op(
443 op,
444 active_idx,
445 EinsumSubscripts {
446 inputs: vjp_input_labels,
447 output: vjp_output_labels.clone(),
448 },
449 &vjp_input_shapes,
450 )?;
451 let mut input_cotangent = builder.add_extension(Arc::new(vjp_op), &vjp_inputs)?[0];
452 if vjp_output_labels != input_labels[active_idx] {
453 input_cotangent = semantic_broadcast_einsum_vjp(
454 builder,
455 input_cotangent,
456 &vjp_output_labels,
457 &input_labels[active_idx],
458 primal_input_shapes[active_idx].clone(),
459 )?;
460 }
461 result.push(AdValue::Value(input_cotangent));
462 }
463 Ok(result.into_boxed_slice())
464}
465
466#[cfg(feature = "autodiff")]
467fn semantic_vjp_einsum_op(
468 primal_op: &EinsumExtensionOp,
469 active_idx: usize,
470 subscripts: EinsumSubscripts,
471 input_shapes: &[Vec<DimExpr>],
472) -> std::result::Result<EinsumExtensionOp, SemanticAdError> {
473 let plan_spec =
474 vjp_plan_spec_for_active(primal_op.plan_spec(), primal_op.input_count(), active_idx)?;
475 let sym_shapes: Vec<Vec<SymDim>> = input_shapes
476 .iter()
477 .enumerate()
478 .map(|(input_idx, shape)| {
479 let tensor_id = u64::MAX - input_idx as u64;
480 shape
481 .iter()
482 .enumerate()
483 .map(|(axis, dim)| match dim {
484 DimExpr::Const(value) => SymDim::from(*value),
485 _ => SymDim::tensor_axis(tensor_id, axis),
486 })
487 .collect()
488 })
489 .collect();
490 if let Some(concrete_shapes) = concrete_sym_shapes(&sym_shapes) {
491 let shape_refs: Vec<&[usize]> = concrete_shapes.iter().map(Vec::as_slice).collect();
492 let raw_subscripts = Subscripts::from(&subscripts);
493 let _tree = resolve_plan_spec(&plan_spec, &raw_subscripts, &shape_refs)
494 .map_err(|source| semantic_einsum_unsupported(source.to_string()))?;
495 }
496 Ok(EinsumExtensionOp::with_plan_spec(subscripts, plan_spec))
497}
498
499#[cfg(feature = "autodiff")]
500fn semantic_value_shape(
501 builder: &SemanticProgramBuilder,
502 value: ProgramValue,
503) -> std::result::Result<Vec<DimExpr>, SemanticAdError> {
504 builder
505 .value_metadata(value)?
506 .shape()
507 .iter()
508 .map(|extent| {
509 extent.bound_expr().cloned().ok_or_else(|| {
510 semantic_einsum_unsupported(
511 "einsum semantic AD requires a symbolic expression for every extent",
512 )
513 })
514 })
515 .collect()
516}
517
518#[cfg(feature = "autodiff")]
519fn semantic_conjugate_if_complex(
520 builder: &mut SemanticProgramBuilder,
521 value: ProgramValue,
522) -> std::result::Result<ProgramValue, SemanticAdError> {
523 if matches!(
524 builder.value_metadata(value)?.dtype(),
525 DType::C32 | DType::C64
526 ) {
527 Ok(builder.add_op(CoreSemanticOp::Conj, &[value])?[0])
528 } else {
529 Ok(value)
530 }
531}
532
533#[cfg(feature = "autodiff")]
534fn semantic_broadcast_einsum_vjp(
535 builder: &mut SemanticProgramBuilder,
536 cotangent: ProgramValue,
537 cotangent_labels: &[u32],
538 input_labels: &[u32],
539 shape: Vec<DimExpr>,
540) -> std::result::Result<ProgramValue, SemanticAdError> {
541 let dims = map_label_occurrences(cotangent_labels, input_labels).ok_or_else(|| {
542 semantic_einsum_unsupported(format!(
543 "einsum VJP cannot remap labels {cotangent_labels:?} into {input_labels:?}"
544 ))
545 })?;
546 let broadcast =
547 builder.add_op(CoreSemanticOp::BroadcastInDim { shape, dims }, &[cotangent])?[0];
548 semantic_project_repeated_labels(builder, broadcast, input_labels)
549}
550
551#[cfg(feature = "autodiff")]
552fn semantic_project_repeated_labels(
553 builder: &mut SemanticProgramBuilder,
554 cotangent: ProgramValue,
555 labels: &[u32],
556) -> std::result::Result<ProgramValue, SemanticAdError> {
557 let mut result = cotangent;
558 let mut first_axis_by_label = HashMap::new();
559 for (axis_b, label) in labels.iter().copied().enumerate() {
560 let Some(&axis_a) = first_axis_by_label.get(&label) else {
561 first_axis_by_label.insert(label, axis_b);
562 continue;
563 };
564 let extracted =
565 builder.add_op(CoreSemanticOp::ExtractDiag { axis_a, axis_b }, &[result])?[0];
566 result = builder.add_op(CoreSemanticOp::EmbedDiag { axis_a, axis_b }, &[extracted])?[0];
567 }
568 Ok(result)
569}
570
571#[cfg(feature = "autodiff")]
572fn semantic_sum_terms(
573 builder: &mut SemanticProgramBuilder,
574 terms: Vec<ProgramValue>,
575) -> std::result::Result<AdValue, SemanticAdError> {
576 let mut terms = terms.into_iter();
577 let Some(mut sum) = terms.next() else {
578 return Ok(AdValue::Absent);
579 };
580 for term in terms {
581 sum = builder.add_op(CoreSemanticOp::Add, &[sum, term])?[0];
582 }
583 Ok(AdValue::Value(sum))
584}
585
586#[cfg(feature = "autodiff")]
587fn semantic_einsum_payload(
588 op: &dyn ExtensionOp,
589 role: SemanticAdRuleRole,
590) -> std::result::Result<&EinsumExtensionOp, SemanticAdError> {
591 op.as_any()
592 .downcast_ref::<EinsumExtensionOp>()
593 .ok_or_else(|| SemanticAdError::Unsupported {
594 family_id: EINSUM_EXTENSION_FAMILY_ID,
595 role,
596 message: "einsum semantic AD received an incompatible payload".into(),
597 })
598}
599
600#[cfg(feature = "autodiff")]
601fn semantic_einsum_unsupported(message: impl Into<String>) -> SemanticAdError {
602 SemanticAdError::Unsupported {
603 family_id: EINSUM_EXTENSION_FAMILY_ID,
604 role: SemanticAdRuleRole::LinearTranspose,
605 message: message.into(),
606 }
607}
608
609#[cfg(feature = "autodiff")]
610fn vjp_plan_spec_for_active(
611 primal_plan: &EinsumPlanSpec,
612 input_count: usize,
613 active_idx: usize,
614) -> std::result::Result<EinsumPlanSpec, SemanticAdError> {
615 if active_idx >= input_count {
616 return Err(semantic_einsum_unsupported(format!(
617 "einsum VJP active input {active_idx} is outside {input_count} inputs"
618 )));
619 }
620
621 match primal_plan {
622 EinsumPlanSpec::Auto(options) => Ok(EinsumPlanSpec::Auto(options.clone())),
623 EinsumPlanSpec::LeftToRight => Ok(EinsumPlanSpec::LeftToRight),
624 EinsumPlanSpec::Path(path) => {
625 let pairs = jax_path_to_v1_pairs(path, input_count).map_err(|err| {
626 semantic_einsum_unsupported(format!(
627 "failed to inherit einsum Path plan for VJP active input {active_idx}: {err}"
628 ))
629 })?;
630 derive_vjp_fixed_pairs(&pairs, input_count, active_idx).map(EinsumPlanSpec::FixedPairs)
631 }
632 EinsumPlanSpec::FixedPairs(pairs) => {
633 derive_vjp_fixed_pairs(pairs, input_count, active_idx).map(EinsumPlanSpec::FixedPairs)
634 }
635 }
636}
637
638#[cfg(feature = "autodiff")]
639fn derive_vjp_fixed_pairs(
640 primal_pairs: &[(usize, usize)],
641 input_count: usize,
642 active_idx: usize,
643) -> std::result::Result<Vec<(usize, usize)>, SemanticAdError> {
644 if input_count == 0 {
645 return Err(semantic_einsum_unsupported(
646 "einsum VJP cannot derive a plan for zero primal inputs",
647 ));
648 }
649 if active_idx >= input_count {
650 return Err(semantic_einsum_unsupported(format!(
651 "einsum VJP active input {active_idx} is outside {input_count} inputs"
652 )));
653 }
654 let required_steps = input_count.saturating_sub(1);
655 if primal_pairs.len() != required_steps {
656 return Err(semantic_einsum_unsupported(format!(
657 "einsum VJP cannot inherit explicit plan for active input {active_idx}: \
658 expected {required_steps} primal steps for {input_count} inputs, got {}",
659 primal_pairs.len()
660 )));
661 }
662 if input_count == 1 {
663 return Ok(Vec::new());
664 }
665
666 let children = fixed_pair_children(primal_pairs, input_count, active_idx)?;
667 let mut primal_to_vjp = vec![None; input_count];
668 let mut next_vjp_input = 1;
669 for (input_idx, slot) in primal_to_vjp.iter_mut().enumerate() {
670 if input_idx != active_idx {
671 *slot = Some(next_vjp_input);
672 next_vjp_input += 1;
673 }
674 }
675
676 let root = input_count + primal_pairs.len() - 1;
677 let mut pairs = Vec::with_capacity(required_steps);
678 let final_id = emit_vjp_adjoint(
679 root,
680 0,
681 &children,
682 input_count,
683 active_idx,
684 &primal_to_vjp,
685 &mut pairs,
686 )?;
687 let expected_final = input_count + pairs.len() - 1;
688 if final_id != expected_final || pairs.len() != required_steps {
689 return Err(semantic_einsum_unsupported(format!(
690 "einsum VJP plan derivation for active input {active_idx} produced an invalid \
691 tree: final id {final_id}, expected {expected_final}, steps {}",
692 pairs.len()
693 )));
694 }
695 Ok(pairs)
696}
697
698#[cfg(feature = "autodiff")]
699fn fixed_pair_children(
700 pairs: &[(usize, usize)],
701 input_count: usize,
702 active_idx: usize,
703) -> std::result::Result<Vec<Option<(usize, usize)>>, SemanticAdError> {
704 let mut live = vec![false; input_count + pairs.len()];
705 for slot in live.iter_mut().take(input_count) {
706 *slot = true;
707 }
708 let mut children = vec![None; input_count + pairs.len()];
709
710 for (step_idx, &(left, right)) in pairs.iter().enumerate() {
711 let next_idx = input_count + step_idx;
712 if left == right {
713 return Err(invalid_vjp_plan_error(
714 active_idx,
715 format!("pair ({left}, {right}) references the same operand"),
716 ));
717 }
718 if left >= next_idx || right >= next_idx {
719 return Err(invalid_vjp_plan_error(
720 active_idx,
721 format!("pair ({left}, {right}) references a non-existent operand"),
722 ));
723 }
724 if !live[left] || !live[right] {
725 return Err(invalid_vjp_plan_error(
726 active_idx,
727 format!("pair ({left}, {right}) references an operand that is no longer live"),
728 ));
729 }
730
731 live[left] = false;
732 live[right] = false;
733 live[next_idx] = true;
734 children[next_idx] = Some((left, right));
735 }
736
737 let live_count = live.iter().filter(|&&is_live| is_live).count();
738 if live_count != 1 {
739 return Err(invalid_vjp_plan_error(
740 active_idx,
741 format!("explicit plan leaves {live_count} live operands"),
742 ));
743 }
744
745 Ok(children)
746}
747
748#[cfg(feature = "autodiff")]
749fn emit_vjp_adjoint(
750 node: usize,
751 cotangent_id: usize,
752 children: &[Option<(usize, usize)>],
753 input_count: usize,
754 active_idx: usize,
755 primal_to_vjp: &[Option<usize>],
756 pairs: &mut Vec<(usize, usize)>,
757) -> std::result::Result<usize, SemanticAdError> {
758 if node < input_count {
759 return if node == active_idx {
760 Ok(cotangent_id)
761 } else {
762 Err(invalid_vjp_plan_error(
763 active_idx,
764 format!("adjoint walk reached inactive leaf {node}"),
765 ))
766 };
767 }
768
769 let (left, right) = children.get(node).and_then(|child| *child).ok_or_else(|| {
770 invalid_vjp_plan_error(active_idx, format!("missing children for node {node}"))
771 })?;
772 let left_has_active = subtree_contains_active(left, children, input_count, active_idx)?;
773 let right_has_active = subtree_contains_active(right, children, input_count, active_idx)?;
774 match (left_has_active, right_has_active) {
775 (true, false) => {
776 let sibling_id = emit_vjp_subtree(
777 right,
778 children,
779 input_count,
780 active_idx,
781 primal_to_vjp,
782 pairs,
783 )?;
784 let next = push_vjp_pair(cotangent_id, sibling_id, input_count, pairs);
785 emit_vjp_adjoint(
786 left,
787 next,
788 children,
789 input_count,
790 active_idx,
791 primal_to_vjp,
792 pairs,
793 )
794 }
795 (false, true) => {
796 let sibling_id = emit_vjp_subtree(
797 left,
798 children,
799 input_count,
800 active_idx,
801 primal_to_vjp,
802 pairs,
803 )?;
804 let next = push_vjp_pair(cotangent_id, sibling_id, input_count, pairs);
805 emit_vjp_adjoint(
806 right,
807 next,
808 children,
809 input_count,
810 active_idx,
811 primal_to_vjp,
812 pairs,
813 )
814 }
815 (true, true) => Err(invalid_vjp_plan_error(
816 active_idx,
817 format!("both children of node {node} contain the active input"),
818 )),
819 (false, false) => Err(invalid_vjp_plan_error(
820 active_idx,
821 format!("neither child of node {node} contains the active input"),
822 )),
823 }
824}
825
826#[cfg(feature = "autodiff")]
827fn emit_vjp_subtree(
828 node: usize,
829 children: &[Option<(usize, usize)>],
830 input_count: usize,
831 active_idx: usize,
832 primal_to_vjp: &[Option<usize>],
833 pairs: &mut Vec<(usize, usize)>,
834) -> std::result::Result<usize, SemanticAdError> {
835 if node < input_count {
836 return primal_to_vjp[node].ok_or_else(|| {
837 invalid_vjp_plan_error(
838 active_idx,
839 format!("sibling subtree unexpectedly reached active leaf {node}"),
840 )
841 });
842 }
843
844 let (left, right) = children.get(node).and_then(|child| *child).ok_or_else(|| {
845 invalid_vjp_plan_error(active_idx, format!("missing children for node {node}"))
846 })?;
847 let left_id = emit_vjp_subtree(
848 left,
849 children,
850 input_count,
851 active_idx,
852 primal_to_vjp,
853 pairs,
854 )?;
855 let right_id = emit_vjp_subtree(
856 right,
857 children,
858 input_count,
859 active_idx,
860 primal_to_vjp,
861 pairs,
862 )?;
863 Ok(push_vjp_pair(left_id, right_id, input_count, pairs))
864}
865
866#[cfg(feature = "autodiff")]
867fn push_vjp_pair(
868 left: usize,
869 right: usize,
870 n_vjp_inputs: usize,
871 pairs: &mut Vec<(usize, usize)>,
872) -> usize {
873 pairs.push((left, right));
874 n_vjp_inputs + pairs.len() - 1
875}
876
877#[cfg(feature = "autodiff")]
878fn subtree_contains_active(
879 node: usize,
880 children: &[Option<(usize, usize)>],
881 input_count: usize,
882 active_idx: usize,
883) -> std::result::Result<bool, SemanticAdError> {
884 if node < input_count {
885 return Ok(node == active_idx);
886 }
887 let (left, right) = children.get(node).and_then(|child| *child).ok_or_else(|| {
888 invalid_vjp_plan_error(active_idx, format!("missing children for node {node}"))
889 })?;
890 Ok(
891 subtree_contains_active(left, children, input_count, active_idx)?
892 || subtree_contains_active(right, children, input_count, active_idx)?,
893 )
894}
895
896#[cfg(feature = "autodiff")]
897fn invalid_vjp_plan_error(active_idx: usize, reason: String) -> SemanticAdError {
898 semantic_einsum_unsupported(format!(
899 "einsum VJP cannot inherit explicit plan for active input {active_idx}: {reason}"
900 ))
901}
902
903#[cfg(feature = "autodiff")]
904fn concrete_sym_shapes(shapes: &[Vec<SymDim>]) -> Option<Vec<Vec<usize>>> {
905 shapes
906 .iter()
907 .map(|shape| shape.iter().map(SymDim::constant_value).collect())
908 .collect()
909}
910
911define_extension_runtime! {
912 runtime = EinsumRuntime,
913 family_id = EINSUM_EXTENSION_FAMILY_ID,
914 op_type = EinsumExtensionOp,
915 execute = execute_einsum_extension,
916 execute_reads = execute_einsum_extension_reads,
917}
918
919fn execute_einsum_extension<B: TensorBackend + 'static>(
920 op: &EinsumExtensionOp,
921 inputs: &[&Tensor],
922 ctx: &mut ExtensionExecutionContext<'_, B>,
923) -> tenferro_tensor::Result<Vec<Tensor>> {
924 if inputs.is_empty() {
925 return Err(tenferro_tensor::Error::invalid_argument(
926 "einsum_extension",
927 "inputs",
928 "einsum requires at least one input tensor",
929 ));
930 }
931
932 let shapes: Vec<Vec<usize>> = inputs
933 .iter()
934 .map(|tensor| tensor.shape().to_vec())
935 .collect();
936 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
937 let subs = Subscripts::from(op.subscripts());
938 let tree = cached_runtime_tree(ctx, op.subscripts(), op.plan_spec(), &shapes, || {
939 resolve_plan_spec(op.plan_spec(), &subs, &shape_refs)
940 })?;
941
942 let output = ctx
943 .backend_mut()
944 .with_backend_session(|exec| crate::eager::eager_einsum_exec(exec, inputs, &tree))?;
945 Ok(vec![output])
946}
947
948pub(crate) fn execute_einsum_extension_reads<B: TensorBackend + 'static>(
949 op: &EinsumExtensionOp,
950 inputs: &[TensorRead<'_>],
951 ctx: &mut ExtensionExecutionContext<'_, B>,
952) -> tenferro_tensor::Result<Vec<Tensor>> {
953 if inputs
954 .iter()
955 .all(|input| matches!(input, TensorRead::Tensor(_)))
956 {
957 let input_refs: Vec<&Tensor> = inputs
958 .iter()
959 .map(|input| match input {
960 TensorRead::Tensor(tensor) => *tensor,
961 TensorRead::View(_) => unreachable!("view input filtered above"),
962 })
963 .collect();
964 return execute_einsum_extension(op, &input_refs, ctx);
965 }
966
967 if inputs.is_empty() {
968 return Err(tenferro_tensor::Error::invalid_argument(
969 "einsum_extension",
970 "inputs",
971 "einsum requires at least one input tensor",
972 ));
973 }
974
975 let shapes: Vec<Vec<usize>> = inputs.iter().map(|input| input.shape().to_vec()).collect();
976 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
977 let subs = Subscripts::from(op.subscripts());
978 let tree = cached_runtime_tree(ctx, op.subscripts(), op.plan_spec(), &shapes, || {
979 resolve_plan_spec(op.plan_spec(), &subs, &shape_refs)
980 })?;
981 let output = ctx
982 .backend_mut()
983 .with_backend_session(|exec| crate::eager::eager_einsum_exec_read(exec, inputs, &tree))?;
984 Ok(vec![output])
985}
986
987#[cfg(feature = "autodiff")]
988pub(crate) fn execute_einsum_extension_session_reads(
989 op: &EinsumExtensionOp,
990 inputs: &[TensorRead<'_>],
991 ctx: &mut ExtensionExecutionContext<'_, dyn BackendSession + '_>,
992) -> tenferro_tensor::Result<Vec<Tensor>> {
993 if inputs.is_empty() {
994 return Err(tenferro_tensor::Error::invalid_argument(
995 "einsum_extension",
996 "inputs",
997 "einsum requires at least one input tensor",
998 ));
999 }
1000
1001 let shapes: Vec<Vec<usize>> = inputs.iter().map(|input| input.shape().to_vec()).collect();
1002 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
1003 let subs = Subscripts::from(op.subscripts());
1004 let tree = cached_runtime_tree(ctx, op.subscripts(), op.plan_spec(), &shapes, || {
1005 resolve_plan_spec(op.plan_spec(), &subs, &shape_refs)
1006 })?;
1007 let output = crate::eager::eager_einsum_exec_read(ctx.backend_mut(), inputs, &tree)?;
1008 Ok(vec![output])
1009}
1010
1011#[derive(Clone)]
1012struct RuntimeTreeCacheKeyData {
1013 subscripts: EinsumSubscripts,
1014 shapes: Vec<Vec<usize>>,
1015 plan_spec: EinsumPlanSpec,
1016}
1017
1018impl RuntimeTreeCacheKeyData {
1019 fn new(
1020 subscripts: &EinsumSubscripts,
1021 shapes: &[Vec<usize>],
1022 plan_spec: &EinsumPlanSpec,
1023 ) -> Self {
1024 Self {
1025 subscripts: subscripts.clone(),
1026 shapes: shapes.to_vec(),
1027 plan_spec: plan_spec.clone(),
1028 }
1029 }
1030
1031 fn matches_runtime_tree(
1032 &self,
1033 subscripts: &EinsumSubscripts,
1034 shapes: &[Vec<usize>],
1035 plan_spec: &EinsumPlanSpec,
1036 ) -> bool {
1037 self.subscripts == *subscripts
1038 && self.shapes.as_slice() == shapes
1039 && plan_specs_equal(&self.plan_spec, plan_spec)
1040 }
1041
1042 fn retained_bytes(&self) -> usize {
1043 saturating_sum([
1044 einsum_subscripts_retained_bytes(&self.subscripts),
1045 saturating_sum(self.shapes.iter().map(vec_retained_bytes)),
1046 plan_spec_retained_bytes(&self.plan_spec),
1047 ])
1048 }
1049}
1050
1051struct CachedRuntimeTree {
1052 key_data: RuntimeTreeCacheKeyData,
1053 tree: Arc<ContractionTree>,
1054}
1055
1056fn cached_runtime_tree<B: BackendSession + ?Sized>(
1057 ctx: &mut ExtensionExecutionContext<'_, B>,
1058 subscripts: &EinsumSubscripts,
1059 plan_spec: &EinsumPlanSpec,
1060 shapes: &[Vec<usize>],
1061 build: impl FnOnce() -> EinsumResult<ContractionTree>,
1062) -> tenferro_tensor::Result<Arc<ContractionTree>> {
1063 let plan_hash = plan_spec_hash(plan_spec);
1064 let key = ExtensionCacheKey::new(
1065 EINSUM_EXTENSION_FAMILY_ID,
1066 EINSUM_RUNTIME_PLANS_CACHE,
1067 runtime_tree_cache_discriminator(subscripts, shapes, plan_hash),
1068 );
1069 if let Some(cached) = ctx.caches_mut().get::<CachedRuntimeTree>(&key) {
1070 let key_data = &cached.key_data;
1071 if key_data.matches_runtime_tree(subscripts, shapes, plan_spec) {
1072 return Ok(Arc::clone(&cached.tree));
1073 }
1074 }
1075
1076 let tree = Arc::new(build().map_err(einsum_runtime_error)?);
1077 let key_data = RuntimeTreeCacheKeyData::new(subscripts, shapes, plan_spec);
1078 let retained_bytes = saturating_sum([
1079 key_data.retained_bytes(),
1080 tree.retained_bytes_for_cache_stats(),
1081 ]);
1082 ctx.caches_mut().put(
1083 key,
1084 CachedRuntimeTree {
1085 key_data,
1086 tree: Arc::clone(&tree),
1087 },
1088 retained_bytes,
1089 );
1090 Ok(tree)
1091}
1092
1093fn einsum_runtime_error(error: EinsumError) -> tenferro_tensor::Error {
1094 error.into_tensor_error("einsum_extension")
1095}
1096
1097fn runtime_tree_cache_discriminator(
1098 subscripts: &EinsumSubscripts,
1099 shapes: &[Vec<usize>],
1100 plan_hash: u64,
1101) -> u64 {
1102 let mut hasher = DefaultHasher::new();
1103 subscripts.hash(&mut hasher);
1104 shapes.hash(&mut hasher);
1105 plan_hash.hash(&mut hasher);
1106 hasher.finish()
1107}
1108
1109fn plan_spec_hash(plan_spec: &EinsumPlanSpec) -> u64 {
1110 let mut hasher = DefaultHasher::new();
1111 hash_einsum_plan_spec(plan_spec, &mut hasher);
1112 hasher.finish()
1113}
1114
1115fn plan_spec_retained_bytes(plan_spec: &EinsumPlanSpec) -> usize {
1116 match plan_spec {
1117 EinsumPlanSpec::Auto(options) => saturating_sum([
1118 std::mem::size_of::<EinsumPlanSpec>(),
1119 vec_retained_bytes(&options.betas),
1120 ]),
1121 EinsumPlanSpec::LeftToRight => std::mem::size_of::<EinsumPlanSpec>(),
1122 EinsumPlanSpec::Path(path) | EinsumPlanSpec::FixedPairs(path) => saturating_sum([
1123 std::mem::size_of::<EinsumPlanSpec>(),
1124 vec_retained_bytes(path),
1125 ]),
1126 }
1127}
1128
1129fn promote_dtypes(dtypes: impl IntoIterator<Item = DType>) -> DType {
1130 dtypes
1131 .into_iter()
1132 .reduce(tenferro_tensor::validate::promote_dtype)
1133 .unwrap_or(DType::F64)
1134}
1135
1136#[cfg(test)]
1137mod tests;