1use 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#[derive(Debug, thiserror::Error)]
39#[error("native tensor bridge operation failed: {source}")]
40pub struct BridgeError {
41 #[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#[allow(clippy::large_enum_variant)]
74pub enum NativeTensorReadInput<'a> {
75 Borrowed(TensorRead<'a>),
77 Owned(NativeTensor),
79}
80
81impl<'a> NativeTensorReadInput<'a> {
82 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 pub fn dtype(&self) -> DType {
92 match self {
93 Self::Borrowed(read) => read.dtype(),
94 Self::Owned(tensor) => tensor.dtype(),
95 }
96 }
97
98 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 = 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 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 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 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
576pub 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
582pub 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 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 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
852fn 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
868pub 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
881pub 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
905pub 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
960pub 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
973pub 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
999pub 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
1016pub 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
1057pub 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
1088pub 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
1179pub 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
1207pub 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
1254pub 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
1268pub 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
1281pub 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
1295pub 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
1314pub fn tangent_native_tensor(_tensor: &NativeTensor) -> Option<NativeTensor> {
1319 None
1320}
1321
1322pub 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
1393pub 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
1509pub 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
1600pub 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
1702pub 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
1792pub 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
1833pub 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
1847pub 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
1871pub 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
1882pub 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
1917pub 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
1932pub 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
1952pub 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
1970pub 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
1985pub 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;