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