Skip to main content

tensor4all_tensorbackend/
tenferro_bridge.rs

1//! Bridge helpers between tensor4all storage snapshots and tenferro tensors.
2
3use std::cell::RefCell;
4use std::cmp::Reverse;
5use std::collections::{HashMap, HashSet};
6use std::env;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use anyhow::{anyhow, ensure, Result};
11use num_complex::{Complex32, Complex64};
12use omeco::ScoreFunction;
13use tenferro::{
14    program::CoreSemanticOp, CompiledGraph, DType, GraphCompiler, Tensor as NativeTensor,
15    TensorRead, TensorScalar, TensorSessionOpsExt, TraceContext,
16};
17use tenferro_einsum::{
18    ConcreteEinsumPlan, ContractionOptimizerOptions, ContractionTree, EinsumSubscripts, Subscripts,
19    TraceContextEinsumExt,
20};
21use tenferro_linalg::TensorLinalgExt;
22
23use crate::any_scalar::promote_scalar_native;
24pub(crate) use crate::einsum_ids::{
25    build_binary_einsum_ids, checked_native_einsum_labels, common_dtype, convert_native_tensor_in,
26};
27/// Error returned by the storage/tensor bridge helpers.
28///
29/// Wraps the underlying tensor-element or backend diagnostic, preserving its
30/// source chain.
31///
32/// # Remedies
33/// - Dtype mismatch: extract with the element type matching the native tensor
34///   dtype, or promote explicitly before the call.
35/// - Shape mismatch: validate the native tensor shape against the payload
36///   contract.
37/// - Backend failure: the wrapped source chain identifies the failing stage.
38#[derive(Debug, thiserror::Error)]
39#[error("native tensor bridge operation failed: {source}")]
40pub struct BridgeError {
41    /// Original tensor-element or backend diagnostic.
42    #[source]
43    pub source: anyhow::Error,
44}
45
46impl From<anyhow::Error> for BridgeError {
47    fn from(source: anyhow::Error) -> Self {
48        Self { source }
49    }
50}
51
52fn native_tensor_from_vec<T: TensorScalar>(
53    shape: Vec<usize>,
54    values: Vec<T>,
55) -> std::result::Result<NativeTensor, BridgeError> {
56    NativeTensor::from_vec_col_major(shape, values)
57        .map_err(|source| BridgeError::from(anyhow::Error::new(source)))
58}
59
60use crate::context::{
61    default_context, default_engine_buffer_pool_stats, reset_default_engine,
62    reset_default_engine_buffer_pool, with_default_graph_compiler, with_default_session,
63};
64use crate::memory::release_process_allocator_cached_memory;
65use crate::storage::{storage_dtype_dispatch, Storage, StorageScalar};
66use crate::tensor_element::TensorElement;
67use crate::BackendScalar;
68
69/// Read-only native tensor input that can either borrow external payload data
70/// or own a temporary materialized tensor.
71// Both tenferro handle variants are large; boxing the slightly larger one would
72// add an allocation to the eager einsum path for little enum-size reduction.
73#[allow(clippy::large_enum_variant)]
74pub enum NativeTensorReadInput<'a> {
75    /// Borrowed read-only tensor input.
76    Borrowed(TensorRead<'a>),
77    /// Owned temporary tensor input.
78    Owned(NativeTensor),
79}
80
81impl<'a> NativeTensorReadInput<'a> {
82    /// Return this input as a read-only tenferro tensor input.
83    pub fn as_read(&'a self) -> TensorRead<'a> {
84        match self {
85            Self::Borrowed(read) => read.clone(),
86            Self::Owned(tensor) => TensorRead::from_tensor(tensor),
87        }
88    }
89
90    /// Return the scalar dtype of this input.
91    pub fn dtype(&self) -> DType {
92        match self {
93            Self::Borrowed(read) => read.dtype(),
94            Self::Owned(tensor) => tensor.dtype(),
95        }
96    }
97
98    /// Return the tensor shape of this input.
99    pub fn shape(&self) -> &[usize] {
100        match self {
101            Self::Borrowed(read) => read.shape(),
102            Self::Owned(tensor) => tensor.shape(),
103        }
104    }
105}
106
107#[cfg(test)]
108use std::cell::Cell;
109
110#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
111enum NativeEinsumPath {
112    Owned,
113    Borrowed,
114    BorrowedWithConversions,
115}
116
117#[derive(Debug, Clone, Hash, PartialEq, Eq)]
118struct NativeOperandSignature {
119    shape: Vec<usize>,
120    ids: Vec<u32>,
121    dtype: DType,
122}
123
124#[derive(Debug, Clone, Hash, PartialEq, Eq)]
125struct NativeEinsumSignature {
126    path: NativeEinsumPath,
127    operands: Vec<NativeOperandSignature>,
128    output_ids: Vec<u32>,
129}
130
131#[derive(Debug, Default, Clone)]
132struct NativeEinsumProfileEntry {
133    calls: usize,
134    total_time: Duration,
135}
136
137/// Entries kept before the cache is cleared. Real workloads cycle through
138/// hundreds to a few thousand distinct contraction signatures (#891: a
139/// 400-shape QTT cycle re-prepared every plan at 256), so the bound sits above
140/// that working set while still capping the per-thread cache.
141const CONCRETE_EINSUM_PLAN_CACHE_CAPACITY: usize = 4096;
142
143thread_local! {
144    static NATIVE_EINSUM_PROFILE_STATE: RefCell<HashMap<NativeEinsumSignature, NativeEinsumProfileEntry>> =
145        RefCell::new(HashMap::new());
146    static NATIVE_EINSUM_TRACE_STATE: RefCell<HashSet<NativeEinsumSignature>> =
147        RefCell::new(HashSet::new());
148    static CONCRETE_EINSUM_PLAN_CACHE: RefCell<HashMap<NativeEinsumSignature, Arc<ConcreteEinsumPlan>>> =
149        RefCell::new(HashMap::new());
150}
151
152#[cfg(test)]
153thread_local! {
154    static FORCE_NATIVE_EINSUM_PROFILE: Cell<bool> = const { Cell::new(false) };
155}
156
157fn native_einsum_profile_enabled() -> bool {
158    #[cfg(test)]
159    if FORCE_NATIVE_EINSUM_PROFILE.with(Cell::get) {
160        return true;
161    }
162    env::var("T4A_PROFILE_NATIVE_EINSUM").is_ok()
163}
164
165fn native_einsum_path_trace_enabled() -> bool {
166    env::var("T4A_TRACE_NATIVE_EINSUM_PATHS").is_ok()
167}
168
169fn native_einsum_path_trace_min_bytes() -> usize {
170    env::var("T4A_TRACE_NATIVE_EINSUM_MIN_BYTES")
171        .ok()
172        .and_then(|value| value.parse().ok())
173        .unwrap_or(0)
174}
175
176fn native_einsum_path_trace_max_signatures() -> usize {
177    env::var("T4A_TRACE_NATIVE_EINSUM_MAX_SIGNATURES")
178        .ok()
179        .and_then(|value| value.parse().ok())
180        .unwrap_or(64)
181}
182
183fn native_einsum_pool_trace_enabled() -> bool {
184    env::var("T4A_TRACE_NATIVE_EINSUM_POOL").is_ok()
185}
186
187fn native_einsum_pool_trace_min_output_bytes() -> usize {
188    env::var("T4A_TRACE_NATIVE_EINSUM_POOL_MIN_OUTPUT_BYTES")
189        .ok()
190        .and_then(|value| value.parse().ok())
191        .unwrap_or(0)
192}
193
194fn native_einsum_pool_trace_min_retained_bytes() -> usize {
195    env::var("T4A_TRACE_NATIVE_EINSUM_POOL_MIN_RETAINED_BYTES")
196        .ok()
197        .and_then(|value| value.parse().ok())
198        .unwrap_or(0)
199}
200
201fn reset_native_einsum_engine_after_call() -> bool {
202    env::var("T4A_RESET_NATIVE_EINSUM_ENGINE_AFTER_CALL").is_ok()
203}
204
205fn reset_native_einsum_buffer_pool_after_call() -> bool {
206    env::var("T4A_RESET_NATIVE_EINSUM_BUFFER_POOL_AFTER_CALL").is_ok()
207}
208
209fn release_allocator_after_native_einsum_call() -> bool {
210    env::var("T4A_RELEASE_ALLOCATOR_AFTER_NATIVE_EINSUM_CALL").is_ok()
211}
212
213#[cfg(test)]
214pub(crate) fn set_native_einsum_profile_enabled_for_tests(enabled: bool) {
215    FORCE_NATIVE_EINSUM_PROFILE.with(|slot| slot.set(enabled));
216}
217
218fn native_einsum_signature(
219    path: NativeEinsumPath,
220    operands: &[(&NativeTensor, &[u32])],
221    output_ids: &[u32],
222) -> NativeEinsumSignature {
223    NativeEinsumSignature {
224        path,
225        operands: operands
226            .iter()
227            .map(|(tensor, ids)| NativeOperandSignature {
228                shape: tensor.shape().to_vec(),
229                ids: ids.to_vec(),
230                dtype: tensor.dtype(),
231            })
232            .collect(),
233        output_ids: output_ids.to_vec(),
234    }
235}
236
237fn concrete_einsum_dtype_supported(dtype: DType) -> bool {
238    matches!(dtype, DType::F32 | DType::F64 | DType::C32 | DType::C64)
239}
240
241fn concrete_einsum_signature(
242    inputs: &[TensorRead<'_>],
243    subscripts: &Subscripts,
244) -> NativeEinsumSignature {
245    // ConcreteEinsumPlan owns only dtype, shape, labels, and its contraction
246    // tree. Per-call TensorRead strides are validated and consumed at execute.
247    NativeEinsumSignature {
248        path: NativeEinsumPath::Borrowed,
249        operands: inputs
250            .iter()
251            .zip(subscripts.inputs.iter())
252            .map(|(tensor, ids)| NativeOperandSignature {
253                shape: tensor.shape().to_vec(),
254                ids: ids.clone(),
255                dtype: tensor.dtype(),
256            })
257            .collect(),
258        output_ids: subscripts.output.clone(),
259    }
260}
261
262fn cached_concrete_einsum_plan(
263    inputs: &[TensorRead<'_>],
264    subscripts: &Subscripts,
265    einsum_subscripts: &EinsumSubscripts,
266) -> Result<Arc<ConcreteEinsumPlan>> {
267    let signature = concrete_einsum_signature(inputs, subscripts);
268    if let Some(plan) =
269        CONCRETE_EINSUM_PLAN_CACHE.with(|cache| cache.borrow().get(&signature).cloned())
270    {
271        return Ok(plan);
272    }
273
274    let plan = Arc::new(
275        ConcreteEinsumPlan::prepare_read_subscripts(inputs, einsum_subscripts)
276            .map_err(|error| anyhow!("native einsum session plan preparation failed: {error}"))?,
277    );
278    CONCRETE_EINSUM_PLAN_CACHE.with(|cache| {
279        let mut cache = cache.borrow_mut();
280        // ponytail: bounded clear-on-full cache; use an LRU only if shape churn is measured.
281        if cache.len() >= CONCRETE_EINSUM_PLAN_CACHE_CAPACITY {
282            cache.clear();
283        }
284        cache.insert(signature, Arc::clone(&plan));
285    });
286    Ok(plan)
287}
288
289#[cfg(test)]
290fn reset_concrete_einsum_plan_cache() {
291    CONCRETE_EINSUM_PLAN_CACHE.with(|cache| cache.borrow_mut().clear());
292}
293
294#[cfg(test)]
295fn concrete_einsum_plan_cache_len() -> usize {
296    CONCRETE_EINSUM_PLAN_CACHE.with(|cache| cache.borrow().len())
297}
298
299fn record_native_einsum_profile(
300    path: NativeEinsumPath,
301    operands: &[(&NativeTensor, &[u32])],
302    output_ids: &[u32],
303    elapsed: Duration,
304) {
305    if !native_einsum_profile_enabled() {
306        return;
307    }
308    let signature = native_einsum_signature(path, operands, output_ids);
309    NATIVE_EINSUM_PROFILE_STATE.with(|state| {
310        let mut state = state.borrow_mut();
311        let entry = state.entry(signature).or_default();
312        entry.calls += 1;
313        entry.total_time += elapsed;
314    });
315}
316
317fn native_slice<'a, T: TensorScalar>(
318    tensor: &'a NativeTensor,
319    label: &'static str,
320) -> Result<&'a [T]> {
321    tensor
322        .as_slice::<T>()
323        .map_err(|error| anyhow!("{label}: {error}"))
324}
325
326fn dtype_size_bytes(dtype: DType) -> usize {
327    match dtype {
328        DType::F32 => 4,
329        DType::F64 => 8,
330        DType::C32 => 8,
331        DType::C64 => 16,
332        DType::I32 => 4,
333        DType::I64 => 8,
334        DType::Bool => 1,
335        // External scalar kinds have no element width this crate can know. They
336        // are rejected at every entry point that can materialize storage, so this
337        // accounting arm is unreachable for a tensor that passed those checks.
338        DType::External(_) => 0,
339    }
340}
341
342fn native_tensor_bytes(tensor: &NativeTensor) -> usize {
343    tensor
344        .shape()
345        .iter()
346        .copied()
347        .fold(1usize, usize::saturating_mul)
348        .saturating_mul(dtype_size_bytes(tensor.dtype()))
349}
350
351fn format_label(label: u32) -> String {
352    char::from_u32(label).map_or_else(|| label.to_string(), |label| label.to_string())
353}
354
355fn format_labels(labels: &[u32]) -> String {
356    if labels.is_empty() {
357        "scalar".to_string()
358    } else {
359        labels
360            .iter()
361            .map(|&label| format_label(label))
362            .collect::<Vec<_>>()
363            .join("")
364    }
365}
366
367fn label_dims(subscripts: &Subscripts, shapes: &[Vec<usize>]) -> Result<HashMap<u32, usize>> {
368    let mut dims = HashMap::new();
369    for (labels, shape) in subscripts.inputs.iter().zip(shapes.iter()) {
370        ensure!(
371            labels.len() == shape.len(),
372            "einsum labels {:?} do not match shape {:?}",
373            labels,
374            shape
375        );
376        for (&label, &dim) in labels.iter().zip(shape.iter()) {
377            if let Some(previous) = dims.insert(label, dim) {
378                ensure!(
379                    previous == dim,
380                    "inconsistent dimension for einsum label {}: {} vs {}",
381                    format_label(label),
382                    previous,
383                    dim
384                );
385            }
386        }
387    }
388    Ok(dims)
389}
390
391fn labels_size(labels: &[u32], dims: &HashMap<u32, usize>) -> usize {
392    labels.iter().fold(1usize, |size, label| {
393        size.saturating_mul(dims.get(label).copied().unwrap_or(1))
394    })
395}
396
397fn union_labels(lhs: &[u32], rhs: &[u32]) -> Vec<u32> {
398    let mut seen = HashSet::new();
399    let mut labels = Vec::new();
400    for &label in lhs.iter().chain(rhs.iter()) {
401        if seen.insert(label) {
402            labels.push(label);
403        }
404    }
405    labels
406}
407
408#[derive(Debug)]
409struct NativeEinsumPlanReport {
410    lines: Vec<String>,
411    peak_intermediate_bytes: usize,
412}
413
414fn time_optimized_contraction_options() -> ContractionOptimizerOptions {
415    ContractionOptimizerOptions {
416        score: ScoreFunction::time_optimized(),
417        ..ContractionOptimizerOptions::default()
418    }
419}
420
421fn native_einsum_plan_report_with_options(
422    signature: &NativeEinsumSignature,
423    optimizer_name: &'static str,
424    options: &ContractionOptimizerOptions,
425) -> Result<NativeEinsumPlanReport> {
426    let input_ids = signature
427        .operands
428        .iter()
429        .map(|operand| operand.ids.as_slice())
430        .collect::<Vec<_>>();
431    let subscripts_string = build_einsum_subscripts(&input_ids, &signature.output_ids)?;
432    let subscripts = Subscripts {
433        inputs: input_ids.iter().map(|ids| ids.to_vec()).collect(),
434        output: signature.output_ids.clone(),
435    };
436    let shapes = signature
437        .operands
438        .iter()
439        .map(|operand| operand.shape.clone())
440        .collect::<Vec<_>>();
441    let shape_refs = shapes.iter().map(Vec::as_slice).collect::<Vec<_>>();
442    let tree = ContractionTree::optimize_with_options(&subscripts, &shape_refs, options)
443        .map_err(|e| anyhow!("failed to optimize native einsum path: {e}"))?;
444    let dims = label_dims(&subscripts, &shapes)?;
445    let dtype = signature
446        .operands
447        .first()
448        .map(|operand| operand.dtype)
449        .unwrap_or(DType::F64);
450    let dtype_size = dtype_size_bytes(dtype);
451
452    let mut lines = Vec::new();
453    lines.push(format!(
454        "optimizer={optimizer_name} subscripts={subscripts_string} dtype={dtype:?} steps={}",
455        tree.step_count()
456    ));
457    let mut peak_intermediate_elems = 1usize;
458    for step in 0..tree.step_count() {
459        let Some((left, right)) = tree.step_pair(step) else {
460            continue;
461        };
462        let Some((lhs, rhs, out)) = tree.step_subscripts(step) else {
463            continue;
464        };
465        let lhs_elems = labels_size(lhs, &dims);
466        let rhs_elems = labels_size(rhs, &dims);
467        let out_elems = labels_size(out, &dims);
468        let flop_index_elems = labels_size(&union_labels(lhs, rhs), &dims);
469        peak_intermediate_elems = peak_intermediate_elems.max(out_elems);
470        lines.push(format!(
471            "  step {step:02}: pair=({left},{right}) {}[{}] x {}[{}] -> {}[{}]  flop_index={}  intermediate={} elems ({:.3} MiB)",
472            format_labels(lhs),
473            lhs_elems,
474            format_labels(rhs),
475            rhs_elems,
476            format_labels(out),
477            out_elems,
478            flop_index_elems,
479            out_elems,
480            out_elems as f64 * dtype_size as f64 / (1024.0 * 1024.0),
481        ));
482    }
483    let peak_intermediate_bytes = peak_intermediate_elems.saturating_mul(dtype_size);
484    lines.push(format!(
485        "  peak_intermediate={} elems ({:.3} MiB)",
486        peak_intermediate_elems,
487        peak_intermediate_bytes as f64 / (1024.0 * 1024.0)
488    ));
489
490    Ok(NativeEinsumPlanReport {
491        lines,
492        peak_intermediate_bytes,
493    })
494}
495
496fn native_einsum_time_optimized_plan_report(
497    signature: &NativeEinsumSignature,
498) -> Result<NativeEinsumPlanReport> {
499    native_einsum_plan_report_with_options(
500        signature,
501        "time_optimized",
502        &time_optimized_contraction_options(),
503    )
504}
505
506fn native_einsum_balanced_plan_report(
507    signature: &NativeEinsumSignature,
508) -> Result<NativeEinsumPlanReport> {
509    native_einsum_plan_report_with_options(
510        signature,
511        "balanced_default",
512        &ContractionOptimizerOptions::default(),
513    )
514}
515
516fn maybe_trace_native_einsum_path(
517    path: NativeEinsumPath,
518    operands: &[(&NativeTensor, &[u32])],
519    output_ids: &[u32],
520) {
521    if !native_einsum_path_trace_enabled() {
522        return;
523    }
524    let signature = native_einsum_signature(path, operands, output_ids);
525    let report = match native_einsum_time_optimized_plan_report(&signature) {
526        Ok(report) if report.peak_intermediate_bytes >= native_einsum_path_trace_min_bytes() => {
527            report
528        }
529        Ok(_) => return,
530        Err(err) => {
531            eprintln!("native_einsum path trace failed: {err:#}");
532            return;
533        }
534    };
535
536    let max_signatures = native_einsum_path_trace_max_signatures();
537    let should_trace = NATIVE_EINSUM_TRACE_STATE.with(|state| {
538        let mut state = state.borrow_mut();
539        if state.len() >= max_signatures || state.contains(&signature) {
540            false
541        } else {
542            state.insert(signature.clone());
543            true
544        }
545    });
546    if !should_trace {
547        return;
548    }
549
550    eprintln!("=== native_einsum Path Trace ===");
551    eprintln!(
552        "path={:?} output_ids={:?}",
553        signature.path, signature.output_ids
554    );
555    for operand in &signature.operands {
556        eprintln!(
557            "  operand shape={:?} ids={:?} dtype={:?}",
558            operand.shape, operand.ids, operand.dtype
559        );
560    }
561    for line in report.lines {
562        eprintln!("{line}");
563    }
564    if env::var("T4A_TRACE_NATIVE_EINSUM_COMPARE_BALANCED").is_ok() {
565        match native_einsum_balanced_plan_report(&signature) {
566            Ok(balanced) => {
567                for line in balanced.lines {
568                    eprintln!("{line}");
569                }
570            }
571            Err(err) => eprintln!("balanced native_einsum path trace failed: {err:#}"),
572        }
573    }
574}
575
576/// Reset the aggregated native einsum profile.
577pub fn reset_native_einsum_profile() {
578    NATIVE_EINSUM_PROFILE_STATE.with(|state| state.borrow_mut().clear());
579    NATIVE_EINSUM_TRACE_STATE.with(|state| state.borrow_mut().clear());
580}
581
582/// Print and clear the aggregated native einsum profile.
583pub fn print_and_reset_native_einsum_profile() {
584    if !native_einsum_profile_enabled() {
585        return;
586    }
587    NATIVE_EINSUM_PROFILE_STATE.with(|state| {
588        let mut entries: Vec<_> = state
589            .borrow()
590            .iter()
591            .map(|(k, v)| (k.clone(), v.clone()))
592            .collect();
593        state.borrow_mut().clear();
594        entries.sort_by_key(|(_, entry)| Reverse(entry.total_time));
595
596        eprintln!("=== native_einsum Profile ===");
597        for (idx, (signature, entry)) in entries.into_iter().take(20).enumerate() {
598            eprintln!(
599                "#{idx:02} path={:?} calls={} total={:.3}s per_call={:.3}us output_ids={:?}",
600                signature.path,
601                entry.calls,
602                entry.total_time.as_secs_f64(),
603                entry.total_time.as_secs_f64() * 1e6 / entry.calls as f64,
604                signature.output_ids,
605            );
606            for operand in &signature.operands {
607                eprintln!(
608                    "     shape={:?} ids={:?} dtype={:?}",
609                    operand.shape, operand.ids, operand.dtype
610                );
611            }
612            match native_einsum_time_optimized_plan_report(&signature) {
613                Ok(report) => {
614                    for line in report.lines {
615                        eprintln!("     {line}");
616                    }
617                }
618                Err(err) => eprintln!("     path report failed: {err:#}"),
619            }
620        }
621    });
622}
623
624fn convert_tensor(tensor: &NativeTensor, to: DType) -> Result<NativeTensor> {
625    with_default_session(|session| convert_native_tensor_in(session, tensor, to))
626        .map_err(|e| anyhow::Error::new(e).context(format!("tensor conversion to {to:?} failed")))
627}
628
629fn ids_to_subscript(ids: &[u32]) -> Result<String> {
630    const LETTERS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
631    let mut out = String::with_capacity(ids.len());
632    for &id in ids {
633        let idx = usize::try_from(id).unwrap_or(usize::MAX);
634        let letter = LETTERS
635            .get(idx)
636            .ok_or_else(|| anyhow!("einsum label {id} exceeds supported label range"))?;
637        out.push(char::from(*letter));
638    }
639    Ok(out)
640}
641
642fn build_einsum_subscripts(operands: &[&[u32]], output_ids: &[u32]) -> Result<String> {
643    let inputs = operands
644        .iter()
645        .map(|ids| ids_to_subscript(ids))
646        .collect::<Result<Vec<_>>>()?;
647    Ok(format!(
648        "{}->{}",
649        inputs.join(","),
650        ids_to_subscript(output_ids)?
651    ))
652}
653
654fn compile_native_einsum_program(
655    compiler: &mut GraphCompiler,
656    input_specs: impl IntoIterator<Item = tenferro::program::ProgramInputSpec>,
657    subscripts: &EinsumSubscripts,
658) -> Result<tenferro::CompiledGraph> {
659    let input_specs = input_specs.into_iter().collect::<Vec<_>>();
660    let target = common_dtype(
661        input_specs
662            .iter()
663            .map(|spec| spec.metadata().dtype())
664            .collect::<Vec<_>>()
665            .as_slice(),
666    );
667    let mut trace = TraceContext::new();
668    let trace_inputs = input_specs
669        .into_iter()
670        .map(|spec| {
671            let dtype = spec.metadata().dtype();
672            let input = trace
673                .input(spec)
674                .map_err(|e| anyhow!("native einsum input tracing failed: {e}"))?;
675            if dtype == target {
676                return Ok(input);
677            }
678            trace
679                .add_op(
680                    CoreSemanticOp::Convert {
681                        from: dtype,
682                        to: target,
683                    },
684                    &[input],
685                )
686                .map_err(|e| anyhow!("native einsum promotion tracing failed: {e}"))?
687                .first()
688                .copied()
689                .ok_or_else(|| anyhow!("native einsum promotion returned no output"))
690        })
691        .collect::<Result<Vec<_>>>()?;
692    let output = trace
693        .einsum_subscripts(&trace_inputs, subscripts)
694        .map_err(|e| anyhow!("native einsum tracing failed: {e}"))?;
695    let graph = trace
696        .finish(&[output])
697        .map_err(|e| anyhow!("native einsum graph finalization failed: {e}"))?;
698    compiler
699        .compile_traced_graph(&graph)
700        .map_err(|e| anyhow!("native einsum graph compilation failed: {e}"))
701}
702
703fn run_cached_native_einsum(
704    subscripts: &EinsumSubscripts,
705    build: impl FnOnce(&mut GraphCompiler) -> Result<CompiledGraph>,
706    execute: impl FnOnce(&crate::context::CpuExecutionContext, &CompiledGraph) -> Result<NativeTensor>,
707) -> Result<NativeTensor> {
708    let trace_pool = native_einsum_pool_trace_enabled();
709    let pool_before = trace_pool
710        .then(default_engine_buffer_pool_stats)
711        .transpose()?;
712    // Compile under the compiler lock only; the graph then runs under the
713    // context's engine lock. Holding both would make a compile wait for a graph
714    // that is running.
715    let program = with_default_graph_compiler(build)?;
716    let result = execute(default_context(), &program)?;
717    if trace_pool {
718        let pool_after = default_engine_buffer_pool_stats()?;
719        let output_bytes = native_tensor_bytes(&result);
720        let retained_threshold = native_einsum_pool_trace_min_retained_bytes();
721        if pool_after != pool_before.unwrap_or_default()
722            && pool_after.capacity_bytes >= retained_threshold
723            || output_bytes >= native_einsum_pool_trace_min_output_bytes()
724        {
725            let before = pool_before.unwrap_or_default();
726            eprintln!(
727                "native_einsum pool subscripts={subscripts:?} before_buffers={} before_capacity={:.3} MiB after_buffers={} after_capacity={:.3} MiB output_shape={:?} output_bytes={:.3} MiB",
728                before.buffers,
729                before.capacity_bytes as f64 / (1024.0 * 1024.0),
730                pool_after.buffers,
731                pool_after.capacity_bytes as f64 / (1024.0 * 1024.0),
732                result.shape(),
733                output_bytes as f64 / (1024.0 * 1024.0),
734            );
735        }
736    }
737    if reset_native_einsum_engine_after_call() {
738        let before_reset = trace_pool
739            .then(default_engine_buffer_pool_stats)
740            .transpose()?;
741        reset_default_engine()?;
742        if trace_pool
743            && before_reset.unwrap_or_default().capacity_bytes
744                >= native_einsum_pool_trace_min_retained_bytes()
745        {
746            let before = before_reset.unwrap_or_default();
747            let after = default_engine_buffer_pool_stats()?;
748            eprintln!(
749                "native_einsum engine_reset before_buffers={} before_capacity={:.3} MiB after_buffers={} after_capacity={:.3} MiB",
750                before.buffers,
751                before.capacity_bytes as f64 / (1024.0 * 1024.0),
752                after.buffers,
753                after.capacity_bytes as f64 / (1024.0 * 1024.0),
754            );
755        }
756    } else if reset_native_einsum_buffer_pool_after_call() {
757        let before_clear = trace_pool
758            .then(default_engine_buffer_pool_stats)
759            .transpose()?;
760        reset_default_engine_buffer_pool()?;
761        if trace_pool
762            && before_clear.unwrap_or_default().capacity_bytes
763                >= native_einsum_pool_trace_min_retained_bytes()
764        {
765            let before = before_clear.unwrap_or_default();
766            let after = default_engine_buffer_pool_stats()?;
767            eprintln!(
768                "native_einsum pool_reset before_buffers={} before_capacity={:.3} MiB after_buffers={} after_capacity={:.3} MiB",
769                before.buffers,
770                before.capacity_bytes as f64 / (1024.0 * 1024.0),
771                after.buffers,
772                after.capacity_bytes as f64 / (1024.0 * 1024.0),
773            );
774        }
775    }
776    if release_allocator_after_native_einsum_call() {
777        let report = release_process_allocator_cached_memory();
778        if trace_pool && (report.released_bytes.unwrap_or(0) > 0 || report.success == Some(true)) {
779            eprintln!(
780                "native_einsum allocator_pressure_relief supported={} released_bytes={:?} success={:?}",
781                report.supported,
782                report.released_bytes,
783                report.success,
784            );
785        }
786    }
787    Ok(result)
788}
789
790fn cached_einsum_native_tensors(
791    inputs: &[&NativeTensor],
792    subscripts: &EinsumSubscripts,
793) -> Result<NativeTensor> {
794    run_cached_native_einsum(
795        subscripts,
796        |compiler| {
797            compile_native_einsum_program(
798                compiler,
799                inputs.iter().map(|tensor| {
800                    tenferro::program::ProgramInputSpec::new(
801                        tensor.dtype(),
802                        tensor.shape().iter().copied().map(Into::into),
803                    )
804                }),
805                subscripts,
806            )
807        },
808        |context, program| single_output(context.run_graph(program, inputs)),
809    )
810}
811
812fn cached_einsum_native_reads(
813    inputs: &[TensorRead<'_>],
814    subscripts: &Subscripts,
815) -> Result<NativeTensor> {
816    let einsum_subscripts = EinsumSubscripts::from(subscripts);
817    if inputs.first().is_some_and(|first| {
818        concrete_einsum_dtype_supported(first.dtype())
819            && inputs.iter().all(|input| input.dtype() == first.dtype())
820    }) {
821        let plan = cached_concrete_einsum_plan(inputs, subscripts, &einsum_subscripts)?;
822        // The plan reports `tenferro_einsum::Error`, so the session-entry
823        // rejection is mapped here rather than through the tenferro-tensor-typed
824        // session helper.
825        return default_context()
826            .with_session(|engine| plan.execute_read(inputs, engine))
827            .map_err(|entry| {
828                anyhow::Error::new(entry).context("native einsum session entry failed")
829            })?
830            .map_err(|error| anyhow!("native einsum session execution failed: {error}"));
831    }
832
833    run_cached_native_einsum(
834        &einsum_subscripts,
835        |compiler| {
836            compile_native_einsum_program(
837                compiler,
838                inputs.iter().map(|tensor| {
839                    tenferro::program::ProgramInputSpec::new(
840                        tensor.dtype(),
841                        tensor.shape().iter().copied().map(Into::into),
842                    )
843                }),
844                &einsum_subscripts,
845            )
846        },
847        |context, program| single_output(context.run_graph_reads(program, inputs)),
848    )
849    .map_err(|e| anyhow!("native read einsum failed: {e}"))
850}
851
852/// Reduce a graph's output list to the single tensor an einsum produces.
853fn single_output(
854    outputs: std::result::Result<Vec<NativeTensor>, crate::context::CpuExecutionContextError>,
855) -> Result<NativeTensor> {
856    let mut outputs = outputs.map_err(|e| anyhow!("native einsum failed: {e}"))?;
857    if outputs.len() != 1 {
858        return Err(anyhow!(
859            "native einsum returned {} outputs instead of one",
860            outputs.len()
861        ));
862    }
863    outputs
864        .pop()
865        .ok_or_else(|| anyhow!("native einsum returned no output"))
866}
867
868/// Build a dense native tensor from column-major data.
869/// # Errors
870///
871/// Returns an error when the shape product would overflow `usize`,
872/// when `data.len()` does not match the expected shape, or when the backend
873/// rejects the tensor.
874pub fn dense_native_tensor_from_col_major<T: TensorElement>(
875    data: &[T],
876    logical_dims: &[usize],
877) -> Result<NativeTensor> {
878    T::dense_native_tensor_from_col_major(data, logical_dims)
879}
880
881/// Build a dense native tensor by moving an owned column-major payload.
882///
883/// # Arguments
884///
885/// * `data` - Owned column-major values whose length equals the product of
886///   `logical_dims`.
887/// * `logical_dims` - Logical tensor dimensions in column-major axis order.
888///
889/// # Returns
890///
891/// A native dense tensor that owns the supplied payload without cloning it.
892///
893/// # Errors
894///
895/// Returns an error when the shape product would overflow `usize`,
896/// when `data.len()` does not match the expected shape, or when the backend
897/// rejects the tensor.
898pub fn dense_native_tensor_from_col_major_owned<T: TensorElement>(
899    data: Vec<T>,
900    logical_dims: &[usize],
901) -> Result<NativeTensor> {
902    T::dense_native_tensor_from_col_major_owned(data, logical_dims)
903}
904
905/// Borrow column-major data as a read-only native operand without copying.
906///
907/// The returned input owns nothing: it is a [`TensorRead`] view over `data`, so
908/// a shared operand (a frame matrix, a weight matrix) reaches an einsum as a
909/// borrowed read instead of a full-size copy. Use it with
910/// [`einsum_native_tensor_reads`] on the compatibility frontend, or
911/// [`NativeTensorReadInput::as_read`] to feed an explicit session's read route.
912///
913/// # Arguments
914///
915/// * `data` - Borrowed column-major values whose length equals the product of
916///   `logical_dims`.
917/// * `logical_dims` - Logical tensor dimensions in column-major axis order.
918///
919/// # Returns
920///
921/// A borrowed read input that stays valid for `data`'s lifetime.
922///
923/// # Errors
924///
925/// Returns an error when the shape product overflows `usize`, when `data.len()`
926/// does not match the expected shape, or when the view is rejected as invalid.
927///
928/// # Examples
929///
930/// ```
931/// use tensor4all_tensorbackend::{
932///     dense_native_tensor_read_input_from_col_major, einsum_native_tensor_reads,
933/// };
934///
935/// let lhs = [1.0_f64, 2.0, 3.0, 4.0];
936/// let rhs = [5.0_f64, 6.0];
937/// let lhs = dense_native_tensor_read_input_from_col_major(&lhs, &[2, 2])?;
938/// let rhs = dense_native_tensor_read_input_from_col_major(&rhs, &[2, 1])?;
939/// let product = einsum_native_tensor_reads(
940///     &[(&lhs, &[0, 1]), (&rhs, &[1, 2])],
941///     &[0, 2],
942/// )?;
943/// assert_eq!(product.as_slice::<f64>()?, &[23.0, 34.0]);
944/// # Ok::<(), Box<dyn std::error::Error>>(())
945/// ```
946pub fn dense_native_tensor_read_input_from_col_major<'a, T>(
947    data: &'a [T],
948    logical_dims: &[usize],
949) -> Result<NativeTensorReadInput<'a>>
950where
951    T: TensorElement,
952{
953    let view = tenferro::TypedTensorView::from_col_major(logical_dims, data)
954        .map_err(|error| anyhow!("native tensor view rejected: {error}"))?;
955    Ok(NativeTensorReadInput::Borrowed(TensorRead::from_view(
956        <T as TensorScalar>::tensor_view(view),
957    )))
958}
959
960/// Build a dense native tensor whose logical values are diagonal.
961/// # Errors
962///
963/// Returns an error when `logical_rank` is zero, when expanding the diagonal
964/// would overflow `usize`, or when the backend rejects the resulting dense
965/// tensor.
966pub fn diag_native_tensor_from_col_major<T: TensorElement>(
967    data: &[T],
968    logical_rank: usize,
969) -> Result<NativeTensor> {
970    T::diag_native_tensor_from_col_major(data, logical_rank)
971}
972
973/// Convert storage to a dense native tensor.
974///
975/// The native tensor keeps the storage's own scalar dtype: a compact `f32` or
976/// `Complex32` payload becomes an `f32`/`c32` native tensor instead of being
977/// widened to `f64`/`Complex64`.
978/// # Errors
979///
980/// Returns a [`BridgeError`] when materializing `storage` to the requested
981/// dense shape fails (for example a shape mismatch) or when the
982/// dense-to-native conversion fails.
983pub fn storage_to_native_tensor(
984    storage: &Storage,
985    logical_dims: &[usize],
986) -> std::result::Result<NativeTensor, BridgeError> {
987    storage_dtype_dispatch!(storage.dtype(), Payload => {
988        let values = <Payload as StorageScalar>::dense_values(storage, logical_dims)
989            .map_err(|error| anyhow!("dense payload materialization failed: {error}"))?;
990        <Payload as TensorElement>::dense_native_tensor_from_col_major_owned(
991            values,
992            logical_dims,
993        )
994        .map_err(BridgeError::from)
995    },
996    else Err(anyhow!("unsupported storage scalar type").into()))
997}
998
999/// Copy the compact payload as a native tensor of the stored dtype.
1000///
1001/// This visits only referenced payload entries and preserves the stored scalar
1002/// dtype, so it is the dtype-generic payload bridge for compact storage.
1003/// # Errors
1004///
1005/// Returns a [`BridgeError`] when the payload cannot be copied or the
1006/// backend rejects the native tensor.
1007pub fn storage_payload_native(storage: &Storage) -> std::result::Result<NativeTensor, BridgeError> {
1008    storage_dtype_dispatch!(storage.dtype(), Payload => {
1009        let values = <Payload as StorageScalar>::payload_vec(storage)
1010            .map_err(|error| anyhow!("compact payload copy failed: {error}"))?;
1011        native_tensor_from_vec(storage.payload_dims().to_vec(), values)
1012    },
1013    else Err(anyhow!("unsupported storage scalar type").into()))
1014}
1015
1016/// Build structured storage from a native payload, preserving its dtype.
1017///
1018/// `output_payload_dims` and `output_axis_classes` describe the planned compact
1019/// layout. The compact payload keeps the native tensor's scalar dtype, so a
1020/// narrow contraction result stays narrow.
1021/// # Errors
1022///
1023/// Returns a [`BridgeError`] when the payload shape does not match
1024/// `output_payload_dims`, when its dtype has no compact storage representation,
1025/// or when the storage metadata or payload read fails.
1026pub fn storage_from_payload_native(
1027    payload: NativeTensor,
1028    output_payload_dims: &[usize],
1029    output_axis_classes: Vec<usize>,
1030) -> std::result::Result<Storage, BridgeError> {
1031    if payload.shape() != output_payload_dims {
1032        return Err(anyhow!(
1033            "payload shape {:?} does not match planned payload dims {:?}",
1034            payload.shape(),
1035            output_payload_dims
1036        )
1037        .into());
1038    }
1039    let strides = crate::storage::col_major_strides(output_payload_dims)?;
1040    storage_dtype_dispatch!(payload.dtype(), Payload => {
1041        let values = native_slice::<Payload>(&payload, "failed to read structured payload")?.to_vec();
1042        Storage::new_structured(
1043            values,
1044            output_payload_dims.to_vec(),
1045            strides,
1046            output_axis_classes,
1047        )
1048        .map_err(|error| anyhow!("structured payload storage construction failed: {error}").into())
1049    },
1050    else Err(anyhow!(
1051        "unsupported structured payload dtype {:?}; expected a floating or complex floating dtype",
1052        payload.dtype()
1053    )
1054    .into()))
1055}
1056
1057/// Build a read-only native tensor input over the compact storage payload.
1058///
1059/// Contiguous payloads are borrowed without copying. Non-contiguous payloads
1060/// are materialized into an owned native tensor. Both paths preserve the stored
1061/// scalar dtype, so `f32`/`Complex32` payloads are served without widening.
1062/// # Errors
1063///
1064/// Returns a [`BridgeError`] when the storage dtype is not a floating or
1065/// complex floating dtype, when the contiguous payload view cannot be built, or
1066/// when the payload cannot be read into a native tensor.
1067pub fn storage_payload_native_read_input(
1068    storage: &Storage,
1069) -> std::result::Result<NativeTensorReadInput<'_>, BridgeError> {
1070    storage_dtype_dispatch!(storage.dtype(), Payload => {
1071        if let Some(view) = <Payload as StorageScalar>::payload_view(storage)
1072            .map_err(anyhow::Error::msg)?
1073        {
1074            let typed = tenferro::TypedTensorView::from_col_major(storage.payload_dims(), view)
1075                .map_err(|error| BridgeError::from(anyhow::Error::new(error)))?;
1076            return Ok(NativeTensorReadInput::Borrowed(TensorRead::from_view(
1077                <Payload as TensorScalar>::tensor_view(typed),
1078            )));
1079        }
1080        let values = <Payload as StorageScalar>::payload_vec(storage)
1081            .map_err(|error| anyhow!("compact payload copy failed: {error}"))?;
1082        native_tensor_from_vec(storage.payload_dims().to_vec(), values)
1083            .map(NativeTensorReadInput::Owned)
1084    },
1085    else Err(anyhow!("unsupported storage scalar type").into()))
1086}
1087
1088/// Materialize a native tensor into dense storage.
1089/// # Errors
1090///
1091/// Returns a [`BridgeError`] when the tensor has an external scalar kind, when
1092/// reading its host slice fails, or when the dense storage materialization
1093/// fails.
1094pub fn native_tensor_primal_to_storage(
1095    tensor: &NativeTensor,
1096) -> std::result::Result<Storage, BridgeError> {
1097    match tensor.dtype() {
1098        DType::F32 => Storage::from_dense_col_major(
1099            native_slice::<f32>(tensor, "failed to read f32 native tensor")?.to_vec(),
1100            tensor.shape(),
1101        )
1102        .map_err(|e| {
1103            BridgeError::from(anyhow!(
1104                "native tensor snapshot materialization failed: {e}"
1105            ))
1106        }),
1107        DType::F64 => Storage::from_dense_col_major(
1108            native_slice::<f64>(tensor, "failed to read f64 native tensor")?.to_vec(),
1109            tensor.shape(),
1110        )
1111        .map_err(|e| {
1112            BridgeError::from(anyhow!(
1113                "native tensor snapshot materialization failed: {e}"
1114            ))
1115        }),
1116        DType::I32 => Storage::from_dense_col_major(
1117            native_slice::<i32>(tensor, "failed to read i32 native tensor")?
1118                .iter()
1119                .map(|&value| value as f64)
1120                .collect::<Vec<_>>(),
1121            tensor.shape(),
1122        )
1123        .map_err(|e| {
1124            BridgeError::from(anyhow!(
1125                "native tensor snapshot materialization failed: {e}"
1126            ))
1127        }),
1128        DType::I64 => Storage::from_dense_col_major(
1129            native_slice::<i64>(tensor, "failed to read i64 native tensor")?
1130                .iter()
1131                .map(|&value| value as f64)
1132                .collect::<Vec<_>>(),
1133            tensor.shape(),
1134        )
1135        .map_err(|e| {
1136            BridgeError::from(anyhow!(
1137                "native tensor snapshot materialization failed: {e}"
1138            ))
1139        }),
1140        DType::Bool => Storage::from_dense_col_major(
1141            native_slice::<bool>(tensor, "failed to read bool native tensor")?
1142                .iter()
1143                .map(|&value| if value { 1.0 } else { 0.0 })
1144                .collect::<Vec<_>>(),
1145            tensor.shape(),
1146        )
1147        .map_err(|e| {
1148            BridgeError::from(anyhow!(
1149                "native tensor snapshot materialization failed: {e}"
1150            ))
1151        }),
1152        DType::C32 => Storage::from_dense_col_major(
1153            native_slice::<Complex32>(tensor, "failed to read c32 native tensor")?.to_vec(),
1154            tensor.shape(),
1155        )
1156        .map_err(|e| {
1157            BridgeError::from(anyhow!(
1158                "native tensor snapshot materialization failed: {e}"
1159            ))
1160        }),
1161        DType::C64 => Storage::from_dense_col_major(
1162            native_slice::<Complex64>(tensor, "failed to read c64 native tensor")?.to_vec(),
1163            tensor.shape(),
1164        )
1165        .map_err(|e| {
1166            BridgeError::from(anyhow!(
1167                "native tensor snapshot materialization failed: {e}"
1168            ))
1169        }),
1170        DType::External(_) => Err(BridgeError::from(anyhow::Error::new(
1171            tenferro_tensor::Error::unsupported(
1172                "dense storage",
1173                "external scalar kinds cannot be materialized into dense storage",
1174            ),
1175        ))),
1176    }
1177}
1178
1179/// Materialize dense column-major values from a native tensor.
1180/// # Errors
1181///
1182/// Returns a [`BridgeError`] when `tensor`'s dtype does not match `T` or when
1183/// its host data cannot be read.
1184pub fn native_tensor_primal_to_dense_col_major<T: TensorElement>(
1185    tensor: &NativeTensor,
1186) -> std::result::Result<Vec<T>, BridgeError> {
1187    let target = <T as TensorScalar>::dtype();
1188    let tensor_is_real = matches!(
1189        tensor.dtype(),
1190        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool
1191    );
1192    let target_is_real = matches!(
1193        target,
1194        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool
1195    );
1196    if tensor_is_real != target_is_real {
1197        return Err(anyhow!(
1198            "expected {} native tensor, got dtype {:?}",
1199            if target_is_real { "real" } else { "complex" },
1200            tensor.dtype()
1201        )
1202        .into());
1203    }
1204    <T as TensorElement>::dense_values_from_native_col_major(tensor).map_err(BridgeError::from)
1205}
1206
1207/// Materialize diagonal values from a native tensor, promoting to the
1208/// matching real (`f64`) or complex (`Complex64`) dtype.
1209///
1210/// Real native tensors (`f32`, `f64`, `i32`, `i64`, `bool`) are promoted to
1211/// `f64`; complex tensors (`c32`, `c64`) are promoted to `Complex64`. The
1212/// scalar type `T` selects the promoted target.
1213/// # Errors
1214///
1215/// Returns a [`BridgeError`] when the tensor's real/complex kind does not
1216/// match `T`, when conversion to `T` fails, or when the tensor is not
1217/// diagonal-compatible (rank 0 or unequal dimensions).
1218///
1219/// # Examples
1220/// ```
1221/// use tenferro::Tensor as NativeTensor;
1222/// use tensor4all_tensorbackend::native_tensor_primal_to_diag;
1223/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1224/// let native = NativeTensor::from_vec_col_major(vec![3, 3], vec![1.0_f64, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0])?;
1225/// let diag = native_tensor_primal_to_diag::<f64>(&native)?;
1226/// assert_eq!(diag, vec![1.0, 2.0, 3.0]);
1227/// # Ok(())
1228/// # }
1229/// ```
1230pub fn native_tensor_primal_to_diag<T: TensorElement>(
1231    tensor: &NativeTensor,
1232) -> std::result::Result<Vec<T>, BridgeError> {
1233    let promote_to = <T as TensorScalar>::dtype();
1234    let tensor_is_real = matches!(
1235        tensor.dtype(),
1236        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool
1237    );
1238    let target_is_real = matches!(
1239        promote_to,
1240        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool
1241    );
1242    if tensor_is_real != target_is_real {
1243        return Err(anyhow!(
1244            "expected {} native tensor, got dtype {:?}",
1245            if target_is_real { "real" } else { "complex" },
1246            tensor.dtype()
1247        )
1248        .into());
1249    }
1250    let promoted = convert_tensor(tensor, promote_to)?;
1251    <T as TensorElement>::diag_values_from_native_temp(&promoted).map_err(BridgeError::from)
1252}
1253
1254/// Reshape a native tensor without changing its column-major linearization.
1255/// # Errors
1256///
1257/// Fails with a `CpuExecutionContextError` when the default context's engine is
1258/// already borrowed on this thread, or when the shape `logical_dims` and the
1259/// element count of `tensor` do not match.
1260pub fn reshape_col_major_native_tensor(
1261    tensor: &NativeTensor,
1262    logical_dims: &[usize],
1263) -> Result<NativeTensor> {
1264    with_default_session(|session| tensor.reshape(logical_dims, session))
1265        .map_err(|e| anyhow!("native reshape failed: {e}"))
1266}
1267
1268/// Compute a QR decomposition on a native tensor.
1269/// # Errors
1270///
1271/// Returns a [`BridgeError`] when the context rejects the entry or when
1272/// the QR factorization fails.
1273pub fn qr_native_tensor(
1274    tensor: &NativeTensor,
1275) -> std::result::Result<(NativeTensor, NativeTensor), BridgeError> {
1276    let (q, r) = with_default_session(|session| tensor.qr(session))
1277        .map_err(|e| anyhow!("native QR failed: {e}"))?;
1278    Ok((q, r))
1279}
1280
1281/// Compute an SVD on a native tensor.
1282/// # Errors
1283///
1284/// Fails with a `CpuExecutionContextError` when the default context's engine is
1285/// already borrowed on this thread, or when the backend SVD reports a
1286/// non-convergence failure.
1287pub fn svd_native_tensor(
1288    tensor: &NativeTensor,
1289) -> Result<(NativeTensor, NativeTensor, NativeTensor)> {
1290    let (u, s, vt) = with_default_session(|session| tensor.svd(session))
1291        .map_err(|e| anyhow!("native SVD failed: {e}"))?;
1292    Ok((u, s, vt))
1293}
1294
1295/// Sum all elements of a native tensor, returning a dynamic scalar.
1296/// # Errors
1297///
1298/// Returns a [`BridgeError`] when the backend reduction fails, when the
1299/// reduced result cannot read back its host slice, or when the scalar cannot
1300/// be wrapped as a [`BackendScalar`].
1301pub fn sum_native_tensor(tensor: &NativeTensor) -> std::result::Result<BackendScalar, BridgeError> {
1302    let reduced = if tensor.shape().is_empty() {
1303        tensor
1304            .duplicate()
1305            .map_err(|e| anyhow!("native scalar duplication failed: {e}"))?
1306    } else {
1307        let axes: Vec<usize> = (0..tensor.shape().len()).collect();
1308        with_default_session(|session| tensor.reduce_sum(Some(&axes), session))
1309            .map_err(|e| anyhow!("native sum failed: {e}"))?
1310    };
1311    Ok(BackendScalar::from_native(reduced)?)
1312}
1313
1314/// Return the tangent tensor when present.
1315///
1316/// Plain `Tensor` values do not carry tangent storage, so this bridge returns
1317/// `None`.
1318pub fn tangent_native_tensor(_tensor: &NativeTensor) -> Option<NativeTensor> {
1319    None
1320}
1321
1322/// Multiply a native tensor by a dynamic scalar.
1323/// # Errors
1324///
1325/// Returns a [`BridgeError`] when `tensor` or `scalar` cannot be promoted to a
1326/// common dtype, when the dtype is integer, bool, or external, or when the
1327/// backend rejects the scaled tensor.
1328pub fn scale_native_tensor(
1329    tensor: &NativeTensor,
1330    scalar: &BackendScalar,
1331) -> std::result::Result<NativeTensor, BridgeError> {
1332    let target = common_dtype(&[tensor.dtype(), scalar.as_native().dtype()]);
1333    let tensor = convert_tensor(tensor, target)?;
1334    let scalar = promote_scalar_native(scalar.as_native(), target)?;
1335
1336    match target {
1337        DType::F32 => {
1338            let factor = native_slice::<f32>(&scalar, "failed to read promoted f32 scalar")?
1339                .first()
1340                .copied()
1341                .ok_or_else(|| anyhow!("failed to read promoted f32 scalar"))?;
1342            let values = native_slice::<f32>(&tensor, "failed to read promoted f32 tensor")?
1343                .iter()
1344                .map(|&value| value * factor)
1345                .collect::<Vec<_>>();
1346            native_tensor_from_vec(tensor.shape().to_vec(), values)
1347        }
1348        DType::F64 => {
1349            let factor = native_slice::<f64>(&scalar, "failed to read promoted f64 scalar")?
1350                .first()
1351                .copied()
1352                .ok_or_else(|| anyhow!("failed to read promoted f64 scalar"))?;
1353            let values = native_slice::<f64>(&tensor, "failed to read promoted f64 tensor")?
1354                .iter()
1355                .map(|&value| value * factor)
1356                .collect::<Vec<_>>();
1357            native_tensor_from_vec(tensor.shape().to_vec(), values)
1358        }
1359        DType::C32 => {
1360            let factor = native_slice::<Complex32>(&scalar, "failed to read promoted c32 scalar")?
1361                .first()
1362                .copied()
1363                .ok_or_else(|| anyhow!("failed to read promoted c32 scalar"))?;
1364            let values = native_slice::<Complex32>(&tensor, "failed to read promoted c32 tensor")?
1365                .iter()
1366                .map(|&value| value * factor)
1367                .collect::<Vec<_>>();
1368            native_tensor_from_vec(tensor.shape().to_vec(), values)
1369        }
1370        DType::C64 => {
1371            let factor = native_slice::<Complex64>(&scalar, "failed to read promoted c64 scalar")?
1372                .first()
1373                .copied()
1374                .ok_or_else(|| anyhow!("failed to read promoted c64 scalar"))?;
1375            let values = native_slice::<Complex64>(&tensor, "failed to read promoted c64 tensor")?
1376                .iter()
1377                .map(|&value| value * factor)
1378                .collect::<Vec<_>>();
1379            native_tensor_from_vec(tensor.shape().to_vec(), values)
1380        }
1381        DType::I32 | DType::I64 | DType::Bool => {
1382            Err(anyhow!("scale_native_tensor does not support integer/bool tensors").into())
1383        }
1384        DType::External(_) => Err(BridgeError::from(anyhow::Error::new(
1385            tenferro_tensor::Error::unsupported(
1386                "scaling",
1387                "external scalar kinds are not scalable dtypes",
1388            ),
1389        ))),
1390    }
1391}
1392
1393/// Compute `a * lhs + b * rhs`.
1394/// # Errors
1395///
1396/// Returns a [`BridgeError`] when `lhs` and `rhs` have different shapes, when
1397/// the operands cannot be promoted to a common dtype, when the dtype is
1398/// integer, bool, or external, or when the backend rejects the result.
1399pub fn axpby_native_tensor(
1400    lhs: &NativeTensor,
1401    a: &BackendScalar,
1402    rhs: &NativeTensor,
1403    b: &BackendScalar,
1404) -> std::result::Result<NativeTensor, BridgeError> {
1405    if lhs.shape() != rhs.shape() {
1406        return Err(BridgeError::from(anyhow!(
1407            "axpby requires matching tensor shapes, got lhs {:?} and rhs {:?}",
1408            lhs.shape(),
1409            rhs.shape()
1410        )));
1411    }
1412
1413    let target = common_dtype(&[
1414        lhs.dtype(),
1415        rhs.dtype(),
1416        a.as_native().dtype(),
1417        b.as_native().dtype(),
1418    ]);
1419    let lhs = convert_tensor(lhs, target)?;
1420    let rhs = convert_tensor(rhs, target)?;
1421    let a = promote_scalar_native(a.as_native(), target)?;
1422    let b = promote_scalar_native(b.as_native(), target)?;
1423
1424    match target {
1425        DType::F32 => {
1426            let a = native_slice::<f32>(&a, "failed to read promoted f32 scalar a")?
1427                .first()
1428                .copied()
1429                .ok_or_else(|| anyhow!("failed to read promoted f32 scalar a"))?;
1430            let b = native_slice::<f32>(&b, "failed to read promoted f32 scalar b")?
1431                .first()
1432                .copied()
1433                .ok_or_else(|| anyhow!("failed to read promoted f32 scalar b"))?;
1434            let lhs_values = native_slice::<f32>(&lhs, "failed to read promoted f32 lhs")?;
1435            let rhs_values = native_slice::<f32>(&rhs, "failed to read promoted f32 rhs")?;
1436            let values = lhs_values
1437                .iter()
1438                .zip(rhs_values.iter())
1439                .map(|(&x, &y)| a * x + b * y)
1440                .collect::<Vec<_>>();
1441            native_tensor_from_vec(lhs.shape().to_vec(), values)
1442        }
1443        DType::F64 => {
1444            let a = native_slice::<f64>(&a, "failed to read promoted f64 scalar a")?
1445                .first()
1446                .copied()
1447                .ok_or_else(|| anyhow!("failed to read promoted f64 scalar a"))?;
1448            let b = native_slice::<f64>(&b, "failed to read promoted f64 scalar b")?
1449                .first()
1450                .copied()
1451                .ok_or_else(|| anyhow!("failed to read promoted f64 scalar b"))?;
1452            let lhs_values = native_slice::<f64>(&lhs, "failed to read promoted f64 lhs")?;
1453            let rhs_values = native_slice::<f64>(&rhs, "failed to read promoted f64 rhs")?;
1454            let values = lhs_values
1455                .iter()
1456                .zip(rhs_values.iter())
1457                .map(|(&x, &y)| a * x + b * y)
1458                .collect::<Vec<_>>();
1459            native_tensor_from_vec(lhs.shape().to_vec(), values)
1460        }
1461        DType::C32 => {
1462            let a = native_slice::<Complex32>(&a, "failed to read promoted c32 scalar a")?
1463                .first()
1464                .copied()
1465                .ok_or_else(|| anyhow!("failed to read promoted c32 scalar a"))?;
1466            let b = native_slice::<Complex32>(&b, "failed to read promoted c32 scalar b")?
1467                .first()
1468                .copied()
1469                .ok_or_else(|| anyhow!("failed to read promoted c32 scalar b"))?;
1470            let lhs_values = native_slice::<Complex32>(&lhs, "failed to read promoted c32 lhs")?;
1471            let rhs_values = native_slice::<Complex32>(&rhs, "failed to read promoted c32 rhs")?;
1472            let values = lhs_values
1473                .iter()
1474                .zip(rhs_values.iter())
1475                .map(|(&x, &y)| a * x + b * y)
1476                .collect::<Vec<_>>();
1477            native_tensor_from_vec(lhs.shape().to_vec(), values)
1478        }
1479        DType::C64 => {
1480            let a = native_slice::<Complex64>(&a, "failed to read promoted c64 scalar a")?
1481                .first()
1482                .copied()
1483                .ok_or_else(|| anyhow!("failed to read promoted c64 scalar a"))?;
1484            let b = native_slice::<Complex64>(&b, "failed to read promoted c64 scalar b")?
1485                .first()
1486                .copied()
1487                .ok_or_else(|| anyhow!("failed to read promoted c64 scalar b"))?;
1488            let lhs_values = native_slice::<Complex64>(&lhs, "failed to read promoted c64 lhs")?;
1489            let rhs_values = native_slice::<Complex64>(&rhs, "failed to read promoted c64 rhs")?;
1490            let values = lhs_values
1491                .iter()
1492                .zip(rhs_values.iter())
1493                .map(|(&x, &y)| a * x + b * y)
1494                .collect::<Vec<_>>();
1495            native_tensor_from_vec(lhs.shape().to_vec(), values)
1496        }
1497        DType::I32 | DType::I64 | DType::Bool => {
1498            Err(anyhow!("axpby_native_tensor does not support integer/bool tensors").into())
1499        }
1500        DType::External(_) => Err(BridgeError::from(anyhow::Error::new(
1501            tenferro_tensor::Error::unsupported(
1502                "axpby",
1503                "external scalar kinds are not axpby dtypes",
1504            ),
1505        ))),
1506    }
1507}
1508
1509/// Execute a cached einsum over owned native tensors.
1510///
1511/// This is the consuming bridge used by higher-level owned contraction APIs.
1512/// Inputs are promoted to a common dtype before tenferro evaluates the
1513/// contraction. Repeated calls with the same equation and shapes reuse
1514/// tenferro's process-global contraction path cache.
1515///
1516/// # Arguments
1517/// * `operands` - Native tensors paired with numeric einsum labels for each axis.
1518/// * `output_ids` - Numeric labels to keep in the result, in output axis order.
1519///
1520/// # Returns
1521/// The contracted native tensor in the promoted common dtype.
1522///
1523/// # Errors
1524/// Returns an error if the operand list is empty, any label list length does
1525/// not match its tensor rank, label generation exceeds the supported range, or
1526/// the backend contraction fails.
1527///
1528/// # Examples
1529/// ```
1530/// use tensor4all_tensorbackend::einsum_native_tensors_owned;
1531/// use tenferro::Tensor as NativeTensor;
1532/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1533///
1534/// let lhs = NativeTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6])?;
1535/// let rhs = NativeTensor::from_vec_col_major(vec![3, 2], vec![1.0_f64; 6])?;
1536/// let result = einsum_native_tensors_owned(vec![(lhs, vec![0, 1]), (rhs, vec![1, 2])], &[0, 2])?;
1537///
1538/// assert_eq!(result.shape(), &[2, 2]);
1539/// assert_eq!(result.as_slice::<f64>()?, &[3.0, 3.0, 3.0, 3.0]);
1540/// # Ok(())
1541/// # }
1542/// ```
1543pub fn einsum_native_tensors_owned(
1544    operands: Vec<(NativeTensor, Vec<usize>)>,
1545    output_ids: &[usize],
1546) -> Result<NativeTensor> {
1547    ensure!(
1548        !operands.is_empty(),
1549        "native einsum requires at least one operand"
1550    );
1551
1552    let target = common_dtype(
1553        &operands
1554            .iter()
1555            .map(|(tensor, _)| tensor.dtype())
1556            .collect::<Vec<_>>(),
1557    );
1558
1559    let output_ids_u32 = checked_native_einsum_labels(output_ids)?;
1560    let mut converted = Vec::with_capacity(operands.len());
1561    let mut input_ids = Vec::with_capacity(operands.len());
1562    for (tensor, ids) in operands {
1563        ensure!(
1564            tensor.shape().len() == ids.len(),
1565            "einsum id list {:?} does not match tensor shape {:?}",
1566            ids,
1567            tensor.shape()
1568        );
1569        let checked_ids = checked_native_einsum_labels(&ids)?;
1570        let tensor = if tensor.dtype() == target {
1571            tensor
1572        } else {
1573            convert_tensor(&tensor, target)?
1574        };
1575        input_ids.push(checked_ids);
1576        converted.push(tensor);
1577    }
1578
1579    let input_slices = input_ids.iter().map(Vec::as_slice).collect::<Vec<_>>();
1580    let subscripts = EinsumSubscripts::new(&input_slices, &output_ids_u32);
1581
1582    let input_refs = converted.iter().collect::<Vec<_>>();
1583    let trace_operands = input_refs
1584        .iter()
1585        .zip(input_ids.iter())
1586        .map(|(tensor, ids)| (*tensor, ids.as_slice()))
1587        .collect::<Vec<_>>();
1588    maybe_trace_native_einsum_path(NativeEinsumPath::Owned, &trace_operands, &output_ids_u32);
1589    let started = Instant::now();
1590    let result = cached_einsum_native_tensors(&input_refs, &subscripts)?;
1591    record_native_einsum_profile(
1592        NativeEinsumPath::Owned,
1593        &trace_operands,
1594        &output_ids_u32,
1595        started.elapsed(),
1596    );
1597    Ok(result)
1598}
1599
1600/// Execute a cached einsum over borrowed native tensors.
1601///
1602/// Inputs are promoted to a common dtype before contraction. Operands that
1603/// already have the target dtype are passed to the backend by reference;
1604/// operands with another dtype are converted into temporary native tensors and
1605/// then borrowed for the contraction. Repeated calls with the same equation
1606/// and shapes reuse tenferro's process-global contraction path cache.
1607///
1608/// # Arguments
1609/// * `operands` - Native tensors paired with numeric einsum labels for each axis.
1610///
1611///   Each label slice must have the same length as the corresponding tensor rank.
1612/// * `output_ids` - Numeric labels to keep in the result, in output axis order.
1613///
1614/// # Returns
1615/// The contracted native tensor in the promoted common dtype.
1616///
1617/// # Errors
1618/// Returns an error if the operand list is empty, any label list length does
1619/// not match its tensor rank, label generation exceeds the supported range,
1620/// dtype conversion fails, or the backend contraction fails.
1621///
1622/// # Examples
1623/// ```
1624/// use tensor4all_tensorbackend::einsum_native_tensors;
1625/// use tenferro::Tensor as NativeTensor;
1626/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1627///
1628/// let lhs = NativeTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6])?;
1629/// let rhs = NativeTensor::from_vec_col_major(vec![3, 2], vec![1.0_f64; 6])?;
1630/// let result = einsum_native_tensors(&[(&lhs, &[0, 1]), (&rhs, &[1, 2])], &[0, 2])?;
1631///
1632/// assert_eq!(result.shape(), &[2, 2]);
1633/// assert_eq!(result.as_slice::<f64>()?, &[3.0, 3.0, 3.0, 3.0]);
1634/// # Ok(())
1635/// # }
1636/// ```
1637pub fn einsum_native_tensors(
1638    operands: &[(&NativeTensor, &[usize])],
1639    output_ids: &[usize],
1640) -> Result<NativeTensor> {
1641    ensure!(
1642        !operands.is_empty(),
1643        "native einsum requires at least one operand"
1644    );
1645
1646    let target = common_dtype(
1647        &operands
1648            .iter()
1649            .map(|(tensor, _)| tensor.dtype())
1650            .collect::<Vec<_>>(),
1651    );
1652    let output_ids_u32 = checked_native_einsum_labels(output_ids)?;
1653    let mut converted = Vec::with_capacity(operands.len());
1654    let mut input_ids = Vec::with_capacity(operands.len());
1655    let mut has_conversions = false;
1656    let started = Instant::now();
1657
1658    for (tensor, ids) in operands {
1659        ensure!(
1660            tensor.shape().len() == ids.len(),
1661            "einsum id list {:?} does not match tensor shape {:?}",
1662            ids,
1663            tensor.shape()
1664        );
1665        input_ids.push(checked_native_einsum_labels(ids)?);
1666        if tensor.dtype() == target {
1667            converted.push(None);
1668        } else {
1669            converted.push(Some(convert_tensor(tensor, target)?));
1670            has_conversions = true;
1671        }
1672    }
1673
1674    let input_slices = input_ids.iter().map(Vec::as_slice).collect::<Vec<_>>();
1675    let subscripts = EinsumSubscripts::new(&input_slices, &output_ids_u32);
1676    let input_refs = operands
1677        .iter()
1678        .zip(converted.iter())
1679        .map(|((tensor, _), converted)| converted.as_ref().unwrap_or(*tensor))
1680        .collect::<Vec<_>>();
1681    let trace_path = if has_conversions {
1682        NativeEinsumPath::BorrowedWithConversions
1683    } else {
1684        NativeEinsumPath::Borrowed
1685    };
1686    let trace_operands = input_refs
1687        .iter()
1688        .zip(input_ids.iter())
1689        .map(|(tensor, ids)| (*tensor, ids.as_slice()))
1690        .collect::<Vec<_>>();
1691    maybe_trace_native_einsum_path(trace_path, &trace_operands, &output_ids_u32);
1692    let result = cached_einsum_native_tensors(&input_refs, &subscripts)?;
1693    record_native_einsum_profile(
1694        trace_path,
1695        &trace_operands,
1696        &output_ids_u32,
1697        started.elapsed(),
1698    );
1699    Ok(result)
1700}
1701
1702/// Execute a cached einsum over borrowed native tensors using string subscripts.
1703///
1704/// This is the shared string-notation entry: it parses `subscripts` into the
1705/// numeric labels that [`einsum_native_tensors`] consumes, checks that the
1706/// equation describes exactly the supplied operands, and then delegates the
1707/// contraction. Use it instead of re-implementing the parsing plus label
1708/// mapping in each crate; use [`einsum_native_tensors`] directly when the
1709/// labels are already numeric.
1710///
1711/// # Arguments
1712///
1713/// * `subscripts` - NumPy/PyTorch-style einsum notation such as `"ij,jk->ik"`
1714///   or `"ii->"` for a trace. Parenthesized contraction order is rejected; see
1715///   [`tenferro_einsum::Subscripts::parse`].
1716/// * `operands` - One native tensor per comma-separated input term, in term
1717///   order. Ranks must match the term lengths and repeated labels must have
1718///   equal extents.
1719///
1720/// # Returns
1721///
1722/// The contracted native tensor in the promoted common dtype of the operands.
1723///
1724/// # Errors
1725///
1726/// Returns an error when the notation parser rejects `subscripts` as invalid
1727/// notation, when the operand count does not match the number of comma-separated
1728/// input terms, when an input term's label count does not match its operand
1729/// rank, when repeated or contracted labels have incompatible shapes, or when
1730/// dtype promotion or the backend contraction fails.
1731///
1732/// # Compatibility frontend
1733///
1734/// Like [`einsum_native_tensors`], this runs on the retained process-global
1735/// (compatibility) frontend of
1736/// [tensor4all-rs#859](https://github.com/tensor4all/tensor4all-rs/issues/859):
1737/// it selects the default execution context and pays one graph dispatch per
1738/// call. It is meant for cold or per-stage paths, not per-element hot loops;
1739/// use [`CpuExecutionContext`](crate::CpuExecutionContext) sessions for those.
1740///
1741/// # Examples
1742///
1743/// ```
1744/// use tensor4all_tensorbackend::einsum_native_subscripts;
1745/// use tenferro::Tensor as NativeTensor;
1746/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
1747///
1748/// // Column-major A = [[1, 2], [3, 4]] and B = [[5, 6], [7, 8]].
1749/// let lhs = NativeTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 3.0, 2.0, 4.0])?;
1750/// let rhs = NativeTensor::from_vec_col_major(vec![2, 2], vec![5.0_f64, 7.0, 6.0, 8.0])?;
1751/// let product = einsum_native_subscripts("ij,jk->ik", &[&lhs, &rhs])?;
1752/// assert_eq!(product.shape(), &[2, 2]);
1753/// assert_eq!(product.as_slice::<f64>()?, &[19.0, 43.0, 22.0, 50.0]);
1754///
1755/// let trace = einsum_native_subscripts("ii->", &[&lhs])?;
1756/// assert_eq!(trace.as_slice::<f64>()?, &[5.0]);
1757/// # Ok(())
1758/// # }
1759/// ```
1760pub fn einsum_native_subscripts(
1761    subscripts: &str,
1762    operands: &[&NativeTensor],
1763) -> Result<NativeTensor> {
1764    let parsed = Subscripts::parse(subscripts)
1765        .map_err(|error| anyhow!("invalid einsum subscripts '{subscripts}': {error}"))?;
1766    ensure!(
1767        parsed.inputs.len() == operands.len(),
1768        "einsum subscripts '{subscripts}' describe {} input term(s) but {} operand(s) were given",
1769        parsed.inputs.len(),
1770        operands.len()
1771    );
1772
1773    let input_ids = parsed
1774        .inputs
1775        .iter()
1776        .map(|ids| ids.iter().map(|&id| id as usize).collect::<Vec<_>>())
1777        .collect::<Vec<_>>();
1778    let output_ids = parsed
1779        .output
1780        .iter()
1781        .map(|&id| id as usize)
1782        .collect::<Vec<_>>();
1783    let operand_refs = operands
1784        .iter()
1785        .zip(input_ids.iter())
1786        .map(|(tensor, ids)| (*tensor, ids.as_slice()))
1787        .collect::<Vec<_>>();
1788
1789    einsum_native_tensors(&operand_refs, &output_ids)
1790}
1791
1792/// Execute a cached einsum over read-only native tensor inputs.
1793///
1794/// Backends consume borrowed host views inside their execution session. Mixed
1795/// dtypes are promoted by `Convert` nodes in the compiled einsum graph, so
1796/// non-contiguous operands remain borrowed until runtime execution.
1797/// # Errors
1798///
1799/// Returns an error when `operands` is empty, when a tensor rank does not
1800/// match its id list length, when a label is out of bounds for the supported
1801/// range, or when the backend einsum fails.
1802pub fn einsum_native_tensor_reads(
1803    operands: &[(&NativeTensorReadInput<'_>, &[usize])],
1804    output_ids: &[usize],
1805) -> Result<NativeTensor> {
1806    ensure!(
1807        !operands.is_empty(),
1808        "native einsum requires at least one operand"
1809    );
1810
1811    let output_ids_u32 = checked_native_einsum_labels(output_ids)?;
1812    let mut input_ids = Vec::with_capacity(operands.len());
1813    let mut read_inputs = Vec::with_capacity(operands.len());
1814
1815    for (tensor, ids) in operands {
1816        ensure!(
1817            tensor.shape().len() == ids.len(),
1818            "einsum id list {:?} does not match tensor shape {:?}",
1819            ids,
1820            tensor.shape()
1821        );
1822        input_ids.push(checked_native_einsum_labels(ids)?);
1823        read_inputs.push(tensor.as_read());
1824    }
1825
1826    let subscripts = Subscripts {
1827        inputs: input_ids,
1828        output: output_ids_u32,
1829    };
1830    cached_einsum_native_reads(&read_inputs, &subscripts)
1831}
1832
1833/// Permute axes of a native tensor.
1834/// # Errors
1835///
1836/// Returns a [`BridgeError`] when `perm` is not a permutation of `tensor`'s
1837/// axes or when the backend transpose fails.
1838pub fn permute_native_tensor(
1839    tensor: &NativeTensor,
1840    perm: &[usize],
1841) -> std::result::Result<NativeTensor, BridgeError> {
1842    with_default_session(|session| tensor.transpose(perm, session))
1843        .map_err(|e| anyhow!("native permute failed: {e}"))
1844        .map_err(BridgeError::from)
1845}
1846
1847/// Contract two native tensors along matching axes.
1848/// # Errors
1849///
1850/// Returns an error when `axes_a`/`axes_b` are out of bounds or have differing
1851/// lengths, when the contracted dimensions do not match, or when the backend
1852/// einsum fails.
1853pub fn contract_native_tensor(
1854    lhs: &NativeTensor,
1855    axes_a: &[usize],
1856    rhs: &NativeTensor,
1857    axes_b: &[usize],
1858) -> Result<NativeTensor> {
1859    let (lhs_ids, rhs_ids, output_ids) =
1860        build_binary_einsum_ids(lhs.shape().len(), axes_a, rhs.shape().len(), axes_b)?;
1861    let lhs_ids_usize = lhs_ids.iter().map(|&id| id as usize).collect::<Vec<_>>();
1862    let rhs_ids_usize = rhs_ids.iter().map(|&id| id as usize).collect::<Vec<_>>();
1863    let output_ids_usize = output_ids.iter().map(|&id| id as usize).collect::<Vec<_>>();
1864    let operands = [
1865        (lhs, lhs_ids_usize.as_slice()),
1866        (rhs, rhs_ids_usize.as_slice()),
1867    ];
1868    einsum_native_tensors(&operands, &output_ids_usize)
1869}
1870
1871/// Compute the outer product of two native tensors.
1872/// # Errors
1873///
1874/// Returns a [`BridgeError`] when the underlying einsum contraction fails.
1875pub fn outer_product_native_tensor(
1876    lhs: &NativeTensor,
1877    rhs: &NativeTensor,
1878) -> std::result::Result<NativeTensor, BridgeError> {
1879    contract_native_tensor(lhs, &[], rhs, &[]).map_err(BridgeError::from)
1880}
1881
1882/// Conjugate a native tensor.
1883/// # Errors
1884///
1885/// Returns a [`BridgeError`] when the dtype is external, when duplicating a
1886/// real tensor fails, or when the host slice of a complex tensor cannot be
1887/// read.
1888pub fn conj_native_tensor(tensor: &NativeTensor) -> std::result::Result<NativeTensor, BridgeError> {
1889    match tensor.dtype() {
1890        DType::F32 | DType::F64 | DType::I32 | DType::I64 | DType::Bool => tensor
1891            .duplicate()
1892            .map_err(|e| anyhow!("native tensor duplication failed: {e}"))
1893            .map_err(BridgeError::from),
1894        DType::External(_) => Err(BridgeError::from(anyhow::Error::new(
1895            tenferro_tensor::Error::unsupported(
1896                "conjugation",
1897                "external scalar kinds have no conjugation",
1898            ),
1899        ))),
1900        DType::C32 => native_tensor_from_vec(
1901            tensor.shape().to_vec(),
1902            native_slice::<Complex32>(tensor, "failed to read c32 native tensor")?
1903                .iter()
1904                .map(|&value| value.conj())
1905                .collect::<Vec<_>>(),
1906        ),
1907        DType::C64 => native_tensor_from_vec(
1908            tensor.shape().to_vec(),
1909            native_slice::<Complex64>(tensor, "failed to read c64 native tensor")?
1910                .iter()
1911                .map(|&value| value.conj())
1912                .collect::<Vec<_>>(),
1913        ),
1914    }
1915}
1916
1917/// Permute storage by round-tripping through native tensors.
1918/// # Errors
1919///
1920/// Returns a [`BridgeError`] when the storage-to-native conversion, the
1921/// permutation, or the native-to-storage conversion fails.
1922pub fn permute_storage_native(
1923    storage: &Storage,
1924    logical_dims: &[usize],
1925    perm: &[usize],
1926) -> std::result::Result<Storage, BridgeError> {
1927    let native = storage_to_native_tensor(storage, logical_dims)?;
1928    let permuted = permute_native_tensor(&native, perm)?;
1929    native_tensor_primal_to_storage(&permuted)
1930}
1931
1932/// Contract storages via native tensors.
1933/// # Errors
1934///
1935/// Returns a [`BridgeError`] when either storage-to-native conversion, the
1936/// contraction, or the native-to-storage conversion fails.
1937pub fn contract_storage_native(
1938    storage_a: &Storage,
1939    dims_a: &[usize],
1940    axes_a: &[usize],
1941    storage_b: &Storage,
1942    dims_b: &[usize],
1943    axes_b: &[usize],
1944    _result_dims: &[usize],
1945) -> std::result::Result<Storage, BridgeError> {
1946    let lhs = storage_to_native_tensor(storage_a, dims_a)?;
1947    let rhs = storage_to_native_tensor(storage_b, dims_b)?;
1948    let result = contract_native_tensor(&lhs, axes_a, &rhs, axes_b)?;
1949    native_tensor_primal_to_storage(&result)
1950}
1951
1952/// Outer-product storages via native tensors.
1953/// # Errors
1954///
1955/// Returns a [`BridgeError`] when either storage-to-native conversion, the
1956/// outer product, or the native-to-storage conversion fails.
1957pub fn outer_product_storage_native(
1958    lhs: &Storage,
1959    lhs_dims: &[usize],
1960    rhs: &Storage,
1961    rhs_dims: &[usize],
1962    _result_dims: &[usize],
1963) -> std::result::Result<Storage, BridgeError> {
1964    let lhs = storage_to_native_tensor(lhs, lhs_dims)?;
1965    let rhs = storage_to_native_tensor(rhs, rhs_dims)?;
1966    let result = outer_product_native_tensor(&lhs, &rhs)?;
1967    native_tensor_primal_to_storage(&result)
1968}
1969
1970/// Scale storage by a scalar via native tensors.
1971/// # Errors
1972///
1973/// Returns a [`BridgeError`] when the storage-to-native conversion, the
1974/// scaling, or the native-to-storage conversion fails.
1975pub fn scale_storage_native(
1976    storage: &Storage,
1977    logical_dims: &[usize],
1978    scalar: &BackendScalar,
1979) -> std::result::Result<Storage, BridgeError> {
1980    let native = storage_to_native_tensor(storage, logical_dims)?;
1981    let scaled = scale_native_tensor(&native, scalar)?;
1982    native_tensor_primal_to_storage(&scaled)
1983}
1984
1985/// Compute `a * lhs + b * rhs` over storages via native tensors.
1986/// # Errors
1987///
1988/// Returns a [`BridgeError`] when either storage-to-native conversion, the
1989/// axpby, or the native-to-storage conversion fails.
1990pub fn axpby_storage_native(
1991    lhs: &Storage,
1992    lhs_dims: &[usize],
1993    a: &BackendScalar,
1994    rhs: &Storage,
1995    rhs_dims: &[usize],
1996    b: &BackendScalar,
1997) -> std::result::Result<Storage, BridgeError> {
1998    let lhs = storage_to_native_tensor(lhs, lhs_dims)?;
1999    let rhs = storage_to_native_tensor(rhs, rhs_dims)?;
2000    let combined = axpby_native_tensor(&lhs, a, &rhs, b)?;
2001    native_tensor_primal_to_storage(&combined)
2002}
2003
2004#[cfg(test)]
2005mod tests;