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