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