Skip to main content

tenferro_einsum/
extension.rs

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/// Standard einsum extension payload.
65///
66/// This mirrors the current `tenferro.einsum.v1` payload shape. Runtime-owned
67/// execution goes through [`EinsumRuntime`]. The optional
68/// [`ExtensionOp::host_reference`] hook remains available for direct
69/// context-free reference execution.
70#[derive(Clone)]
71pub(crate) struct EinsumExtensionOp {
72    subscripts: EinsumSubscripts,
73    plan_spec: EinsumPlanSpec,
74    /// Optional execution hint. This is intentionally excluded from
75    /// `ExtensionOp` identity: the shape-independent `plan_spec` carries
76    /// user planning policy, while this tree is a resolved cacheable hint.
77    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    /// Create an einsum extension payload without a precomputed plan.
94    #[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    /// Create an einsum extension payload with a precomputed plan.
111    #[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    /// Create an einsum extension payload with an explicit output shape hint.
121    #[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    /// Attach a precomputed contraction tree as an execution hint.
133    #[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    /// Return the canonical subscripts.
141    #[must_use]
142    pub(crate) fn subscripts(&self) -> &EinsumSubscripts {
143        &self.subscripts
144    }
145
146    /// Return the shape-independent planning policy.
147    #[must_use]
148    pub(crate) fn plan_spec(&self) -> &EinsumPlanSpec {
149        &self.plan_spec
150    }
151
152    /// Return the precomputed contraction tree, if present.
153    #[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/// Return the explicit einsum extension AD rule set.
355#[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;