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::compile::compile;
10use computegraph::graph::GraphBuilder;
11use computegraph::materialize::materialize_merge;
12use computegraph::resolve::resolve;
13#[cfg(feature = "autodiff")]
14use computegraph::types::{LocalValueId, OperationRole};
15use computegraph::types::{ValueKey, ValueRef};
16use smallvec::SmallVec;
17use tenferro_extension_macros::define_extension_runtime;
18#[cfg(feature = "autodiff")]
19use tenferro_ops::ad::context::ShapeGuardContext;
20#[cfg(feature = "autodiff")]
21use tenferro_ops::ad::transpose_input::TransposeInputRef;
22#[cfg(feature = "autodiff")]
23use tenferro_ops::ad::PrimitiveRuleBuilder;
24#[cfg(feature = "autodiff")]
25use tenferro_ops::dim_expr::DimExpr;
26#[cfg(feature = "autodiff")]
27use tenferro_ops::ext_op::{ExtensionLinearTransposeRule, ExtensionLinearizeRule};
28use tenferro_ops::ext_op::{
29 ExtensionLoweringError, ExtensionLoweringResult, ExtensionOp, HostReference,
30};
31use tenferro_ops::input_key::TensorInputKey;
32use tenferro_ops::std_tensor_op::StdTensorOp;
33use tenferro_ops::sym_dim::SymDim;
34#[cfg(feature = "autodiff")]
35use tenferro_ops::{ExtensionRegistryError, ExtensionRuleSet};
36use tenferro_runtime::extension::{
37 ExecInstruction, ExecOp, ExecProgram, ExtensionCacheKey, ExtensionExecutionContext,
38};
39use tenferro_tensor::{
40 DType, Error as TensorError, RuntimeCacheControl, Tensor, TensorBackend, TensorRead,
41};
42#[cfg(feature = "autodiff")]
43use tidu::{ADRuleError, ADRuleKind, ADRuleResult, PrimitiveTransposeInput};
44
45use crate::builder::build_einsum_graph;
46use crate::cache::{
47 einsum_subscripts_retained_bytes, saturating_sum, vec_of_vec_retained_bytes,
48 vec_retained_bytes, EINSUM_EXTENSION_FAMILY_ID, EINSUM_RUNTIME_EXEC_PROGRAMS_CACHE,
49 EINSUM_RUNTIME_PLANS_CACHE,
50};
51#[cfg(test)]
52use crate::optimize::default_auto_options;
53#[cfg(feature = "autodiff")]
54use crate::optimize::jax_path_to_v1_pairs;
55use crate::optimize::{hash_einsum_plan_spec, plan_specs_equal, resolve_plan_spec, EinsumPlanSpec};
56#[cfg(feature = "autodiff")]
57use crate::util::map_label_occurrences;
58use crate::{
59 ContractionTree, EinsumSubscripts, Error as EinsumError, Result as EinsumResult, Subscripts,
60};
61
62type InputIndexVec = SmallVec<[usize; 8]>;
63
64#[derive(Clone)]
71pub(crate) struct EinsumExtensionOp {
72 subscripts: EinsumSubscripts,
73 plan_spec: EinsumPlanSpec,
74 static_tree: Option<Arc<ContractionTree>>,
78 output_shape_hint: Option<Vec<SymDim>>,
79}
80
81impl std::fmt::Debug for EinsumExtensionOp {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("EinsumExtensionOp")
84 .field("subscripts", &self.subscripts)
85 .field("plan_spec", &self.plan_spec)
86 .field("has_static_tree", &self.static_tree.is_some())
87 .field("output_shape_hint", &self.output_shape_hint)
88 .finish()
89 }
90}
91
92impl EinsumExtensionOp {
93 #[must_use]
95 #[cfg(test)]
96 pub(crate) fn new(subscripts: EinsumSubscripts) -> Self {
97 Self::with_plan_spec(subscripts, EinsumPlanSpec::Auto(default_auto_options()))
98 }
99
100 #[must_use]
101 pub(crate) fn with_plan_spec(subscripts: EinsumSubscripts, plan_spec: EinsumPlanSpec) -> Self {
102 Self {
103 subscripts,
104 plan_spec,
105 static_tree: None,
106 output_shape_hint: None,
107 }
108 }
109
110 #[must_use]
112 #[cfg(test)]
113 pub(crate) fn with_static_tree(
114 subscripts: EinsumSubscripts,
115 tree: Arc<ContractionTree>,
116 ) -> Self {
117 Self::new(subscripts).with_static_tree_hint(tree)
118 }
119
120 #[must_use]
122 pub(crate) fn with_output_shape_hint(
123 subscripts: EinsumSubscripts,
124 output_shape_hint: Vec<SymDim>,
125 plan_spec: EinsumPlanSpec,
126 ) -> Self {
127 let mut op = Self::with_plan_spec(subscripts, plan_spec);
128 op.output_shape_hint = Some(output_shape_hint);
129 op
130 }
131
132 #[must_use]
134 #[cfg(any(test, feature = "autodiff"))]
135 pub(crate) fn with_static_tree_hint(mut self, tree: Arc<ContractionTree>) -> Self {
136 self.static_tree = Some(tree);
137 self
138 }
139
140 #[must_use]
142 pub(crate) fn subscripts(&self) -> &EinsumSubscripts {
143 &self.subscripts
144 }
145
146 #[must_use]
148 pub(crate) fn plan_spec(&self) -> &EinsumPlanSpec {
149 &self.plan_spec
150 }
151
152 #[must_use]
154 pub(crate) fn static_tree(&self) -> Option<&Arc<ContractionTree>> {
155 self.static_tree.as_ref()
156 }
157}
158
159impl ExtensionOp for EinsumExtensionOp {
160 fn family_id(&self) -> &'static str {
161 EINSUM_EXTENSION_FAMILY_ID
162 }
163
164 fn payload_hash(&self, hasher: &mut dyn Hasher) {
165 hasher.write_usize(self.subscripts.inputs.len());
166 for input in &self.subscripts.inputs {
167 hasher.write_usize(input.len());
168 for label in input {
169 hasher.write_u32(*label);
170 }
171 }
172 hasher.write_usize(self.subscripts.output.len());
173 for label in &self.subscripts.output {
174 hasher.write_u32(*label);
175 }
176 hash_einsum_plan_spec(self.plan_spec(), hasher);
177 if let Some(shape) = &self.output_shape_hint {
178 hasher.write_usize(shape.len());
179 for dim in shape {
180 match dim.constant_value() {
181 Some(value) => {
182 hasher.write_u8(1);
183 hasher.write_usize(value);
184 }
185 None => hasher.write_u8(0),
186 }
187 }
188 } else {
189 hasher.write_usize(usize::MAX);
190 }
191 }
192
193 fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
194 other.as_any().downcast_ref::<Self>().is_some_and(|that| {
195 self.subscripts == that.subscripts
196 && plan_specs_equal(self.plan_spec(), that.plan_spec())
197 && self.output_shape_hint == that.output_shape_hint
198 })
199 }
200
201 fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
202 Arc::new(self.clone())
203 }
204
205 fn as_any(&self) -> &dyn Any {
206 self
207 }
208
209 fn input_count(&self) -> usize {
210 self.subscripts.inputs.len()
211 }
212
213 fn output_count(&self) -> usize {
214 1
215 }
216
217 fn infer_output_meta(
218 &self,
219 input_dtypes: &[DType],
220 input_shapes: &[&[SymDim]],
221 ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
222 if input_shapes.len() != self.subscripts.inputs.len()
223 || input_dtypes.len() != input_shapes.len()
224 {
225 return Err(TensorError::InvalidConfig {
226 op: "einsum",
227 message: format!(
228 "expected {} input metadata entries, got dtypes={} shapes={}",
229 self.subscripts.inputs.len(),
230 input_dtypes.len(),
231 input_shapes.len()
232 ),
233 });
234 }
235
236 let mut label_dims: HashMap<u32, SymDim> = HashMap::new();
237 for (labels, shape) in self.subscripts.inputs.iter().zip(input_shapes.iter()) {
238 if labels.len() != shape.len() {
239 return Err(TensorError::InvalidConfig {
240 op: "einsum",
241 message: format!(
242 "subscript rank {} does not match input rank {}",
243 labels.len(),
244 shape.len()
245 ),
246 });
247 }
248 for (&label, dim) in labels.iter().zip(shape.iter()) {
249 if let Some(existing) = label_dims.get(&label) {
250 if let (Some(lhs), Some(rhs)) =
251 (existing.constant_value(), dim.constant_value())
252 {
253 if lhs != rhs {
254 return Err(TensorError::ShapeMismatch {
255 op: "einsum",
256 lhs: vec![lhs],
257 rhs: vec![rhs],
258 });
259 }
260 }
261 } else {
262 label_dims.insert(label, dim.clone());
263 }
264 }
265 }
266
267 let output_shape = match &self.output_shape_hint {
268 Some(shape) if shape.iter().all(|dim| dim.constant_value().is_some()) => shape.clone(),
269 _ => self
270 .subscripts
271 .output
272 .iter()
273 .map(|label| label_dims.get(label).cloned())
274 .collect::<Option<Vec<_>>>()
275 .ok_or_else(|| TensorError::InvalidConfig {
276 op: "einsum",
277 message: "output labels must be present in input metadata".into(),
278 })?,
279 };
280 if output_shape.len() != self.subscripts.output.len() {
281 return Err(TensorError::InvalidConfig {
282 op: "einsum",
283 message: format!(
284 "output rank {} does not match subscript rank {}",
285 output_shape.len(),
286 self.subscripts.output.len()
287 ),
288 });
289 }
290 Ok(vec![(
291 promote_dtypes(input_dtypes.iter().copied()),
292 output_shape,
293 )])
294 }
295
296 fn host_reference(&self) -> Option<&dyn HostReference> {
297 Some(self)
298 }
299
300 fn lower_to_standard_ops(
301 &self,
302 builder: &mut GraphBuilder<StdTensorOp>,
303 inputs: &[ValueRef<StdTensorOp>],
304 input_dtypes: &[DType],
305 input_shapes: &[&[SymDim]],
306 ) -> ExtensionLoweringResult {
307 if inputs.len() != self.input_count()
308 || input_dtypes.len() != self.input_count()
309 || input_shapes.len() != self.input_count()
310 {
311 return Err(ExtensionLoweringError::new(format!(
312 "einsum extension expects {} inputs, got values={}, dtypes={}, shapes={}",
313 self.input_count(),
314 inputs.len(),
315 input_dtypes.len(),
316 input_shapes.len()
317 )));
318 }
319
320 let Some(shapes) = concrete_sym_shape_slices(input_shapes) else {
321 return Ok(None);
322 };
323 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
324 let subs = Subscripts::from(&self.subscripts);
325 let tree = resolve_plan_spec(self.plan_spec(), &subs, &shape_refs)
326 .map_err(|err| ExtensionLoweringError::new(err.to_string()))?;
327 let output = build_einsum_graph(builder, &tree, inputs, &shapes)
328 .map_err(|err| ExtensionLoweringError::new(err.to_string()))?;
329 Ok(Some(vec![output]))
330 }
331}
332
333impl HostReference for EinsumExtensionOp {
334 fn execute(&self, inputs: &[&Tensor]) -> tenferro_tensor::Result<Vec<Tensor>> {
335 let mut backend = tenferro_cpu::CpuBackend::new();
336 let subscripts = Subscripts::from(&self.subscripts);
337 crate::eager::eager_einsum_subscripts(&mut backend, inputs, &subscripts)
338 .map(|output| vec![output])
339 }
340}
341
342fn concrete_sym_shape_slices(input_shapes: &[&[SymDim]]) -> Option<Vec<Vec<usize>>> {
343 input_shapes
344 .iter()
345 .map(|shape| {
346 shape
347 .iter()
348 .map(SymDim::constant_value)
349 .collect::<Option<Vec<_>>>()
350 })
351 .collect()
352}
353
354#[cfg(feature = "autodiff")]
356pub fn ad_rules() -> Result<ExtensionRuleSet, ExtensionRegistryError> {
357 ExtensionRuleSet::new()
358 .with_linearize(Arc::new(EinsumAdRule))?
359 .with_linear_transpose(Arc::new(EinsumAdRule))
360}
361
362#[derive(Debug)]
363#[cfg(feature = "autodiff")]
364struct EinsumAdRule;
365
366#[cfg(feature = "autodiff")]
367impl ExtensionLinearizeRule for EinsumAdRule {
368 fn family_id(&self) -> &'static str {
369 EINSUM_EXTENSION_FAMILY_ID
370 }
371
372 fn linearize(
373 &self,
374 op: &dyn ExtensionOp,
375 builder: &mut dyn PrimitiveRuleBuilder,
376 primal_in: &[ValueKey<StdTensorOp>],
377 _primal_out: &[ValueKey<StdTensorOp>],
378 tangent_in: &[Option<LocalValueId>],
379 _ctx: &mut ShapeGuardContext,
380 ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
381 let op = downcast_ad_op(op, ADRuleKind::Jvp)?;
382 let mut terms = Vec::new();
383
384 for (active_idx, tangent) in tangent_in.iter().enumerate() {
385 let Some(dt) = tangent else {
386 continue;
387 };
388
389 let mut inputs = Vec::with_capacity(primal_in.len());
390 for (input_idx, key) in primal_in.iter().enumerate() {
391 if input_idx == active_idx {
392 inputs.push(ValueRef::Local(*dt));
393 } else {
394 inputs.push(ValueRef::External(key.clone()));
395 }
396 }
397
398 let out = builder.add_operation(
399 StdTensorOp::Extension(Arc::new(op.clone())),
400 inputs,
401 OperationRole::Linearized {
402 active_mask: (0..primal_in.len()).map(|idx| idx == active_idx).collect(),
403 },
404 );
405 terms.push(out[0]);
406 }
407
408 Ok(vec![sum_terms(builder, terms)])
409 }
410}
411
412#[cfg(feature = "autodiff")]
413impl ExtensionLinearTransposeRule for EinsumAdRule {
414 fn family_id(&self) -> &'static str {
415 EINSUM_EXTENSION_FAMILY_ID
416 }
417
418 fn linear_transpose(
419 &self,
420 op: &dyn ExtensionOp,
421 builder: &mut dyn PrimitiveRuleBuilder,
422 cotangent_out: &[Option<LocalValueId>],
423 inputs: &[PrimitiveTransposeInput<StdTensorOp>],
424 active_mask: &[bool],
425 ctx: &mut ShapeGuardContext,
426 ) -> ADRuleResult<Vec<Option<LocalValueId>>> {
427 let op = downcast_ad_op(op, ADRuleKind::Transpose)?;
428 let inputs: Vec<_> = inputs.iter().map(TransposeInputRef::new).collect();
429 let input_labels = &op.subscripts.inputs;
430 let output_labels = &op.subscripts.output;
431 let input_count = input_labels.len();
432
433 let Some(ct) = cotangent_out.first().copied().flatten() else {
434 return Ok(vec![None; input_count]);
435 };
436 let primal_input_shapes: Vec<Vec<SymDim>> = inputs
437 .iter()
438 .map(|input| {
439 let metadata = input.metadata_value();
440 ctx.shape_of(&metadata).map(|shape| shape.to_vec())
441 })
442 .collect::<Result<_, _>>()?;
443 let cotangent_shape = op.output_shape_hint.clone().ok_or_else(|| {
444 ADRuleError::unsupported(
445 "einsum VJP requires an output shape hint for cotangent planning",
446 ADRuleKind::Transpose,
447 )
448 })?;
449
450 let mut result = Vec::with_capacity(input_count);
451 for active_idx in 0..input_count {
452 if !active_mask.get(active_idx).copied().unwrap_or(false) {
453 result.push(None);
454 continue;
455 }
456
457 let mut available_labels: HashSet<u32> = output_labels.iter().copied().collect();
458 for (input_idx, labels) in input_labels.iter().enumerate() {
459 if input_idx != active_idx {
460 available_labels.extend(labels.iter().copied());
461 }
462 }
463 let vjp_output_labels: Vec<u32> = input_labels[active_idx]
464 .iter()
465 .copied()
466 .filter(|label| available_labels.contains(label))
467 .collect();
468 let mut vjp_input_labels = Vec::with_capacity(input_count);
469 let mut vjp_inputs = Vec::with_capacity(input_count);
470 let mut vjp_input_shapes = Vec::with_capacity(input_count);
471 vjp_input_labels.push(output_labels.clone());
472 vjp_inputs.push(ValueRef::Local(ct));
473 vjp_input_shapes.push(cotangent_shape.clone());
474
475 for input_idx in 0..input_count {
476 if input_idx == active_idx {
477 continue;
478 }
479 vjp_input_labels.push(input_labels[input_idx].clone());
480 vjp_input_shapes.push(primal_input_shapes[input_idx].clone());
481 let fixed_input = inputs[input_idx].fixed_value("einsum VJP", input_idx)?;
482 vjp_inputs.push(conjugate_primal_if_complex(builder, fixed_input, ctx)?);
483 }
484
485 let output_shape_hint = primal_input_shapes[active_idx].clone();
486 let vjp_op = vjp_einsum_op_with_inherited_plan(
487 op,
488 active_idx,
489 EinsumSubscripts {
490 inputs: vjp_input_labels,
491 output: vjp_output_labels.clone(),
492 },
493 output_shape_hint.clone(),
494 &vjp_input_shapes,
495 )?;
496 let out = builder.add_operation(
497 StdTensorOp::Extension(Arc::new(vjp_op)),
498 vjp_inputs,
499 OperationRole::Linearized {
500 active_mask: std::iter::once(true)
501 .chain(std::iter::repeat_n(false, input_count.saturating_sub(1)))
502 .collect(),
503 },
504 );
505 let mut cotangent = out[0];
506 if vjp_output_labels != input_labels[active_idx] {
507 let (shape, shape_sources) =
508 inputs[active_idx].shape_operand(output_shape_hint.len(), 1, ctx)?;
509 let remapped = broadcast_einsum_vjp_to_input_shape(
510 builder,
511 cotangent,
512 &vjp_output_labels,
513 &input_labels[active_idx],
514 shape,
515 shape_sources,
516 )?;
517 cotangent = remapped;
518 }
519 result.push(Some(cotangent));
520 }
521
522 Ok(result)
523 }
524}
525
526#[cfg(feature = "autodiff")]
527fn vjp_einsum_op_with_inherited_plan(
528 primal_op: &EinsumExtensionOp,
529 active_idx: usize,
530 subscripts: EinsumSubscripts,
531 output_shape_hint: Vec<SymDim>,
532 input_shapes: &[Vec<SymDim>],
533) -> ADRuleResult<EinsumExtensionOp> {
534 let plan_spec =
535 vjp_plan_spec_for_active(primal_op.plan_spec(), primal_op.input_count(), active_idx)?;
536 let mut op = EinsumExtensionOp::with_output_shape_hint(
537 subscripts.clone(),
538 output_shape_hint,
539 plan_spec.clone(),
540 );
541 if let Some(concrete_shapes) = concrete_sym_shapes(input_shapes) {
542 let shape_refs: Vec<&[usize]> = concrete_shapes.iter().map(Vec::as_slice).collect();
543 let raw_subscripts = Subscripts::from(&subscripts);
544 let tree =
545 resolve_plan_spec(&plan_spec, &raw_subscripts, &shape_refs).map_err(|err| {
546 ADRuleError::unsupported(
547 format!(
548 "failed to resolve inherited einsum VJP plan for active input {active_idx}: {err}"
549 ),
550 ADRuleKind::Transpose,
551 )
552 })?;
553 op = op.with_static_tree_hint(Arc::new(tree));
554 }
555 Ok(op)
556}
557
558#[cfg(feature = "autodiff")]
559fn vjp_plan_spec_for_active(
560 primal_plan: &EinsumPlanSpec,
561 input_count: usize,
562 active_idx: usize,
563) -> ADRuleResult<EinsumPlanSpec> {
564 if active_idx >= input_count {
565 return Err(ADRuleError::unsupported(
566 format!("einsum VJP active input {active_idx} is outside {input_count} inputs"),
567 ADRuleKind::Transpose,
568 ));
569 }
570
571 match primal_plan {
572 EinsumPlanSpec::Auto(options) => Ok(EinsumPlanSpec::Auto(options.clone())),
573 EinsumPlanSpec::LeftToRight => Ok(EinsumPlanSpec::LeftToRight),
574 EinsumPlanSpec::Path(path) => {
575 let pairs = jax_path_to_v1_pairs(path, input_count).map_err(|err| {
576 ADRuleError::unsupported(
577 format!(
578 "failed to inherit einsum Path plan for VJP active input {active_idx}: {err}"
579 ),
580 ADRuleKind::Transpose,
581 )
582 })?;
583 derive_vjp_fixed_pairs(&pairs, input_count, active_idx).map(EinsumPlanSpec::FixedPairs)
584 }
585 EinsumPlanSpec::FixedPairs(pairs) => {
586 derive_vjp_fixed_pairs(pairs, input_count, active_idx).map(EinsumPlanSpec::FixedPairs)
587 }
588 }
589}
590
591#[cfg(feature = "autodiff")]
592fn derive_vjp_fixed_pairs(
593 primal_pairs: &[(usize, usize)],
594 input_count: usize,
595 active_idx: usize,
596) -> ADRuleResult<Vec<(usize, usize)>> {
597 if input_count == 0 {
598 return Err(ADRuleError::unsupported(
599 "einsum VJP cannot derive a plan for zero primal inputs",
600 ADRuleKind::Transpose,
601 ));
602 }
603 if active_idx >= input_count {
604 return Err(ADRuleError::unsupported(
605 format!("einsum VJP active input {active_idx} is outside {input_count} inputs"),
606 ADRuleKind::Transpose,
607 ));
608 }
609 let required_steps = input_count.saturating_sub(1);
610 if primal_pairs.len() != required_steps {
611 return Err(ADRuleError::unsupported(
612 format!(
613 "einsum VJP cannot inherit explicit plan for active input {active_idx}: \
614 expected {required_steps} primal steps for {input_count} inputs, got {}",
615 primal_pairs.len()
616 ),
617 ADRuleKind::Transpose,
618 ));
619 }
620 if input_count == 1 {
621 return Ok(Vec::new());
622 }
623
624 let children = fixed_pair_children(primal_pairs, input_count, active_idx)?;
625 let mut primal_to_vjp = vec![None; input_count];
626 let mut next_vjp_input = 1;
627 for (input_idx, slot) in primal_to_vjp.iter_mut().enumerate() {
628 if input_idx != active_idx {
629 *slot = Some(next_vjp_input);
630 next_vjp_input += 1;
631 }
632 }
633
634 let root = input_count + primal_pairs.len() - 1;
635 let mut pairs = Vec::with_capacity(required_steps);
636 let final_id = emit_vjp_adjoint(
637 root,
638 0,
639 &children,
640 input_count,
641 active_idx,
642 &primal_to_vjp,
643 &mut pairs,
644 )?;
645 let expected_final = input_count + pairs.len() - 1;
646 if final_id != expected_final || pairs.len() != required_steps {
647 return Err(ADRuleError::unsupported(
648 format!(
649 "einsum VJP plan derivation for active input {active_idx} produced an invalid \
650 tree: final id {final_id}, expected {expected_final}, steps {}",
651 pairs.len()
652 ),
653 ADRuleKind::Transpose,
654 ));
655 }
656 Ok(pairs)
657}
658
659#[cfg(feature = "autodiff")]
660fn fixed_pair_children(
661 pairs: &[(usize, usize)],
662 input_count: usize,
663 active_idx: usize,
664) -> ADRuleResult<Vec<Option<(usize, usize)>>> {
665 let mut live = vec![false; input_count + pairs.len()];
666 for slot in live.iter_mut().take(input_count) {
667 *slot = true;
668 }
669 let mut children = vec![None; input_count + pairs.len()];
670
671 for (step_idx, &(left, right)) in pairs.iter().enumerate() {
672 let next_idx = input_count + step_idx;
673 if left == right {
674 return Err(invalid_vjp_plan_error(
675 active_idx,
676 format!("pair ({left}, {right}) references the same operand"),
677 ));
678 }
679 if left >= next_idx || right >= next_idx {
680 return Err(invalid_vjp_plan_error(
681 active_idx,
682 format!("pair ({left}, {right}) references a non-existent operand"),
683 ));
684 }
685 if !live[left] || !live[right] {
686 return Err(invalid_vjp_plan_error(
687 active_idx,
688 format!("pair ({left}, {right}) references an operand that is no longer live"),
689 ));
690 }
691
692 live[left] = false;
693 live[right] = false;
694 live[next_idx] = true;
695 children[next_idx] = Some((left, right));
696 }
697
698 let live_count = live.iter().filter(|&&is_live| is_live).count();
699 if live_count != 1 {
700 return Err(invalid_vjp_plan_error(
701 active_idx,
702 format!("explicit plan leaves {live_count} live operands"),
703 ));
704 }
705
706 Ok(children)
707}
708
709#[cfg(feature = "autodiff")]
710fn emit_vjp_adjoint(
711 node: usize,
712 cotangent_id: usize,
713 children: &[Option<(usize, usize)>],
714 input_count: usize,
715 active_idx: usize,
716 primal_to_vjp: &[Option<usize>],
717 pairs: &mut Vec<(usize, usize)>,
718) -> ADRuleResult<usize> {
719 if node < input_count {
720 return if node == active_idx {
721 Ok(cotangent_id)
722 } else {
723 Err(invalid_vjp_plan_error(
724 active_idx,
725 format!("adjoint walk reached inactive leaf {node}"),
726 ))
727 };
728 }
729
730 let (left, right) = children.get(node).and_then(|child| *child).ok_or_else(|| {
731 invalid_vjp_plan_error(active_idx, format!("missing children for node {node}"))
732 })?;
733 let left_has_active = subtree_contains_active(left, children, input_count, active_idx)?;
734 let right_has_active = subtree_contains_active(right, children, input_count, active_idx)?;
735 match (left_has_active, right_has_active) {
736 (true, false) => {
737 let sibling_id = emit_vjp_subtree(
738 right,
739 children,
740 input_count,
741 active_idx,
742 primal_to_vjp,
743 pairs,
744 )?;
745 let next = push_vjp_pair(cotangent_id, sibling_id, input_count, pairs);
746 emit_vjp_adjoint(
747 left,
748 next,
749 children,
750 input_count,
751 active_idx,
752 primal_to_vjp,
753 pairs,
754 )
755 }
756 (false, true) => {
757 let sibling_id = emit_vjp_subtree(
758 left,
759 children,
760 input_count,
761 active_idx,
762 primal_to_vjp,
763 pairs,
764 )?;
765 let next = push_vjp_pair(cotangent_id, sibling_id, input_count, pairs);
766 emit_vjp_adjoint(
767 right,
768 next,
769 children,
770 input_count,
771 active_idx,
772 primal_to_vjp,
773 pairs,
774 )
775 }
776 (true, true) => Err(invalid_vjp_plan_error(
777 active_idx,
778 format!("both children of node {node} contain the active input"),
779 )),
780 (false, false) => Err(invalid_vjp_plan_error(
781 active_idx,
782 format!("neither child of node {node} contains the active input"),
783 )),
784 }
785}
786
787#[cfg(feature = "autodiff")]
788fn emit_vjp_subtree(
789 node: usize,
790 children: &[Option<(usize, usize)>],
791 input_count: usize,
792 active_idx: usize,
793 primal_to_vjp: &[Option<usize>],
794 pairs: &mut Vec<(usize, usize)>,
795) -> ADRuleResult<usize> {
796 if node < input_count {
797 return primal_to_vjp[node].ok_or_else(|| {
798 invalid_vjp_plan_error(
799 active_idx,
800 format!("sibling subtree unexpectedly reached active leaf {node}"),
801 )
802 });
803 }
804
805 let (left, right) = children.get(node).and_then(|child| *child).ok_or_else(|| {
806 invalid_vjp_plan_error(active_idx, format!("missing children for node {node}"))
807 })?;
808 let left_id = emit_vjp_subtree(
809 left,
810 children,
811 input_count,
812 active_idx,
813 primal_to_vjp,
814 pairs,
815 )?;
816 let right_id = emit_vjp_subtree(
817 right,
818 children,
819 input_count,
820 active_idx,
821 primal_to_vjp,
822 pairs,
823 )?;
824 Ok(push_vjp_pair(left_id, right_id, input_count, pairs))
825}
826
827#[cfg(feature = "autodiff")]
828fn push_vjp_pair(
829 left: usize,
830 right: usize,
831 n_vjp_inputs: usize,
832 pairs: &mut Vec<(usize, usize)>,
833) -> usize {
834 pairs.push((left, right));
835 n_vjp_inputs + pairs.len() - 1
836}
837
838#[cfg(feature = "autodiff")]
839fn subtree_contains_active(
840 node: usize,
841 children: &[Option<(usize, usize)>],
842 input_count: usize,
843 active_idx: usize,
844) -> ADRuleResult<bool> {
845 if node < input_count {
846 return Ok(node == active_idx);
847 }
848 let (left, right) = children.get(node).and_then(|child| *child).ok_or_else(|| {
849 invalid_vjp_plan_error(active_idx, format!("missing children for node {node}"))
850 })?;
851 Ok(
852 subtree_contains_active(left, children, input_count, active_idx)?
853 || subtree_contains_active(right, children, input_count, active_idx)?,
854 )
855}
856
857#[cfg(feature = "autodiff")]
858fn invalid_vjp_plan_error(active_idx: usize, reason: String) -> ADRuleError {
859 ADRuleError::unsupported(
860 format!("einsum VJP cannot inherit explicit plan for active input {active_idx}: {reason}"),
861 ADRuleKind::Transpose,
862 )
863}
864
865#[cfg(feature = "autodiff")]
866fn concrete_sym_shapes(shapes: &[Vec<SymDim>]) -> Option<Vec<Vec<usize>>> {
867 shapes
868 .iter()
869 .map(|shape| shape.iter().map(SymDim::constant_value).collect())
870 .collect()
871}
872
873#[cfg(feature = "autodiff")]
874fn broadcast_einsum_vjp_to_input_shape(
875 builder: &mut dyn PrimitiveRuleBuilder,
876 cotangent: LocalValueId,
877 cotangent_labels: &[u32],
878 input_labels: &[u32],
879 shape: Vec<DimExpr>,
880 shape_sources: Vec<ValueRef<StdTensorOp>>,
881) -> ADRuleResult<LocalValueId> {
882 let dims = map_label_occurrences(cotangent_labels, input_labels).ok_or_else(|| {
883 ADRuleError::unsupported(
884 format!(
885 "einsum VJP broadcast remap failed for cotangent labels {cotangent_labels:?} \
886 into active input labels {input_labels:?}"
887 ),
888 ADRuleKind::Transpose,
889 )
890 })?;
891 let source_count = shape_sources.len();
892 let mut inputs = vec![ValueRef::Local(cotangent)];
893 inputs.extend(shape_sources);
894 let active_mask = std::iter::once(true)
895 .chain(std::iter::repeat_n(false, source_count))
896 .collect();
897 let broadcast = builder.add_operation(
898 StdTensorOp::BroadcastInDim { shape, dims },
899 inputs,
900 OperationRole::Linearized { active_mask },
901 )[0];
902 Ok(project_repeated_labels_to_diagonal(
903 builder,
904 broadcast,
905 input_labels,
906 ))
907}
908
909#[cfg(feature = "autodiff")]
910fn project_repeated_labels_to_diagonal(
911 builder: &mut dyn PrimitiveRuleBuilder,
912 cotangent: LocalValueId,
913 labels: &[u32],
914) -> LocalValueId {
915 let mut result = cotangent;
916 let mut first_axis_by_label = HashMap::new();
917 for (axis_b, label) in labels.iter().copied().enumerate() {
918 let Some(&axis_a) = first_axis_by_label.get(&label) else {
919 first_axis_by_label.insert(label, axis_b);
920 continue;
921 };
922 let extracted = builder.add_operation(
923 StdTensorOp::ExtractDiag { axis_a, axis_b },
924 vec![ValueRef::Local(result)],
925 OperationRole::Linearized {
926 active_mask: vec![true],
927 },
928 )[0];
929 result = builder.add_operation(
930 StdTensorOp::EmbedDiag { axis_a, axis_b },
931 vec![ValueRef::Local(extracted)],
932 OperationRole::Linearized {
933 active_mask: vec![true],
934 },
935 )[0];
936 }
937 result
938}
939
940define_extension_runtime! {
941 runtime = EinsumRuntime,
942 family_id = EINSUM_EXTENSION_FAMILY_ID,
943 op_type = EinsumExtensionOp,
944 execute = execute_einsum_extension,
945 execute_reads = execute_einsum_extension_reads,
946 register_fn = register_runtime,
947}
948
949fn execute_einsum_extension<B: TensorBackend + 'static>(
950 op: &EinsumExtensionOp,
951 inputs: &[&Tensor],
952 ctx: &mut ExtensionExecutionContext<'_, B>,
953) -> tenferro_tensor::Result<Vec<Tensor>> {
954 if inputs.is_empty() {
955 return Err(tenferro_tensor::Error::InvalidConfig {
956 op: "einsum_extension",
957 message: "einsum requires at least one input tensor".into(),
958 });
959 }
960
961 let shapes: Vec<Vec<usize>> = inputs
962 .iter()
963 .map(|tensor| tensor.shape().to_vec())
964 .collect();
965 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
966 let subs = Subscripts::from(op.subscripts());
967 let tree = if let Some(tree) = op.static_tree() {
968 Arc::clone(tree)
969 } else {
970 cached_runtime_tree(ctx, op.subscripts(), op.plan_spec(), &shapes, || {
971 resolve_plan_spec(op.plan_spec(), &subs, &shape_refs)
972 })?
973 };
974
975 if is_binary_non_contracting(&subs) {
976 let output = ctx
977 .backend_mut()
978 .with_backend_session(|exec| crate::eager::eager_einsum_exec(exec, inputs, &tree))?;
979 return Ok(vec![output]);
980 }
981
982 let (backend, caches) = ctx.parts_mut();
983 let compiler_options = tenferro_runtime::extension::CompilerOptions::default();
984 let optimizer_fingerprint = compiler_options.optimizer.fingerprint();
985 let plan_hash = plan_spec_hash(op.plan_spec());
986 let key = runtime_exec_program_cache_key(op, inputs, &shapes, plan_hash, optimizer_fingerprint);
987 let cache_matches = caches
988 .get::<CachedRuntimeExecProgram<B::RuntimeCache>>(&key)
989 .is_some_and(|cached| {
990 let key_data = &cached.key_data;
991 key_data.matches_runtime_exec_program(op, inputs, &shapes, optimizer_fingerprint)
992 });
993 if !cache_matches {
994 let key_data =
995 RuntimeExecProgramCacheKeyData::new(op, inputs, &shapes, optimizer_fingerprint);
996 let cached = build_runtime_exec_program::<B>(
997 tree.as_ref(),
998 inputs,
999 &shapes,
1000 compiler_options,
1001 key_data,
1002 )?;
1003 caches.put_with_retained_bytes(key, cached, |cached| {
1004 cached_runtime_exec_program_retained_bytes(cached)
1005 });
1006 }
1007 let cached = caches
1008 .get_mut::<CachedRuntimeExecProgram<B::RuntimeCache>>(&key)
1009 .ok_or_else(|| {
1010 tenferro_tensor::Error::backend_failure(
1011 "einsum_extension",
1012 "runtime exec program cache entry missing after insertion",
1013 )
1014 })?;
1015 let key_data = &cached.key_data;
1016 if !key_data.matches_runtime_exec_program(op, inputs, &shapes, optimizer_fingerprint) {
1017 return Err(tenferro_tensor::Error::backend_failure(
1018 "einsum_extension",
1019 "runtime exec program cache hash collision was not replaced",
1020 ));
1021 }
1022 let program_inputs = runtime_program_inputs(inputs, cached.input_indices.as_slice())?;
1023 let mut outputs = tenferro_runtime::extension::execute_lowered_program_with_backend_cache(
1024 backend,
1025 &cached.program,
1026 program_inputs,
1027 &mut cached.backend_cache,
1028 )
1029 .map_err(|err| tenferro_tensor::Error::backend_failure("einsum_extension", err.to_string()))?;
1030 if outputs.len() != 1 {
1031 return Err(tenferro_tensor::Error::backend_failure(
1032 "einsum_extension",
1033 format!("expected 1 output, got {}", outputs.len()),
1034 ));
1035 }
1036 Ok(vec![outputs.remove(0)])
1037}
1038
1039fn execute_einsum_extension_reads<B: TensorBackend + 'static>(
1040 op: &EinsumExtensionOp,
1041 inputs: &[TensorRead<'_>],
1042 ctx: &mut ExtensionExecutionContext<'_, B>,
1043) -> tenferro_tensor::Result<Vec<Tensor>> {
1044 if inputs
1045 .iter()
1046 .all(|input| matches!(input, TensorRead::Tensor(_)))
1047 {
1048 let input_refs: Vec<&Tensor> = inputs
1049 .iter()
1050 .map(|input| match input {
1051 TensorRead::Tensor(tensor) => *tensor,
1052 TensorRead::View(_) => unreachable!("view input filtered above"),
1053 })
1054 .collect();
1055 return execute_einsum_extension(op, &input_refs, ctx);
1056 }
1057
1058 if inputs.is_empty() {
1059 return Err(tenferro_tensor::Error::InvalidConfig {
1060 op: "einsum_extension",
1061 message: "einsum requires at least one input tensor".into(),
1062 });
1063 }
1064
1065 let shapes: Vec<Vec<usize>> = inputs.iter().map(|input| input.shape().to_vec()).collect();
1066 let shape_refs: Vec<&[usize]> = shapes.iter().map(Vec::as_slice).collect();
1067 let subs = Subscripts::from(op.subscripts());
1068 let tree = if let Some(tree) = op.static_tree() {
1069 Arc::clone(tree)
1070 } else {
1071 cached_runtime_tree(ctx, op.subscripts(), op.plan_spec(), &shapes, || {
1072 resolve_plan_spec(op.plan_spec(), &subs, &shape_refs)
1073 })?
1074 };
1075 let output = ctx
1076 .backend_mut()
1077 .with_backend_session(|exec| crate::eager::eager_einsum_exec_read(exec, inputs, &tree))?;
1078 Ok(vec![output])
1079}
1080
1081fn is_binary_non_contracting(subs: &Subscripts) -> bool {
1082 if subs.inputs.len() != 2 {
1083 return false;
1084 }
1085
1086 let lhs = &subs.inputs[0];
1087 let rhs = &subs.inputs[1];
1088 let output = &subs.output;
1089 !lhs.iter()
1090 .any(|label| rhs.contains(label) && !output.contains(label))
1091}
1092
1093#[derive(Clone)]
1094struct RuntimeTreeCacheKeyData {
1095 subscripts: EinsumSubscripts,
1096 shapes: Vec<Vec<usize>>,
1097 plan_spec: EinsumPlanSpec,
1098}
1099
1100impl RuntimeTreeCacheKeyData {
1101 fn new(
1102 subscripts: &EinsumSubscripts,
1103 shapes: &[Vec<usize>],
1104 plan_spec: &EinsumPlanSpec,
1105 ) -> Self {
1106 Self {
1107 subscripts: subscripts.clone(),
1108 shapes: shapes.to_vec(),
1109 plan_spec: plan_spec.clone(),
1110 }
1111 }
1112
1113 fn matches_runtime_tree(
1114 &self,
1115 subscripts: &EinsumSubscripts,
1116 shapes: &[Vec<usize>],
1117 plan_spec: &EinsumPlanSpec,
1118 ) -> bool {
1119 self.subscripts == *subscripts
1120 && self.shapes.as_slice() == shapes
1121 && plan_specs_equal(&self.plan_spec, plan_spec)
1122 }
1123
1124 fn retained_bytes(&self) -> usize {
1125 saturating_sum([
1126 einsum_subscripts_retained_bytes(&self.subscripts),
1127 saturating_sum(self.shapes.iter().map(vec_retained_bytes)),
1128 plan_spec_retained_bytes(&self.plan_spec),
1129 ])
1130 }
1131}
1132
1133struct CachedRuntimeTree {
1134 key_data: RuntimeTreeCacheKeyData,
1135 tree: Arc<ContractionTree>,
1136}
1137
1138#[derive(Clone)]
1139struct RuntimeExecProgramCacheKeyData {
1140 subscripts: EinsumSubscripts,
1141 shapes: Vec<Vec<usize>>,
1142 input_dtypes: Vec<DType>,
1143 plan_spec: EinsumPlanSpec,
1144 optimizer_fingerprint: u64,
1145}
1146
1147impl RuntimeExecProgramCacheKeyData {
1148 fn new(
1149 op: &EinsumExtensionOp,
1150 inputs: &[&Tensor],
1151 shapes: &[Vec<usize>],
1152 optimizer_fingerprint: u64,
1153 ) -> Self {
1154 Self {
1155 subscripts: op.subscripts().clone(),
1156 shapes: shapes.to_vec(),
1157 input_dtypes: inputs.iter().map(|tensor| tensor.dtype()).collect(),
1158 plan_spec: op.plan_spec().clone(),
1159 optimizer_fingerprint,
1160 }
1161 }
1162
1163 fn matches_runtime_exec_program(
1164 &self,
1165 op: &EinsumExtensionOp,
1166 inputs: &[&Tensor],
1167 shapes: &[Vec<usize>],
1168 optimizer_fingerprint: u64,
1169 ) -> bool {
1170 self.subscripts == *op.subscripts()
1171 && self.shapes.as_slice() == shapes
1172 && self.optimizer_fingerprint == optimizer_fingerprint
1173 && plan_specs_equal(&self.plan_spec, op.plan_spec())
1174 && self.input_dtypes.len() == inputs.len()
1175 && self
1176 .input_dtypes
1177 .iter()
1178 .zip(inputs.iter())
1179 .all(|(&dtype, tensor)| dtype == tensor.dtype())
1180 }
1181
1182 fn retained_bytes(&self) -> usize {
1183 saturating_sum([
1184 einsum_subscripts_retained_bytes(&self.subscripts),
1185 saturating_sum(self.shapes.iter().map(vec_retained_bytes)),
1186 vec_retained_bytes(&self.input_dtypes),
1187 plan_spec_retained_bytes(&self.plan_spec),
1188 std::mem::size_of_val(&self.optimizer_fingerprint),
1189 ])
1190 }
1191}
1192
1193struct CachedRuntimeExecProgram<C> {
1194 key_data: RuntimeExecProgramCacheKeyData,
1195 program: ExecProgram,
1196 input_indices: InputIndexVec,
1197 backend_cache: C,
1198}
1199
1200fn runtime_exec_program_cache_key(
1201 op: &EinsumExtensionOp,
1202 inputs: &[&Tensor],
1203 shapes: &[Vec<usize>],
1204 plan_hash: u64,
1205 optimizer_fingerprint: u64,
1206) -> ExtensionCacheKey {
1207 let mut hasher = DefaultHasher::new();
1208 op.subscripts().hash(&mut hasher);
1209 shapes.hash(&mut hasher);
1210 for input in inputs {
1211 input.dtype().hash(&mut hasher);
1212 }
1213 plan_hash.hash(&mut hasher);
1214 optimizer_fingerprint.hash(&mut hasher);
1215 ExtensionCacheKey::new(
1216 EINSUM_EXTENSION_FAMILY_ID,
1217 EINSUM_RUNTIME_EXEC_PROGRAMS_CACHE,
1218 hasher.finish(),
1219 )
1220}
1221
1222fn build_runtime_exec_program<B: TensorBackend>(
1223 tree: &ContractionTree,
1224 inputs: &[&Tensor],
1225 shapes: &[Vec<usize>],
1226 compiler_options: tenferro_runtime::extension::CompilerOptions,
1227 key_data: RuntimeExecProgramCacheKeyData,
1228) -> tenferro_tensor::Result<CachedRuntimeExecProgram<B::RuntimeCache>> {
1229 let mut builder = GraphBuilder::<StdTensorOp>::new();
1230 let mut input_vals = Vec::with_capacity(inputs.len());
1231 for input_idx in 0..inputs.len() {
1232 let local = builder.add_input(TensorInputKey::User {
1233 id: input_idx as u64,
1234 });
1235 input_vals.push(ValueRef::Local(local));
1236 }
1237
1238 let result_ref = build_einsum_graph(&mut builder, tree, &input_vals, shapes)
1239 .map_err(einsum_runtime_error)?;
1240 let result_local = match result_ref {
1241 ValueRef::Local(local) => local,
1242 ValueRef::External(_) => {
1243 return Err(tenferro_tensor::Error::backend_failure(
1244 "einsum_extension",
1245 "einsum builder returned an external value at runtime",
1246 ))
1247 }
1248 };
1249 builder.set_outputs(vec![result_local]);
1250 let graph = Arc::new(builder.build());
1251 let output_key = graph.values()[result_local].key.clone();
1252
1253 let view = resolve(vec![graph]);
1254 let graph = materialize_merge(&view, &[output_key]);
1255 let compiled = compile(&graph);
1256
1257 let mut input_indices = InputIndexVec::new();
1258 let mut input_dtypes = Vec::with_capacity(graph.inputs.len());
1259 let mut input_shapes = Vec::with_capacity(graph.inputs.len());
1260 for key in &graph.inputs {
1261 match key {
1262 ValueKey::Input(TensorInputKey::User { id }) => {
1263 let input_idx = *id as usize;
1264 let tensor = inputs.get(input_idx).ok_or_else(|| {
1265 tenferro_tensor::Error::backend_failure(
1266 "einsum_extension",
1267 format!("runtime input {input_idx} missing"),
1268 )
1269 })?;
1270 input_indices.push(input_idx);
1271 input_dtypes.push(tensor.dtype());
1272 input_shapes.push(tenferro_ops::dim_expr::DimExpr::from_concrete(
1273 tensor.shape(),
1274 ));
1275 }
1276 other => {
1277 return Err(tenferro_tensor::Error::backend_failure(
1278 "einsum_extension",
1279 format!("unexpected runtime input key: {other:?}"),
1280 ))
1281 }
1282 }
1283 }
1284
1285 let program = tenferro_runtime::extension::compile_std_to_exec_with_options(
1286 &compiled,
1287 &input_dtypes,
1288 &input_shapes,
1289 compiler_options,
1290 )
1291 .map_err(|err| tenferro_tensor::Error::backend_failure("einsum_extension", err.to_string()))?;
1292 Ok(CachedRuntimeExecProgram {
1293 key_data,
1294 program,
1295 input_indices,
1296 backend_cache: B::RuntimeCache::default(),
1297 })
1298}
1299
1300fn runtime_program_inputs(
1301 inputs: &[&Tensor],
1302 input_indices: &[usize],
1303) -> tenferro_tensor::Result<Vec<Tensor>> {
1304 let mut program_inputs = Vec::with_capacity(input_indices.len());
1305 for &input_idx in input_indices {
1306 let tensor = inputs.get(input_idx).ok_or_else(|| {
1307 tenferro_tensor::Error::backend_failure(
1308 "einsum_extension",
1309 format!("runtime input {input_idx} missing"),
1310 )
1311 })?;
1312 program_inputs.push((*tensor).clone());
1313 }
1314 Ok(program_inputs)
1315}
1316
1317fn cached_runtime_exec_program_retained_bytes<C: RuntimeCacheControl>(
1318 cached: &CachedRuntimeExecProgram<C>,
1319) -> usize {
1320 saturating_sum([
1321 std::mem::size_of::<CachedRuntimeExecProgram<C>>(),
1322 cached.key_data.retained_bytes(),
1323 exec_program_retained_bytes(&cached.program),
1324 smallvec_retained_bytes(&cached.input_indices),
1325 cached.backend_cache.stats().retained_bytes,
1326 ])
1327}
1328
1329fn smallvec_retained_bytes<A: smallvec::Array>(values: &SmallVec<A>) -> usize {
1330 if values.spilled() {
1331 values
1332 .capacity()
1333 .saturating_mul(std::mem::size_of::<A::Item>())
1334 } else {
1335 0
1336 }
1337}
1338
1339fn exec_program_retained_bytes(program: &ExecProgram) -> usize {
1340 saturating_sum([
1341 std::mem::size_of::<ExecProgram>(),
1342 vec_retained_bytes(&program.instructions),
1343 saturating_sum(
1344 program
1345 .instructions
1346 .iter()
1347 .map(exec_instruction_retained_bytes),
1348 ),
1349 vec_retained_bytes(&program.input_slots),
1350 vec_retained_bytes(&program.output_slots),
1351 ])
1352}
1353
1354fn exec_instruction_retained_bytes(inst: &ExecInstruction) -> usize {
1355 saturating_sum([
1356 std::mem::size_of::<ExecInstruction>(),
1357 exec_op_retained_bytes(&inst.op),
1358 vec_retained_bytes(&inst.input_slots),
1359 vec_retained_bytes(&inst.output_slots),
1360 vec_of_vec_retained_bytes(&inst.output_shapes),
1361 vec_of_vec_retained_bytes(&inst.output_extents),
1362 vec_retained_bytes(&inst.last_use),
1363 ])
1364}
1365
1366fn exec_op_retained_bytes(op: &ExecOp) -> usize {
1367 match op {
1368 ExecOp::Constant { bytes, .. } => vec_retained_bytes(bytes),
1369 ExecOp::Extension(extension) => std::mem::size_of_val(extension),
1370 _ => 0,
1371 }
1372}
1373
1374fn cached_runtime_tree<B: TensorBackend>(
1375 ctx: &mut ExtensionExecutionContext<'_, B>,
1376 subscripts: &EinsumSubscripts,
1377 plan_spec: &EinsumPlanSpec,
1378 shapes: &[Vec<usize>],
1379 build: impl FnOnce() -> EinsumResult<ContractionTree>,
1380) -> tenferro_tensor::Result<Arc<ContractionTree>> {
1381 let plan_hash = plan_spec_hash(plan_spec);
1382 let key = ExtensionCacheKey::new(
1383 EINSUM_EXTENSION_FAMILY_ID,
1384 EINSUM_RUNTIME_PLANS_CACHE,
1385 runtime_tree_cache_discriminator(subscripts, shapes, plan_hash),
1386 );
1387 if let Some(cached) = ctx.caches_mut().get::<CachedRuntimeTree>(&key) {
1388 let key_data = &cached.key_data;
1389 if key_data.matches_runtime_tree(subscripts, shapes, plan_spec) {
1390 return Ok(Arc::clone(&cached.tree));
1391 }
1392 }
1393
1394 let tree = Arc::new(build().map_err(einsum_runtime_error)?);
1395 let key_data = RuntimeTreeCacheKeyData::new(subscripts, shapes, plan_spec);
1396 let retained_bytes = saturating_sum([
1397 key_data.retained_bytes(),
1398 tree.retained_bytes_for_cache_stats(),
1399 ]);
1400 ctx.caches_mut().put(
1401 key,
1402 CachedRuntimeTree {
1403 key_data,
1404 tree: Arc::clone(&tree),
1405 },
1406 retained_bytes,
1407 );
1408 Ok(tree)
1409}
1410
1411fn einsum_runtime_error(error: EinsumError) -> tenferro_tensor::Error {
1412 error.to_tensor_error("einsum_extension")
1413}
1414
1415fn runtime_tree_cache_discriminator(
1416 subscripts: &EinsumSubscripts,
1417 shapes: &[Vec<usize>],
1418 plan_hash: u64,
1419) -> u64 {
1420 let mut hasher = DefaultHasher::new();
1421 subscripts.hash(&mut hasher);
1422 shapes.hash(&mut hasher);
1423 plan_hash.hash(&mut hasher);
1424 hasher.finish()
1425}
1426
1427fn plan_spec_hash(plan_spec: &EinsumPlanSpec) -> u64 {
1428 let mut hasher = DefaultHasher::new();
1429 hash_einsum_plan_spec(plan_spec, &mut hasher);
1430 hasher.finish()
1431}
1432
1433fn plan_spec_retained_bytes(plan_spec: &EinsumPlanSpec) -> usize {
1434 match plan_spec {
1435 EinsumPlanSpec::Auto(options) => saturating_sum([
1436 std::mem::size_of::<EinsumPlanSpec>(),
1437 vec_retained_bytes(&options.betas),
1438 ]),
1439 EinsumPlanSpec::LeftToRight => std::mem::size_of::<EinsumPlanSpec>(),
1440 EinsumPlanSpec::Path(path) | EinsumPlanSpec::FixedPairs(path) => saturating_sum([
1441 std::mem::size_of::<EinsumPlanSpec>(),
1442 vec_retained_bytes(path),
1443 ]),
1444 }
1445}
1446
1447#[cfg(feature = "autodiff")]
1448fn downcast_ad_op(op: &dyn ExtensionOp, kind: ADRuleKind) -> ADRuleResult<&EinsumExtensionOp> {
1449 op.as_any()
1450 .downcast_ref::<EinsumExtensionOp>()
1451 .ok_or_else(|| ADRuleError::unsupported("tenferro.einsum.v1 payload type mismatch", kind))
1452}
1453
1454#[cfg(feature = "autodiff")]
1455fn sum_terms(
1456 builder: &mut dyn PrimitiveRuleBuilder,
1457 terms: Vec<LocalValueId>,
1458) -> Option<LocalValueId> {
1459 match terms.as_slice() {
1460 [] => None,
1461 [only] => Some(*only),
1462 [head, tail @ ..] => {
1463 let mut result = *head;
1464 for &term in tail {
1465 let sum = builder.add_operation(
1466 StdTensorOp::Add,
1467 vec![ValueRef::Local(result), ValueRef::Local(term)],
1468 OperationRole::Linearized {
1469 active_mask: vec![true, true],
1470 },
1471 );
1472 result = sum[0];
1473 }
1474 Some(result)
1475 }
1476 }
1477}
1478
1479#[cfg(feature = "autodiff")]
1480fn conjugate_primal_if_complex(
1481 builder: &mut dyn PrimitiveRuleBuilder,
1482 input: ValueRef<StdTensorOp>,
1483 ctx: &mut ShapeGuardContext,
1484) -> ADRuleResult<ValueRef<StdTensorOp>> {
1485 Ok(match ctx.dtype_of(&input)? {
1486 DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => input,
1487 DType::C32 | DType::C64 => ValueRef::Local(
1488 builder.add_operation(StdTensorOp::Conj, vec![input], OperationRole::Primary)[0],
1489 ),
1490 })
1491}
1492
1493fn promote_dtypes(dtypes: impl IntoIterator<Item = DType>) -> DType {
1494 dtypes
1495 .into_iter()
1496 .reduce(tenferro_tensor::validate::promote_dtype)
1497 .unwrap_or(DType::F64)
1498}
1499
1500#[cfg(test)]
1501mod tests;