tenferro_runtime/traced.rs
1use std::collections::HashMap;
2use std::error::Error as StdError;
3use std::fmt;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Arc;
6
7use computegraph::graph::{Graph, GraphBuilder};
8use computegraph::types::{OperationRole, ValueKey, ValueRef};
9use computegraph::LocalValueId;
10use num_complex::{Complex32, Complex64};
11use tenferro_ops::ad::context::GlobalMetadataScope;
12use tenferro_ops::broadcast::{
13 broadcast_error_to_validation, broadcast_in_dim_extent_error, broadcast_input_plan,
14 broadcast_shape, broadcast_shapes, BroadcastError,
15};
16use tenferro_ops::dim_expr::DimExpr;
17use tenferro_ops::input_key::TensorInputKey;
18use tenferro_ops::std_tensor_op::StdTensorOp;
19use tenferro_ops::TensorMeta;
20use tenferro_tensor::{
21 CompareDir, DType, DotGeneralConfig, Error as TensorError, GatherConfig, IntoShapeVec,
22 PadConfig, ScatterConfig, ShapeMismatch, SliceConfig, Tensor, TensorScalar, TensorValue,
23 ValidationError,
24};
25
26use super::error::{Error, ErrorPhase, Result};
27use super::sym_dim::SymDim;
28use crate::checkpoint::{CheckpointNode, RetainedInputMap, RetainedValue};
29use crate::metadata::{
30 concrete_tensor_meta, register_scoped_graph_metadata, register_scoped_value_metadata,
31 symbolic_input_meta, tensor_meta, MetadataScopeChain,
32};
33use crate::scalar_semantics::{bool_from_real_for_op, round_real_to_i32_for_op, round_real_to_i64};
34use crate::shape_constraint::ConstraintScopeChain;
35
36static NEXT_INPUT_ID: AtomicU64 = AtomicU64::new(0);
37static NEXT_TRACED_ID: AtomicU64 = AtomicU64::new(0);
38
39pub type TracedTensorId = u64;
40
41pub(crate) fn next_input_key() -> TensorInputKey {
42 TensorInputKey::User {
43 id: NEXT_INPUT_ID.fetch_add(1, Ordering::Relaxed),
44 }
45}
46
47pub(crate) fn next_traced_id() -> TracedTensorId {
48 NEXT_TRACED_ID.fetch_add(1, Ordering::Relaxed)
49}
50
51type TracedInputMap = RetainedInputMap;
52
53#[derive(Clone)]
54pub struct TracedTensor {
55 pub id: TracedTensorId,
56 pub rank: usize,
57 pub dtype: DType,
58 pub(crate) graph: Arc<Graph<StdTensorOp>>,
59 pub val: LocalValueId,
60 pub(crate) data: Option<Arc<RetainedValue>>,
61 pub(crate) shape_hint: Option<Vec<SymDim>>,
62 pub(crate) inputs_map: Arc<TracedInputMap>,
63 /// Retained construction-time metadata per bound leaf input key.
64 ///
65 /// Populated by data-attached leaf constructors with the exact
66 /// `TensorMeta` they register (symbolic `tensor_axis` extents for
67 /// [`TracedTensor::from_shared_tensor_value_symbolic_shape`]), so the
68 /// deferred eager-AD analysis can seed symbolic leaf metadata without
69 /// deriving concrete extents from the bound values.
70 pub(crate) leaf_metas: Arc<HashMap<TensorInputKey, TensorMeta>>,
71 pub(crate) extra_roots: Vec<Arc<Graph<StdTensorOp>>>,
72 pub(crate) checkpoint_chain: Option<Arc<CheckpointNode>>,
73 pub(crate) metadata_scopes: MetadataScopeChain,
74 pub(crate) constraint_scopes: ConstraintScopeChain,
75}
76
77impl fmt::Debug for TracedTensor {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 f.debug_struct("TracedTensor")
80 .field("id", &self.id)
81 .field("rank", &self.rank)
82 .field("dtype", &self.dtype)
83 .field("val", &self.val)
84 .field("shape_hint", &self.shape_hint)
85 .field("has_data", &self.data.is_some())
86 .finish_non_exhaustive()
87 }
88}
89
90pub(crate) fn merge_traced_inputs_map<'a>(
91 inputs: impl IntoIterator<Item = &'a TracedTensor>,
92) -> Arc<TracedInputMap> {
93 let maps: Vec<_> = inputs
94 .into_iter()
95 .map(|input| &input.inputs_map)
96 .filter(|map| !map.is_empty())
97 .collect();
98 match maps.as_slice() {
99 [] => return Arc::new(HashMap::new()),
100 [single] => return Arc::clone(*single),
101 _ => {}
102 }
103
104 for &candidate in &maps {
105 if input_map_matches_ordered_merge(candidate.as_ref(), &maps) {
106 return Arc::clone(candidate);
107 }
108 }
109
110 let mut merged = (**maps[0]).clone();
111 for map in maps.iter().skip(1) {
112 merged.extend(
113 map.iter()
114 .map(|(key, tensor)| (key.clone(), tensor.clone())),
115 );
116 }
117 Arc::new(merged)
118}
119
120fn input_map_matches_ordered_merge(
121 candidate: &TracedInputMap,
122 maps: &[&Arc<TracedInputMap>],
123) -> bool {
124 merged_map_matches_ordered(candidate, maps, Arc::ptr_eq)
125}
126
127/// Merge the retained leaf-metadata maps of several traced tensors into one.
128///
129/// Sibling of [`merge_traced_inputs_map`]: the merged map covers exactly the
130/// same leaf keys as the merged bindings map, mapping each to the
131/// construction-time `TensorMeta` its leaf registered (symbolic for symbolic
132/// leaves).
133pub(crate) fn merge_traced_leaf_metas<'a>(
134 inputs: impl IntoIterator<Item = &'a TracedTensor>,
135) -> Arc<HashMap<TensorInputKey, TensorMeta>> {
136 let maps: Vec<_> = inputs
137 .into_iter()
138 .map(|input| &input.leaf_metas)
139 .filter(|map| !map.is_empty())
140 .collect();
141 match maps.as_slice() {
142 [] => return Arc::new(HashMap::new()),
143 [single] => return Arc::clone(*single),
144 _ => {}
145 }
146
147 for &candidate in &maps {
148 if merged_map_matches_ordered(candidate.as_ref(), &maps, |a, b| a == b) {
149 return Arc::clone(candidate);
150 }
151 }
152
153 let mut merged = (**maps[0]).clone();
154 for map in maps.iter().skip(1) {
155 merged.extend(map.iter().map(|(key, meta)| (key.clone(), meta.clone())));
156 }
157 Arc::new(merged)
158}
159
160fn merged_map_matches_ordered<V>(
161 candidate: &HashMap<TensorInputKey, V>,
162 maps: &[&Arc<HashMap<TensorInputKey, V>>],
163 matches: impl Fn(&V, &V) -> bool,
164) -> bool {
165 for map in maps {
166 for key in map.keys() {
167 let Some(final_value) = maps.iter().rev().find_map(|source| source.get(key)) else {
168 return false;
169 };
170 let Some(candidate_value) = candidate.get(key) else {
171 return false;
172 };
173 if !matches(candidate_value, final_value) {
174 return false;
175 }
176 }
177 }
178 true
179}
180
181pub(crate) fn try_concrete_shape(tensor: &TracedTensor) -> Option<Vec<usize>> {
182 tensor
183 .shape_hint
184 .as_ref()?
185 .iter()
186 .map(SymDim::constant_value)
187 .collect()
188}
189
190fn graph_validation(op: &'static str, source: impl Into<ValidationError>) -> Error {
191 Error::validation(op, ErrorPhase::GraphBuild, source.into())
192}
193
194fn graph_invalid_argument(
195 op: &'static str,
196 argument: &'static str,
197 message: impl Into<String>,
198) -> Error {
199 graph_validation(
200 op,
201 ValidationError::InvalidArgument {
202 argument,
203 message: message.into(),
204 },
205 )
206}
207
208fn graph_broadcast_error(op: &'static str, error: BroadcastError) -> Error {
209 graph_validation(op, broadcast_error_to_validation(error))
210}
211
212fn graph_tensor_error(op: &'static str, error: TensorError) -> Error {
213 match error {
214 TensorError::Validation { source, .. } => graph_validation(op, source),
215 other => Error::TensorRuntime(other),
216 }
217}
218
219fn graph_error_with_context(op: &'static str, error: Error) -> Error {
220 match error {
221 Error::Validation { source, .. } => graph_validation(op, source),
222 other => other,
223 }
224}
225
226pub(crate) fn concrete_shape(tensor: &TracedTensor) -> Result<Vec<usize>> {
227 tensor
228 .shape_hint
229 .as_ref()
230 .ok_or_else(|| {
231 graph_invalid_argument(
232 "TracedTensor::concrete_shape",
233 "shape",
234 format!("missing shape hint for traced tensor {}", tensor.id),
235 )
236 })?
237 .iter()
238 .map(|dim| {
239 dim.constant_value().ok_or_else(|| {
240 graph_invalid_argument(
241 "TracedTensor::concrete_shape",
242 "shape",
243 format!("symbolic dimension in shape hint for tensor {}", tensor.id),
244 )
245 })
246 })
247 .collect()
248}
249
250/// Broadcast a traced tensor to `target_shape`.
251///
252/// Expanding singleton axes are first reshaped away so the existing
253/// `BroadcastInDim` transpose rule reduces them correctly during VJP.
254pub(crate) fn broadcast_to(tensor: &TracedTensor, target_shape: &[usize]) -> Result<TracedTensor> {
255 let tensor_shape = concrete_shape(tensor)?;
256 if tensor_shape == target_shape {
257 return Ok(tensor.clone());
258 }
259
260 let plan = broadcast_input_plan(&tensor_shape, target_shape)
261 .map_err(|err| graph_broadcast_error("broadcast_to", err))?;
262
263 let source = if plan.source_shape == tensor_shape {
264 tensor.clone()
265 } else {
266 tensor.reshape(&plan.source_shape)?
267 };
268 source.broadcast_in_dim(target_shape, &plan.dims)
269}
270
271/// Broadcast two tensors to a common shape.
272pub(crate) fn broadcast_binary(
273 a: &TracedTensor,
274 b: &TracedTensor,
275) -> Result<(TracedTensor, TracedTensor)> {
276 if a.shape_hint == b.shape_hint && a.rank == b.rank {
277 return Ok((a.clone(), b.clone()));
278 }
279 if (try_concrete_shape(a).is_none() || try_concrete_shape(b).is_none()) && a.rank == b.rank {
280 return Ok((a.clone(), b.clone()));
281 }
282 let a_shape = concrete_shape(a)?;
283 let b_shape = concrete_shape(b)?;
284 let target = broadcast_shape(&a_shape, &b_shape)
285 .map_err(|err| graph_broadcast_error("broadcast_binary", err))?;
286 Ok((broadcast_to(a, &target)?, broadcast_to(b, &target)?))
287}
288
289pub(crate) fn broadcast_ternary(
290 a: &TracedTensor,
291 b: &TracedTensor,
292 c: &TracedTensor,
293) -> Result<(TracedTensor, TracedTensor, TracedTensor)> {
294 let a_shape = concrete_shape(a)?;
295 let b_shape = concrete_shape(b)?;
296 let c_shape = concrete_shape(c)?;
297 let target = broadcast_shapes([a_shape.as_slice(), b_shape.as_slice(), c_shape.as_slice()])
298 .map_err(|err| graph_broadcast_error("broadcast_ternary", err))?;
299 Ok((
300 broadcast_to(a, &target)?,
301 broadcast_to(b, &target)?,
302 broadcast_to(c, &target)?,
303 ))
304}
305
306fn scale_with_constant(input: &TracedTensor, op: StdTensorOp) -> Result<TracedTensor> {
307 let scalar = apply_nullary(op, 0, input.dtype, Some(vec![]))?;
308 // A binary op applies to operands of one shape, so the rank-0 factor has to be
309 // broadcast here, exactly as `TracedTensor::add`/`mul` do through
310 // `broadcast_binary`. Without it the graph carries a rank-0 `Mul` operand: the
311 // CPU backend happens to accept one, the CUDA backend rejects it with
312 // `incompatible shapes: lhs=[], rhs=<input shape>`, so the same graph would run
313 // on one backend and not the other.
314 let (input, scalar) = broadcast_binary(input, &scalar)?;
315 apply_binary(
316 StdTensorOp::Mul,
317 &input,
318 &scalar,
319 input.rank,
320 input.shape_hint.clone(),
321 )
322}
323
324fn try_inferred_output_dtype(
325 op: &StdTensorOp,
326 inputs: &[DType],
327 context: &'static str,
328) -> Result<DType> {
329 crate::shape_infer::infer_output_dtype_at(op, inputs, ErrorPhase::GraphBuild)
330 .map_err(|err| graph_error_with_context(context, err))
331}
332
333fn checked_shape_product_for_graph_build(
334 shape: &[usize],
335 context: &'static str,
336 _role: &'static str,
337) -> Result<usize> {
338 shape.iter().copied().try_fold(1usize, |acc, dim| {
339 acc.checked_mul(dim)
340 .ok_or_else(|| graph_validation(context, ValidationError::IntegerOverflow))
341 })
342}
343
344fn validate_concrete_reshape_shape(input: &TracedTensor, shape: &[usize]) -> Result<()> {
345 let to = checked_shape_product_for_graph_build(shape, "TracedTensor::reshape", "target")?;
346 let Some(input_shape) = try_concrete_shape(input) else {
347 return Ok(());
348 };
349 let from =
350 checked_shape_product_for_graph_build(&input_shape, "TracedTensor::reshape", "input")?;
351 if from != to {
352 return Err(graph_validation(
353 "TracedTensor::reshape",
354 ShapeMismatch::ReshapeElementCount { from, to },
355 ));
356 }
357 Ok(())
358}
359
360fn traced_input_shape_exprs(input_idx: usize, tensor: &TracedTensor) -> Vec<DimExpr> {
361 match tensor.shape_hint.as_ref() {
362 Some(shape) => shape
363 .iter()
364 .enumerate()
365 .map(|(axis, dim)| {
366 dim.constant_value()
367 .map_or(DimExpr::InputDim { input_idx, axis }, DimExpr::Const)
368 })
369 .collect(),
370 None => (0..tensor.rank)
371 .map(|axis| DimExpr::InputDim { input_idx, axis })
372 .collect(),
373 }
374}
375
376fn traced_input_sym_shape(tensor: &TracedTensor) -> Vec<SymDim> {
377 tensor.shape_hint.clone().unwrap_or_else(|| {
378 (0..tensor.rank)
379 .map(|axis| SymDim::tensor_axis(tensor.id, axis))
380 .collect()
381 })
382}
383
384pub(crate) fn infer_traced_single_output_shape(
385 op_name: &'static str,
386 op: &StdTensorOp,
387 inputs: &[&TracedTensor],
388) -> Result<(usize, Option<Vec<SymDim>>)> {
389 let input_shape_exprs: Vec<Vec<DimExpr>> = inputs
390 .iter()
391 .enumerate()
392 .map(|(input_idx, tensor)| traced_input_shape_exprs(input_idx, tensor))
393 .collect();
394 let input_shape_refs: Vec<&[DimExpr]> = input_shape_exprs.iter().map(Vec::as_slice).collect();
395 let output_shapes = crate::shape_infer::infer_output_shapes(op, &input_shape_refs)
396 .map_err(|err| graph_error_with_context(op_name, err))?;
397 let output_shape = output_shapes.first().ok_or_else(|| {
398 Error::Internal(format!("{op_name}: shape inference returned no outputs"))
399 })?;
400 if output_shapes.len() != 1 {
401 return Err(Error::Internal(format!(
402 "{op_name}: expected single-output shape inference, got {} outputs",
403 output_shapes.len()
404 )));
405 }
406
407 let input_sym_shapes: Vec<Vec<SymDim>> = inputs
408 .iter()
409 .map(|tensor| traced_input_sym_shape(tensor))
410 .collect();
411 let input_sym_refs: Vec<&[SymDim]> = input_sym_shapes.iter().map(Vec::as_slice).collect();
412 let out_shape_hint = output_shape
413 .iter()
414 .map(|dim| SymDim::from_dim_expr(dim, &input_sym_refs))
415 .collect();
416 Ok((output_shape.len(), Some(out_shape_hint)))
417}
418
419pub(crate) fn register_metadata_or_runtime_state<E>(
420 result: std::result::Result<GlobalMetadataScope, E>,
421) -> Result<GlobalMetadataScope>
422where
423 E: StdError + Send + Sync + 'static,
424{
425 result.map_err(|err| Error::runtime_state_source("metadata", ErrorPhase::Compile, err))
426}
427
428fn reduction_output_meta(
429 tensor: &TracedTensor,
430 axes: &[usize],
431 op: &'static str,
432) -> Result<(usize, Option<Vec<SymDim>>)> {
433 let mut seen = vec![false; tensor.rank];
434 for &axis in axes {
435 if axis >= tensor.rank {
436 return Err(graph_validation(
437 op,
438 ValidationError::AxisOutOfBounds {
439 axis,
440 rank: tensor.rank,
441 },
442 ));
443 }
444 if seen[axis] {
445 return Err(graph_validation(
446 op,
447 ValidationError::DuplicateAxis {
448 axis,
449 role: "reduction",
450 },
451 ));
452 }
453 seen[axis] = true;
454 }
455
456 let out_shape_hint = tensor.shape_hint.as_ref().map(|shape| {
457 (0..shape.len())
458 .filter(|d| !axes.contains(d))
459 .map(|d| shape[d].clone())
460 .collect()
461 });
462 Ok((tensor.rank - axes.len(), out_shape_hint))
463}
464
465fn validate_traced_axis(tensor: &TracedTensor, axis: usize, op: &'static str) -> Result<()> {
466 if axis >= tensor.rank {
467 return Err(graph_validation(
468 op,
469 ValidationError::AxisOutOfBounds {
470 axis,
471 rank: tensor.rank,
472 },
473 ));
474 }
475 Ok(())
476}
477
478fn validate_traced_axes(rank: usize, axes: &[usize], op: &'static str) -> Result<()> {
479 let mut seen = vec![false; rank];
480 for &axis in axes {
481 if axis >= rank {
482 return Err(graph_validation(
483 op,
484 ValidationError::AxisOutOfBounds { axis, rank },
485 ));
486 }
487 if seen[axis] {
488 return Err(graph_validation(
489 op,
490 ValidationError::DuplicateAxis { axis, role: "axis" },
491 ));
492 }
493 seen[axis] = true;
494 }
495 Ok(())
496}
497
498fn validate_traced_insert_axis(rank: usize, axis: usize, op: &'static str) -> Result<()> {
499 if axis > rank {
500 return Err(graph_invalid_argument(
501 op,
502 "axis",
503 format!("axis {axis} out of bounds for rank {rank} insertion"),
504 ));
505 }
506 Ok(())
507}
508
509fn validate_traced_perm(rank: usize, perm: &[usize], op: &'static str) -> Result<()> {
510 if perm.len() != rank {
511 return Err(graph_validation(
512 op,
513 ValidationError::InvalidPermutationLength {
514 expected: rank,
515 actual: perm.len(),
516 },
517 ));
518 }
519 let mut seen = vec![false; rank];
520 for &axis in perm {
521 if axis >= rank {
522 return Err(graph_validation(
523 op,
524 ValidationError::AxisOutOfBounds { axis, rank },
525 ));
526 }
527 if seen[axis] {
528 return Err(graph_validation(
529 op,
530 ValidationError::DuplicateAxis {
531 axis,
532 role: "permutation",
533 },
534 ));
535 }
536 seen[axis] = true;
537 }
538 Ok(())
539}
540
541fn validate_broadcast_in_dim_args(
542 input: &TracedTensor,
543 output_shape: &[SymDim],
544 dims: &[usize],
545 op: &'static str,
546) -> Result<()> {
547 if dims.len() != input.rank {
548 return Err(graph_validation(
549 op,
550 ValidationError::RankMismatch {
551 expected: input.rank,
552 actual: dims.len(),
553 },
554 ));
555 }
556
557 let concrete_input_shape: Option<Vec<usize>> = input
558 .shape_hint
559 .as_ref()
560 .and_then(|shape| shape.iter().map(SymDim::constant_value).collect());
561 let concrete_output_shape = output_shape
562 .iter()
563 .map(SymDim::constant_value)
564 .collect::<Option<Vec<_>>>();
565 if let (Some(input_shape), Some(output_shape)) = (
566 concrete_input_shape.as_deref(),
567 concrete_output_shape.as_deref(),
568 ) && let Some(error) = broadcast_in_dim_extent_error(input_shape, output_shape, dims)
569 {
570 return Err(graph_broadcast_error(op, error));
571 }
572
573 let mut seen = vec![false; output_shape.len()];
574 for &dim in dims {
575 if dim >= output_shape.len() {
576 return Err(graph_validation(
577 op,
578 ValidationError::AxisOutOfBounds {
579 axis: dim,
580 rank: output_shape.len(),
581 },
582 ));
583 }
584 if seen[dim] {
585 return Err(graph_validation(
586 op,
587 ValidationError::DuplicateAxis {
588 axis: dim,
589 role: "broadcast",
590 },
591 ));
592 }
593 seen[dim] = true;
594 }
595
596 Ok(())
597}
598
599impl std::ops::Add for &TracedTensor {
600 type Output = Result<TracedTensor>;
601
602 fn add(self, rhs: &TracedTensor) -> Result<TracedTensor> {
603 TracedTensor::add(self, rhs)
604 }
605}
606
607impl std::ops::Sub for &TracedTensor {
608 type Output = Result<TracedTensor>;
609
610 fn sub(self, rhs: &TracedTensor) -> Result<TracedTensor> {
611 TracedTensor::sub(self, rhs)
612 }
613}
614
615impl std::ops::Mul for &TracedTensor {
616 type Output = Result<TracedTensor>;
617
618 fn mul(self, rhs: &TracedTensor) -> Result<TracedTensor> {
619 TracedTensor::mul(self, rhs)
620 }
621}
622
623impl std::ops::Mul<f64> for &TracedTensor {
624 type Output = Result<TracedTensor>;
625
626 fn mul(self, rhs: f64) -> Result<TracedTensor> {
627 self.scale_real(rhs)
628 }
629}
630
631impl std::ops::Mul<&TracedTensor> for f64 {
632 type Output = Result<TracedTensor>;
633
634 fn mul(self, rhs: &TracedTensor) -> Result<TracedTensor> {
635 rhs.scale_real(self)
636 }
637}
638
639impl std::ops::Neg for &TracedTensor {
640 type Output = Result<TracedTensor>;
641
642 fn neg(self) -> Self::Output {
643 TracedTensor::neg(self)
644 }
645}
646
647impl std::ops::Div for &TracedTensor {
648 type Output = Result<TracedTensor>;
649
650 fn div(self, rhs: &TracedTensor) -> Result<TracedTensor> {
651 TracedTensor::div(self, rhs)
652 }
653}
654
655impl std::ops::Rem for &TracedTensor {
656 type Output = Result<TracedTensor>;
657
658 fn rem(self, rhs: &TracedTensor) -> Result<TracedTensor> {
659 TracedTensor::rem(self, rhs)
660 }
661}
662
663impl TracedTensor {
664 /// Return the graph that owns this traced tensor's current value.
665 ///
666 /// # Examples
667 ///
668 /// ```
669 /// use tenferro_runtime::TracedTensor;
670 ///
671 /// let x = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
672 /// let _graph = x.graph();
673 /// ```
674 pub fn graph(&self) -> &Arc<Graph<StdTensorOp>> {
675 &self.graph
676 }
677
678 /// Return the concrete tensor data attached to this traced value, if any.
679 ///
680 /// Placeholder tensors created with `input_concrete_shape` or
681 /// `input_symbolic_shape` have no attached data until execution bindings
682 /// provide it.
683 ///
684 /// # Examples
685 ///
686 /// ```
687 /// use tenferro_runtime::{DType, TracedTensor};
688 ///
689 /// let concrete = TracedTensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
690 /// assert!(concrete.attached_value().is_some());
691 ///
692 /// let placeholder = TracedTensor::input_symbolic_shape(DType::F64, 1).unwrap();
693 /// assert!(placeholder.attached_value().is_none());
694 /// ```
695 pub fn attached_value(&self) -> Option<&Arc<RetainedValue>> {
696 self.data.as_ref()
697 }
698
699 /// Build a [`TracedTensor`] leaf from a concrete [`Tensor`], keeping its
700 /// shape as a concrete `shape_hint`.
701 ///
702 /// This is the common constructor when you have concrete tensor data that
703 /// you want to use both for graph building and for evaluation. The
704 /// resulting tensor is treated as a concrete-shape leaf by downstream
705 /// passes (binary einsum decomposition, build-time reshape folding, etc.).
706 ///
707 /// # Examples
708 ///
709 /// ```
710 /// use tenferro_runtime::{Tensor, TracedTensor};
711 ///
712 /// let a = TracedTensor::from_tensor_concrete_shape(
713 /// Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(),
714 /// )
715 /// .unwrap();
716 /// assert_eq!(a.rank, 2);
717 /// assert!(a.is_concrete_shape());
718 /// ```
719 ///
720 /// # Errors
721 ///
722 /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
723 /// cannot retain the concrete tensor's shape or dtype.
724 pub fn from_tensor_concrete_shape(tensor: Tensor) -> Result<Self> {
725 Self::from_tensor_concrete_shape_with_identity(tensor, None)
726 }
727
728 /// Build a [`TracedTensor`] leaf from a concrete tensor that declares the
729 /// canonical identity of its externally defined scalar.
730 ///
731 /// # Examples
732 ///
733 /// ```rust
734 /// use tenferro_runtime::TracedTensor;
735 /// use tenferro_tensor::DType;
736 ///
737 /// let dtype = DType::External(std::any::TypeId::of::<f64>());
738 /// let leaf = TracedTensor::input_concrete_shape_declaring_scalar(dtype, &[2], "example.scalar.v1")?;
739 /// assert!(leaf.is_concrete_shape());
740 /// # Ok::<(), tenferro_runtime::Error>(())
741 /// ```
742 ///
743 /// # Errors
744 ///
745 /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
746 /// cannot retain the concrete tensor's shape or dtype.
747 pub fn from_tensor_concrete_shape_declaring_scalar(
748 tensor: Tensor,
749 identity: &'static str,
750 ) -> Result<Self> {
751 Self::from_tensor_concrete_shape_with_identity(tensor, Some(identity))
752 }
753
754 fn from_tensor_concrete_shape_with_identity(
755 tensor: Tensor,
756 identity: Option<&'static str>,
757 ) -> Result<Self> {
758 let shape = tensor.shape().to_vec();
759 let rank = shape.len();
760 let dtype = tensor.dtype();
761 let key = next_input_key();
762 let id = next_traced_id();
763 let data = Arc::new(RetainedValue::from_tensor(tensor));
764 let meta = concrete_tensor_meta(dtype, &shape);
765 let meta = match identity {
766 Some(identity) => meta.with_scalar_identity(identity),
767 None => meta,
768 };
769
770 let mut builder = GraphBuilder::new();
771 let val = builder.add_input(key.clone());
772 builder.set_outputs(vec![val]);
773 let graph = Arc::new(builder.build());
774 let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
775 graph.values()[val].key.clone(),
776 meta.clone(),
777 ))?;
778
779 let mut map = HashMap::new();
780 map.insert(key.clone(), Arc::clone(&data));
781 let mut leaf_metas = HashMap::new();
782 leaf_metas.insert(key, meta);
783
784 Ok(Self {
785 id,
786 rank,
787 dtype,
788 graph,
789 val,
790 data: Some(data),
791 shape_hint: Some(shape.into_iter().map(SymDim::from).collect()),
792 inputs_map: Arc::new(map),
793 leaf_metas: Arc::new(leaf_metas),
794 extra_roots: Vec::new(),
795 checkpoint_chain: None,
796 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
797 constraint_scopes: ConstraintScopeChain::empty(),
798 })
799 }
800
801 /// Build a [`TracedTensor`] leaf from a concrete [`Tensor`] but advertise
802 /// a symbolic shape during graph construction.
803 ///
804 /// The tensor data is still attached (so plain `eval` works without
805 /// bindings), but graph passes see the leaf as shape-symbolic. This is
806 /// useful for building a single traced program that should not bake in
807 /// shape-specific optimizations.
808 ///
809 /// # Examples
810 ///
811 /// ```
812 /// use tenferro_runtime::{Tensor, TracedTensor};
813 ///
814 /// let t = TracedTensor::from_tensor_symbolic_shape(
815 /// Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(),
816 /// )
817 /// .unwrap();
818 /// assert_eq!(t.rank, 2);
819 /// assert!(!t.is_concrete_shape());
820 /// ```
821 ///
822 /// # Errors
823 ///
824 /// Returns [`Error::RuntimeStateSource`] when symbolic graph metadata
825 /// registration is unavailable or its registry state is poisoned.
826 pub fn from_tensor_symbolic_shape(tensor: Tensor) -> Result<Self> {
827 Self::from_tensor_value_symbolic_shape(TensorValue::from_tensor(tensor))
828 }
829
830 /// Build a data-attached symbolic-shape traced leaf from shared tensor data.
831 ///
832 /// This is an internal crate-boundary helper for eager AD recording. It
833 /// preserves the same tensor allocation used by the eager value while graph
834 /// passes see symbolic input extents.
835 ///
836 /// # Examples
837 ///
838 /// ```
839 /// use std::sync::Arc;
840 /// use tenferro_runtime::{Tensor, TracedTensor};
841 ///
842 /// let tensor = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
843 /// let traced = TracedTensor::from_tensor_symbolic_shape(tensor)?;
844 /// assert_eq!(traced.rank, 1);
845 /// assert!(!traced.is_concrete_shape());
846 /// # Ok::<(), tenferro_runtime::Error>(())
847 /// ```
848 ///
849 /// # Errors
850 ///
851 /// Returns [`Error::RuntimeStateSource`] when symbolic graph metadata
852 /// registration is unavailable or its registry state is poisoned.
853 #[doc(hidden)]
854 pub fn from_tensor_value_symbolic_shape(value: TensorValue) -> Result<Self> {
855 let data = Arc::new(RetainedValue::from_tensor_value(value)?);
856 Self::from_shared_tensor_value_symbolic_shape(data)
857 }
858
859 #[doc(hidden)]
860 pub fn from_shared_tensor_value_symbolic_shape(data: Arc<RetainedValue>) -> Result<Self> {
861 let rank = data.shape().len();
862 let dtype = data.dtype();
863 let key = next_input_key();
864 let id = next_traced_id();
865 let meta = symbolic_input_meta(dtype, id, rank);
866
867 let mut builder = GraphBuilder::new();
868 let val = builder.add_input(key.clone());
869 builder.set_outputs(vec![val]);
870 let graph = Arc::new(builder.build());
871 let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
872 graph.values()[val].key.clone(),
873 meta.clone(),
874 ))?;
875
876 let mut map = HashMap::new();
877 map.insert(key.clone(), Arc::clone(&data));
878 let mut leaf_metas = HashMap::new();
879 leaf_metas.insert(key, meta);
880
881 Ok(Self {
882 id,
883 rank,
884 dtype,
885 graph,
886 val,
887 data: Some(data),
888 shape_hint: None,
889 inputs_map: Arc::new(map),
890 leaf_metas: Arc::new(leaf_metas),
891 extra_roots: Vec::new(),
892 checkpoint_chain: None,
893 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
894 constraint_scopes: ConstraintScopeChain::empty(),
895 })
896 }
897
898 /// Build a data-less placeholder leaf with a fixed (concrete) shape.
899 ///
900 /// Must be passed as an input to [`crate::Runtime::prepare`] before evaluation.
901 /// Use this when you know the exact shape of the input but want to build
902 /// the graph once and feed different concrete tensors at execution time.
903 ///
904 /// # Examples
905 ///
906 /// ```
907 /// use tenferro_tensor::DType;
908 /// use tenferro_runtime::TracedTensor;
909 ///
910 /// let x = TracedTensor::input_concrete_shape(DType::F64, &[2, 3]).unwrap();
911 /// assert_eq!(x.rank, 2);
912 /// assert!(x.is_concrete_shape());
913 /// ```
914 ///
915 /// # Errors
916 ///
917 /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
918 /// fails or the registry state is poisoned. `dtype` and `shape` are
919 /// metadata values and are not revalidated by this constructor.
920 pub fn input_concrete_shape(dtype: DType, shape: &[usize]) -> Result<Self> {
921 Self::input_concrete_shape_with_identity(dtype, shape, None)
922 }
923
924 /// Build a data-less placeholder leaf of a concrete shape that declares the
925 /// canonical identity of its externally defined scalar.
926 ///
927 /// A semantic program's identity must be reproducible across processes, while an
928 /// externally defined scalar's tag is a process-local `TypeId`, so an input whose
929 /// dtype is external must declare the stable name its contribution owns.
930 ///
931 /// # Examples
932 ///
933 /// ```rust
934 /// use tenferro_runtime::TracedTensor;
935 /// use tenferro_tensor::DType;
936 ///
937 /// let dtype = DType::External(std::any::TypeId::of::<f64>());
938 /// let x = TracedTensor::input_concrete_shape_declaring_scalar(dtype, &[2], "example.scalar.v1")?;
939 /// assert_eq!(x.rank, 1);
940 /// assert!(x.is_concrete_shape());
941 /// # Ok::<(), tenferro_runtime::Error>(())
942 /// ```
943 ///
944 /// # Errors
945 ///
946 /// Returns [`Error::RuntimeStateSource`] when graph metadata registration fails
947 /// or the registry state is poisoned.
948 pub fn input_concrete_shape_declaring_scalar(
949 dtype: DType,
950 shape: &[usize],
951 identity: &'static str,
952 ) -> Result<Self> {
953 Self::input_concrete_shape_with_identity(dtype, shape, Some(identity))
954 }
955
956 fn input_concrete_shape_with_identity(
957 dtype: DType,
958 shape: &[usize],
959 identity: Option<&'static str>,
960 ) -> Result<Self> {
961 let shape = shape.to_vec();
962 let rank = shape.len();
963 let key = next_input_key();
964 let id = next_traced_id();
965
966 let mut builder = GraphBuilder::new();
967 let val = builder.add_input(key.clone());
968 builder.set_outputs(vec![val]);
969 let graph = Arc::new(builder.build());
970 let meta = concrete_tensor_meta(dtype, &shape);
971 let meta = match identity {
972 Some(identity) => meta.with_scalar_identity(identity),
973 None => meta,
974 };
975 let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
976 graph.values()[val].key.clone(),
977 meta,
978 ))?;
979
980 Ok(Self {
981 id,
982 rank,
983 dtype,
984 graph,
985 val,
986 data: None,
987 shape_hint: Some(shape.into_iter().map(SymDim::from).collect()),
988 inputs_map: Arc::new(HashMap::new()),
989 leaf_metas: Arc::new(HashMap::new()),
990 extra_roots: Vec::new(),
991 checkpoint_chain: None,
992 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
993 constraint_scopes: ConstraintScopeChain::empty(),
994 })
995 }
996
997 /// Build a data-less placeholder leaf with the given rank but fully
998 /// symbolic shape (every dim is a distinct `SymDim::TensorAxis`).
999 ///
1000 /// Must be passed as an input to [`crate::Runtime::prepare`] before
1001 /// evaluation. Use this to build shape-agnostic graphs.
1002 ///
1003 /// # Examples
1004 ///
1005 /// ```
1006 /// use tenferro_tensor::DType;
1007 /// use tenferro_runtime::TracedTensor;
1008 ///
1009 /// let x = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
1010 /// assert_eq!(x.rank, 2);
1011 /// assert!(!x.is_concrete_shape());
1012 /// ```
1013 ///
1014 /// # Errors
1015 ///
1016 /// Returns [`Error::RuntimeStateSource`] when graph metadata registration
1017 /// fails or the registry state is poisoned. `rank` is recorded as the
1018 /// symbolic placeholder rank and is not otherwise rejected here.
1019 pub fn input_symbolic_shape(dtype: DType, rank: usize) -> Result<Self> {
1020 let key = next_input_key();
1021 let id = next_traced_id();
1022
1023 let mut builder = GraphBuilder::new();
1024 let val = builder.add_input(key.clone());
1025 builder.set_outputs(vec![val]);
1026 let graph = Arc::new(builder.build());
1027 let metadata_scope = register_metadata_or_runtime_state(register_scoped_value_metadata(
1028 graph.values()[val].key.clone(),
1029 symbolic_input_meta(dtype, id, rank),
1030 ))?;
1031
1032 Ok(Self {
1033 id,
1034 rank,
1035 dtype,
1036 graph,
1037 val,
1038 data: None,
1039 shape_hint: None,
1040 inputs_map: Arc::new(HashMap::new()),
1041 leaf_metas: Arc::new(HashMap::new()),
1042 extra_roots: Vec::new(),
1043 checkpoint_chain: None,
1044 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
1045 constraint_scopes: ConstraintScopeChain::empty(),
1046 })
1047 }
1048
1049 /// Build a concrete-shape [`TracedTensor`] leaf from column-major typed
1050 /// `Vec<T>` data.
1051 ///
1052 /// The data must already be in tenferro's physical column-major order.
1053 ///
1054 /// # Examples
1055 ///
1056 /// ```
1057 /// use tenferro_runtime::TracedTensor;
1058 ///
1059 /// let a = TracedTensor::from_vec_col_major(
1060 /// vec![2, 3],
1061 /// vec![1.0_f64, 4.0, 2.0, 5.0, 3.0, 6.0],
1062 /// )?;
1063 /// assert_eq!(a.rank, 2);
1064 /// # Ok::<(), tenferro_runtime::Error>(())
1065 /// ```
1066 ///
1067 /// # Errors
1068 ///
1069 /// Returns [`Error::TensorRuntime`] containing
1070 /// `ValidationError::ShapeDataLengthMismatch` when the shape product does
1071 /// not equal `data.len()`, or `ValidationError::IntegerOverflow` when the
1072 /// shape product cannot be represented by `usize`.
1073 pub fn from_vec_col_major<T: TensorScalar>(
1074 shape: impl IntoShapeVec,
1075 data: Vec<T>,
1076 ) -> Result<Self> {
1077 Self::from_tensor_concrete_shape(Tensor::from_vec_col_major(shape, data)?)
1078 }
1079
1080 /// Return the tensor element dtype recorded for this traced value.
1081 pub fn dtype(&self) -> DType {
1082 self.dtype
1083 }
1084
1085 /// Returns `true` iff every dim of this tensor's `shape_hint` is a
1086 /// constant `SymDim` (i.e. the shape is fully known at graph-build time).
1087 ///
1088 /// # Examples
1089 ///
1090 /// ```
1091 /// use tenferro_tensor::DType;
1092 /// use tenferro_runtime::TracedTensor;
1093 ///
1094 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1095 /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
1096 /// assert!(a.is_concrete_shape());
1097 /// assert!(!b.is_concrete_shape());
1098 /// ```
1099 pub fn is_concrete_shape(&self) -> bool {
1100 try_concrete_shape(self).is_some()
1101 }
1102
1103 /// Return the fully-concrete shape of this tensor, if every dim of
1104 /// its shape-hint is a constant `SymDim`. Returns `None` if any
1105 /// dimension is symbolic.
1106 ///
1107 /// This is the counterpart to [`Self::is_concrete_shape`] for callers
1108 /// that need to *use* the concrete shape (e.g. external composition
1109 /// wrappers building `broadcast_in_dim` payloads from known shapes).
1110 ///
1111 /// # Examples
1112 ///
1113 /// ```
1114 /// use tenferro_tensor::DType;
1115 /// use tenferro_runtime::TracedTensor;
1116 ///
1117 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1118 /// assert_eq!(a.try_concrete_shape(), Some(vec![2, 3]));
1119 ///
1120 /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
1121 /// assert!(b.try_concrete_shape().is_none());
1122 /// ```
1123 pub fn try_concrete_shape(&self) -> Option<Vec<usize>> {
1124 try_concrete_shape(self)
1125 }
1126
1127 /// Return the concrete tensor shape.
1128 ///
1129 /// Returns an error when a shape hint is missing or any dimension is
1130 /// symbolic. Composite traced ops that require concrete sizes should
1131 /// propagate this error instead of panicking.
1132 ///
1133 /// # Errors
1134 ///
1135 /// Returns [`Error::Validation`] with `InvalidArgument` when this tensor
1136 /// has no shape hint or any dimension is symbolic.
1137 pub fn concrete_shape(&self) -> Result<Vec<usize>> {
1138 concrete_shape(self)
1139 }
1140
1141 /// If this `TracedTensor` is a leaf (single-node input graph),
1142 /// return its input key. Computed tensors return `None`.
1143 pub fn input_key(&self) -> Option<TensorInputKey> {
1144 match &self.graph.values()[self.val].key {
1145 ValueKey::Input(key) => Some(key.clone()),
1146 _ => None,
1147 }
1148 }
1149
1150 /// Return whether this traced graph carries default data for `key`.
1151 #[doc(hidden)]
1152 pub fn has_attached_input_key(&self, key: &TensorInputKey) -> bool {
1153 self.inputs_map.contains_key(key)
1154 }
1155
1156 /// Elementwise addition with NumPy-style broadcasting.
1157 ///
1158 /// Prefer using the `+` operator when it reads naturally.
1159 ///
1160 /// A longer expression such as `a + b + c` does not compose because the
1161 /// first `+` returns `Result<TracedTensor, Error>`, so the second `+`
1162 /// would receive a result rather than a tensor. Use `?` at each step or
1163 /// the explicit fallible method chain shown below when the operation
1164 /// sequence is more important than notation:
1165 ///
1166 /// # Examples
1167 ///
1168 /// ```rust
1169 /// # use tenferro_runtime::{Error, TracedTensor};
1170 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1171 /// # let z = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1172 /// let y = x.add(&z);
1173 /// let y2 = &x + &z;
1174 /// # fn add_three(
1175 /// # a: &TracedTensor,
1176 /// # b: &TracedTensor,
1177 /// # c: &TracedTensor,
1178 /// # ) -> Result<TracedTensor, Error> {
1179 /// let ab = (a + b)?;
1180 /// let sum = (&ab + c)?;
1181 /// let method_chain = a.add(b)?.add(c)?;
1182 /// let _ = method_chain;
1183 /// # Ok(sum)
1184 /// # }
1185 /// ```
1186 ///
1187 /// Tenferro prioritizes robust error handling over the conciseness of
1188 /// chained operator notation; the explicit fallible methods are the
1189 /// canonical form for longer sequences.
1190 ///
1191 /// # Errors
1192 ///
1193 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1194 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1195 /// metadata registration fails.
1196 ///
1197 /// # Deferred errors
1198 ///
1199 /// If symbolic dimensions prevent shape comparison during graph
1200 /// construction, the same `ShapeMismatch` can be reported during
1201 /// compilation or execution, with the corresponding [`ErrorPhase`].
1202 pub fn add(&self, other: &TracedTensor) -> Result<TracedTensor> {
1203 let (lhs, rhs) = broadcast_binary(self, other)?;
1204 apply_binary(
1205 StdTensorOp::Add,
1206 &lhs,
1207 &rhs,
1208 lhs.rank,
1209 lhs.shape_hint.clone(),
1210 )
1211 }
1212
1213 /// Elementwise subtraction with NumPy-style broadcasting.
1214 ///
1215 /// Prefer using the `-` operator when it reads naturally.
1216 ///
1217 /// # Errors
1218 ///
1219 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1220 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1221 /// metadata registration fails.
1222 ///
1223 /// # Deferred errors
1224 ///
1225 /// If symbolic dimensions prevent shape comparison during graph
1226 /// construction, the same `ShapeMismatch` can be reported during
1227 /// compilation or execution, with the corresponding [`ErrorPhase`].
1228 pub fn sub(&self, other: &TracedTensor) -> Result<TracedTensor> {
1229 let (lhs, rhs) = broadcast_binary(self, other)?;
1230 apply_binary(
1231 StdTensorOp::Sub,
1232 &lhs,
1233 &rhs,
1234 lhs.rank,
1235 lhs.shape_hint.clone(),
1236 )
1237 }
1238
1239 /// Elementwise multiplication with NumPy-style broadcasting.
1240 ///
1241 /// Prefer using the `*` operator when it reads naturally.
1242 ///
1243 /// # Examples
1244 ///
1245 /// ```rust
1246 /// # use tenferro_runtime::TracedTensor;
1247 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1248 /// # let z = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1249 /// let y = x.mul(&z);
1250 /// let y2 = &x * &z;
1251 /// ```
1252 ///
1253 /// # Errors
1254 ///
1255 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1256 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1257 /// metadata registration fails.
1258 ///
1259 /// # Deferred errors
1260 ///
1261 /// If symbolic ranks prevent shape comparison during graph construction,
1262 /// the same `ShapeMismatch` can be reported during compilation or
1263 /// execution, with the corresponding [`ErrorPhase`].
1264 pub fn mul(&self, other: &TracedTensor) -> Result<TracedTensor> {
1265 let (lhs, rhs) = broadcast_binary(self, other)?;
1266 apply_binary(
1267 StdTensorOp::Mul,
1268 &lhs,
1269 &rhs,
1270 lhs.rank,
1271 lhs.shape_hint.clone(),
1272 )
1273 }
1274
1275 /// Elementwise division with NumPy-style broadcasting.
1276 ///
1277 /// Prefer using the `/` operator when it reads naturally.
1278 ///
1279 /// # Examples
1280 ///
1281 /// ```rust
1282 /// # use tenferro_runtime::TracedTensor;
1283 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1284 /// # let z = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
1285 /// let y = x.div(&z);
1286 /// let y2 = &x / &z;
1287 /// ```
1288 ///
1289 /// # Errors
1290 ///
1291 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1292 /// cannot be broadcast, or [`Error::RuntimeStateSource`] when graph
1293 /// metadata registration fails.
1294 ///
1295 /// # Deferred errors
1296 ///
1297 /// If symbolic ranks prevent shape comparison during graph construction,
1298 /// the same `ShapeMismatch` can be reported during compilation or
1299 /// execution, with the corresponding [`ErrorPhase`]. For integer inputs,
1300 /// a zero divisor is reported during execution as
1301 /// [`Error::TensorRuntime`] containing a
1302 /// [`tenferro_tensor::Error::Extension`] classified as
1303 /// `tenferro_tensor::ErrorKind::NumericalFailure` and retaining the typed
1304 /// backend source; floating-point and complex zero divisors follow their
1305 /// numeric semantics instead.
1306 pub fn div(&self, other: &TracedTensor) -> Result<TracedTensor> {
1307 let (lhs, rhs) = broadcast_binary(self, other)?;
1308 apply_binary(
1309 StdTensorOp::Div,
1310 &lhs,
1311 &rhs,
1312 lhs.rank,
1313 lhs.shape_hint.clone(),
1314 )
1315 }
1316
1317 /// Elementwise remainder with NumPy-style broadcasting.
1318 ///
1319 /// Prefer using the `%` operator when it reads naturally.
1320 ///
1321 /// # Errors
1322 ///
1323 /// Returns [`Error::Validation`] with `ShapeMismatch` when operand shapes
1324 /// cannot be broadcast, [`Error::Unsupported`] at
1325 /// [`ErrorPhase::GraphBuild`] when either operand has a complex dtype, or
1326 /// [`Error::RuntimeStateSource`] when graph metadata registration fails.
1327 ///
1328 /// # Deferred errors
1329 ///
1330 /// If symbolic ranks prevent shape comparison during graph construction,
1331 /// the same `ShapeMismatch` can be reported during compilation or
1332 /// execution, with the corresponding [`ErrorPhase`]. For integer inputs,
1333 /// a zero divisor is reported during execution as
1334 /// [`Error::TensorRuntime`] containing a
1335 /// [`tenferro_tensor::Error::Extension`] classified as
1336 /// `tenferro_tensor::ErrorKind::NumericalFailure` and retaining the typed
1337 /// backend source; floating-point zero divisors follow their numeric
1338 /// semantics.
1339 pub fn rem(&self, other: &TracedTensor) -> Result<TracedTensor> {
1340 let (lhs, rhs) = broadcast_binary(self, other)?;
1341 apply_binary(
1342 StdTensorOp::Rem,
1343 &lhs,
1344 &rhs,
1345 lhs.rank,
1346 lhs.shape_hint.clone(),
1347 )
1348 }
1349
1350 /// Elementwise comparison with NumPy-style broadcasting.
1351 ///
1352 /// # Errors
1353 ///
1354 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1355 /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
1356 /// comparison rejects a complex dtype, or [`Error::RuntimeStateSource`]
1357 /// when result metadata cannot be registered.
1358 ///
1359 /// # Deferred errors
1360 ///
1361 /// With same-rank symbolic operands, shape compatibility is retained as a
1362 /// graph constraint. A concrete mismatch is reported later as
1363 /// [`Error::TensorRuntime`] containing a typed validation source, with the
1364 /// failure phase identifying compilation or execution.
1365 pub fn compare(&self, other: &TracedTensor, dir: CompareDir) -> Result<TracedTensor> {
1366 apply_broadcast_binary_op(StdTensorOp::Compare(dir), self, other)
1367 }
1368
1369 /// Elementwise maximum with NumPy-style broadcasting.
1370 ///
1371 /// # Errors
1372 ///
1373 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1374 /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
1375 /// maximum rejects a complex dtype, or [`Error::RuntimeStateSource`] when
1376 /// result metadata cannot be registered.
1377 ///
1378 /// # Deferred errors
1379 ///
1380 /// With same-rank symbolic operands, the broadcast constraint may fail at
1381 /// compile or execution and is returned as [`Error::TensorRuntime`] with
1382 /// its typed validation source.
1383 pub fn maximum(&self, other: &TracedTensor) -> Result<TracedTensor> {
1384 apply_broadcast_binary_op(StdTensorOp::Maximum, self, other)
1385 }
1386
1387 /// Elementwise minimum with NumPy-style broadcasting.
1388 ///
1389 /// # Errors
1390 ///
1391 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1392 /// operands cannot be broadcast, [`Error::Unsupported`] when ordered
1393 /// minimum rejects a complex dtype, or [`Error::RuntimeStateSource`] when
1394 /// result metadata cannot be registered.
1395 ///
1396 /// # Deferred errors
1397 ///
1398 /// With same-rank symbolic operands, the broadcast constraint may fail at
1399 /// compile or execution and is returned as [`Error::TensorRuntime`] with
1400 /// its typed validation source.
1401 pub fn minimum(&self, other: &TracedTensor) -> Result<TracedTensor> {
1402 apply_broadcast_binary_op(StdTensorOp::Minimum, self, other)
1403 }
1404
1405 /// Select values from `on_true` or `on_false` using `condition`.
1406 ///
1407 /// # Errors
1408 ///
1409 /// Returns [`Error::Validation`] with `InvalidArgument` when an operand
1410 /// lacks concrete shape metadata, or `ShapeMismatch` when the concrete
1411 /// condition and branches cannot share a broadcast shape. Dtype promotion
1412 /// failures are returned as [`Error::TensorRuntime`] with the typed
1413 /// `UnsupportedDTypeConversion` source; metadata failures retain
1414 /// [`Error::RuntimeStateSource`].
1415 pub fn where_select(
1416 condition: &TracedTensor,
1417 on_true: &TracedTensor,
1418 on_false: &TracedTensor,
1419 ) -> Result<TracedTensor> {
1420 apply_broadcast_ternary_op(StdTensorOp::Select, condition, on_true, on_false)
1421 }
1422
1423 /// Alias for [`Self::where_select`].
1424 ///
1425 /// # Errors
1426 ///
1427 /// Returns the same concrete failures as [`Self::where_select`]:
1428 /// [`Error::Validation`] with `InvalidArgument`/`ShapeMismatch` for shape
1429 /// metadata or broadcasting, [`Error::TensorRuntime`] with
1430 /// `UnsupportedDTypeConversion` for failed promotion, and
1431 /// [`Error::RuntimeStateSource`] for metadata registration.
1432 pub fn select(
1433 condition: &TracedTensor,
1434 on_true: &TracedTensor,
1435 on_false: &TracedTensor,
1436 ) -> Result<TracedTensor> {
1437 Self::where_select(condition, on_true, on_false)
1438 }
1439
1440 /// Clamp values elementwise between lower and upper bounds.
1441 ///
1442 /// # Errors
1443 ///
1444 /// Returns [`Error::Validation`] with `InvalidArgument` when an operand
1445 /// lacks concrete shape metadata, `ShapeMismatch` when bounds cannot be
1446 /// broadcast with the input, [`Error::Unsupported`] for an ordered
1447 /// complex dtype, or [`Error::RuntimeStateSource`] when metadata cannot be
1448 /// registered.
1449 pub fn clamp(&self, lower: &TracedTensor, upper: &TracedTensor) -> Result<TracedTensor> {
1450 apply_broadcast_ternary_op(StdTensorOp::Clamp, self, lower, upper)
1451 }
1452
1453 fn apply_same_shape_unary(&self, op: StdTensorOp) -> Result<TracedTensor> {
1454 apply_unary(op, self, self.rank, self.shape_hint.clone())
1455 }
1456
1457 /// Elementwise negation.
1458 ///
1459 /// Prefer using the unary `-` operator when it reads naturally.
1460 ///
1461 /// # Examples
1462 ///
1463 /// ```rust
1464 /// # use tenferro_runtime::TracedTensor;
1465 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1466 /// let y = x.neg().unwrap();
1467 /// let y2 = (-&x).unwrap();
1468 /// ```
1469 ///
1470 /// # Errors
1471 ///
1472 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1473 /// is unavailable or poisoned while recording the unary result.
1474 pub fn neg(&self) -> Result<TracedTensor> {
1475 self.apply_same_shape_unary(StdTensorOp::Neg)
1476 }
1477
1478 /// Elementwise complex conjugate.
1479 ///
1480 /// # Examples
1481 ///
1482 /// ```rust
1483 /// # use num_complex::Complex64;
1484 /// # use tenferro_runtime::TracedTensor;
1485 /// # let x = TracedTensor::from_vec_col_major(
1486 /// # vec![2],
1487 /// # vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)],
1488 /// # )
1489 /// # .unwrap();
1490 /// let y = x.conj().unwrap();
1491 /// ```
1492 ///
1493 /// # Errors
1494 ///
1495 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1496 /// is unavailable or poisoned while recording the unary result.
1497 pub fn conj(&self) -> Result<TracedTensor> {
1498 self.apply_same_shape_unary(StdTensorOp::Conj)
1499 }
1500
1501 /// Elementwise absolute value.
1502 ///
1503 /// Complex inputs return real magnitudes (`C32 -> F32`, `C64 -> F64`).
1504 ///
1505 /// # Examples
1506 ///
1507 /// ```rust
1508 /// # use tenferro_runtime::TracedTensor;
1509 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![-1.0_f64, 2.0]).unwrap();
1510 /// let y = x.abs().unwrap();
1511 /// ```
1512 ///
1513 /// # Errors
1514 ///
1515 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1516 /// is unavailable or poisoned while recording the unary result.
1517 pub fn abs(&self) -> Result<TracedTensor> {
1518 self.apply_same_shape_unary(StdTensorOp::Abs)
1519 }
1520
1521 /// Elementwise sign.
1522 ///
1523 /// # Examples
1524 ///
1525 /// ```rust
1526 /// # use tenferro_runtime::TracedTensor;
1527 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![-1.0_f64, 2.0]).unwrap();
1528 /// let y = x.sign().unwrap();
1529 /// ```
1530 ///
1531 /// # Errors
1532 ///
1533 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1534 /// is unavailable or poisoned while recording the unary result.
1535 pub fn sign(&self) -> Result<TracedTensor> {
1536 self.apply_same_shape_unary(StdTensorOp::Sign)
1537 }
1538
1539 /// Scale by a real scalar: `y = factor * x`.
1540 ///
1541 /// # Examples
1542 ///
1543 /// ```rust
1544 /// # use tenferro_runtime::TracedTensor;
1545 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1546 /// let y = x.scale_real(2.0)?;
1547 /// # Ok::<(), tenferro_runtime::Error>(())
1548 /// ```
1549 ///
1550 /// # Errors
1551 ///
1552 /// Returns [`Error::Validation`] with `InvalidArgument` when an integer or
1553 /// boolean factor is non-finite or out of range for the input dtype, or
1554 /// [`Error::RuntimeStateSource`] when output metadata registration fails.
1555 pub fn scale_real(&self, factor: f64) -> Result<TracedTensor> {
1556 let op = match self.dtype {
1557 DType::F64 => StdTensorOp::constant(factor),
1558 DType::F32 => StdTensorOp::constant(factor as f32),
1559 DType::I32 => StdTensorOp::constant(round_real_to_i32_for_op("scale_real", factor)?),
1560 DType::I64 => StdTensorOp::constant(round_real_to_i64(factor)?),
1561 DType::Bool => StdTensorOp::constant(bool_from_real_for_op("scale_real", factor)?),
1562 DType::C64 => StdTensorOp::constant(Complex64::new(factor, 0.0)),
1563 DType::C32 => StdTensorOp::constant(Complex32::new(factor as f32, 0.0)),
1564 // An externally defined scalar has no traced constant, so the closure
1565 // rejects it instead of guessing one.
1566 DType::External(_) => {
1567 return Err(graph_invalid_argument(
1568 "scale_real",
1569 "dtype",
1570 format!("requires a preset tensor dtype, got {:?}", self.dtype),
1571 ));
1572 }
1573 };
1574 scale_with_constant(self, op)
1575 }
1576
1577 /// Scale by a complex scalar: `y = factor * x`.
1578 ///
1579 /// Only complex tensors support complex scaling. For a real scalar factor
1580 /// that should preserve the input dtype, prefer [`scale_real`](Self::scale_real).
1581 ///
1582 /// # Examples
1583 ///
1584 /// ```rust
1585 /// use num_complex::Complex64;
1586 /// # use tenferro_runtime::TracedTensor;
1587 /// # let x = TracedTensor::from_vec_col_major(
1588 /// # vec![2],
1589 /// # vec![Complex64::new(1.0, 0.0), Complex64::new(2.0, 0.0)],
1590 /// # )
1591 /// # .unwrap();
1592 /// let y = x.scale_complex(Complex64::new(0.0, 1.0)).unwrap(); // multiply by i
1593 /// ```
1594 ///
1595 /// # Errors
1596 ///
1597 /// Returns [`Error::Validation`] with `InvalidArgument` when a complex
1598 /// factor is applied to a non-complex dtype, or
1599 /// [`Error::RuntimeStateSource`] when output metadata registration fails.
1600 pub fn scale_complex(&self, factor: Complex64) -> Result<TracedTensor> {
1601 match self.dtype {
1602 DType::C64 => scale_with_constant(self, StdTensorOp::constant(factor)),
1603 DType::C32 => scale_with_constant(
1604 self,
1605 StdTensorOp::constant(Complex32::new(factor.re as f32, factor.im as f32)),
1606 ),
1607 DType::F32
1608 | DType::F64
1609 | DType::I32
1610 | DType::I64
1611 | DType::Bool
1612 | DType::External(_) => Err(graph_invalid_argument(
1613 "scale_complex",
1614 "dtype",
1615 format!("requires complex tensor dtype, got {:?}", self.dtype),
1616 )),
1617 }
1618 }
1619
1620 /// Elementwise exponential.
1621 ///
1622 /// # Examples
1623 ///
1624 /// ```rust
1625 /// # use tenferro_runtime::TracedTensor;
1626 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1627 /// let y = x.exp().unwrap();
1628 /// ```
1629 ///
1630 /// # Errors
1631 ///
1632 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1633 /// is unavailable or poisoned while recording the unary result.
1634 pub fn exp(&self) -> Result<TracedTensor> {
1635 self.apply_same_shape_unary(StdTensorOp::Exp)
1636 }
1637
1638 /// Elementwise natural logarithm.
1639 ///
1640 /// # Examples
1641 ///
1642 /// ```rust
1643 /// # use tenferro_runtime::TracedTensor;
1644 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1645 /// let y = x.log().unwrap();
1646 /// ```
1647 ///
1648 /// # Errors
1649 ///
1650 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1651 /// is unavailable or poisoned while recording the unary result.
1652 pub fn log(&self) -> Result<TracedTensor> {
1653 self.apply_same_shape_unary(StdTensorOp::Log)
1654 }
1655
1656 /// Elementwise sine.
1657 ///
1658 /// # Examples
1659 ///
1660 /// ```rust
1661 /// # use tenferro_runtime::TracedTensor;
1662 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1663 /// let y = x.sin().unwrap();
1664 /// ```
1665 ///
1666 /// # Errors
1667 ///
1668 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1669 /// is unavailable or poisoned while recording the unary result.
1670 pub fn sin(&self) -> Result<TracedTensor> {
1671 self.apply_same_shape_unary(StdTensorOp::Sin)
1672 }
1673
1674 /// Elementwise cosine.
1675 ///
1676 /// # Examples
1677 ///
1678 /// ```rust
1679 /// # use tenferro_runtime::TracedTensor;
1680 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1681 /// let y = x.cos().unwrap();
1682 /// ```
1683 ///
1684 /// # Errors
1685 ///
1686 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1687 /// is unavailable or poisoned while recording the unary result.
1688 pub fn cos(&self) -> Result<TracedTensor> {
1689 self.apply_same_shape_unary(StdTensorOp::Cos)
1690 }
1691
1692 /// Elementwise hyperbolic tangent.
1693 ///
1694 /// # Examples
1695 ///
1696 /// ```rust
1697 /// # use tenferro_runtime::TracedTensor;
1698 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1699 /// let y = x.tanh().unwrap();
1700 /// ```
1701 ///
1702 /// # Errors
1703 ///
1704 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1705 /// is unavailable or poisoned while recording the unary result.
1706 pub fn tanh(&self) -> Result<TracedTensor> {
1707 self.apply_same_shape_unary(StdTensorOp::Tanh)
1708 }
1709
1710 /// Elementwise square root.
1711 ///
1712 /// # Examples
1713 ///
1714 /// ```rust
1715 /// # use tenferro_runtime::TracedTensor;
1716 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 4.0]).unwrap();
1717 /// let y = x.sqrt().unwrap();
1718 /// ```
1719 ///
1720 /// # Errors
1721 ///
1722 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1723 /// is unavailable or poisoned while recording the unary result.
1724 pub fn sqrt(&self) -> Result<TracedTensor> {
1725 self.apply_same_shape_unary(StdTensorOp::Sqrt)
1726 }
1727
1728 /// Elementwise reciprocal square root.
1729 ///
1730 /// # Examples
1731 ///
1732 /// ```rust
1733 /// # use tenferro_runtime::TracedTensor;
1734 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 4.0]).unwrap();
1735 /// let y = x.rsqrt().unwrap();
1736 /// ```
1737 ///
1738 /// # Errors
1739 ///
1740 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1741 /// is unavailable or poisoned while recording the unary result.
1742 pub fn rsqrt(&self) -> Result<TracedTensor> {
1743 self.apply_same_shape_unary(StdTensorOp::Rsqrt)
1744 }
1745
1746 /// Elementwise power with NumPy-style broadcasting.
1747 ///
1748 /// # Examples
1749 ///
1750 /// ```rust
1751 /// # use tenferro_runtime::TracedTensor;
1752 /// # let base = TracedTensor::from_vec_col_major(vec![2], vec![2.0_f64, 3.0]).unwrap();
1753 /// # let exp = TracedTensor::from_vec_col_major(vec![2], vec![3.0_f64, 2.0]).unwrap();
1754 /// let y = base.pow(&exp);
1755 /// ```
1756 ///
1757 /// # Errors
1758 ///
1759 /// Returns [`Error::Validation`] with `ShapeMismatch` when the concrete
1760 /// operands cannot be broadcast, or [`Error::RuntimeStateSource`] when
1761 /// result metadata cannot be registered.
1762 ///
1763 /// # Deferred errors
1764 ///
1765 /// A symbolic broadcast mismatch or integer negative exponent is
1766 /// discovered at compile or execution and is returned as
1767 /// [`Error::TensorRuntime`] with a typed `ShapeMismatch` or
1768 /// `NegativeIntegerExponent` numerical source and the corresponding
1769 /// [`ErrorPhase`].
1770 pub fn pow(&self, other: &TracedTensor) -> Result<TracedTensor> {
1771 let (lhs, rhs) = broadcast_binary(self, other)?;
1772 apply_binary(
1773 StdTensorOp::Pow,
1774 &lhs,
1775 &rhs,
1776 lhs.rank,
1777 lhs.shape_hint.clone(),
1778 )
1779 }
1780
1781 /// Elementwise `exp(x) - 1`.
1782 ///
1783 /// # Examples
1784 ///
1785 /// ```rust
1786 /// # use tenferro_runtime::TracedTensor;
1787 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1788 /// let y = x.expm1().unwrap();
1789 /// ```
1790 ///
1791 /// # Errors
1792 ///
1793 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1794 /// is unavailable or poisoned while recording the unary result.
1795 pub fn expm1(&self) -> Result<TracedTensor> {
1796 self.apply_same_shape_unary(StdTensorOp::Expm1)
1797 }
1798
1799 /// Elementwise `log(1 + x)`.
1800 ///
1801 /// # Examples
1802 ///
1803 /// ```rust
1804 /// # use tenferro_runtime::TracedTensor;
1805 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1806 /// let y = x.log1p().unwrap();
1807 /// ```
1808 ///
1809 /// # Errors
1810 ///
1811 /// Returns [`Error::RuntimeStateSource`] when the graph metadata registry
1812 /// is unavailable or poisoned while recording the unary result.
1813 pub fn log1p(&self) -> Result<TracedTensor> {
1814 self.apply_same_shape_unary(StdTensorOp::Log1p)
1815 }
1816
1817 /// Elementwise error function `erf(x)`, for real `F32`/`F64` tensors.
1818 ///
1819 /// `erf(+-0) = +-0`, `erf(+-inf) = +-1`, and `NaN` stays `NaN`. The
1820 /// derivative is `2/sqrt(pi) * exp(-x^2)`.
1821 ///
1822 /// # Examples
1823 ///
1824 /// ```rust
1825 /// # use tenferro_runtime::TracedTensor;
1826 /// let x = TracedTensor::from_vec_col_major(vec![2], vec![0.0_f64, 1.0])?;
1827 /// let y = x.erf()?;
1828 /// assert_eq!(y.dtype(), tenferro_runtime::DType::F64);
1829 /// # Ok::<(), tenferro_runtime::Error>(())
1830 /// ```
1831 ///
1832 /// # Errors
1833 ///
1834 /// Returns [`Error::Unsupported`] (phase `GraphBuild`) for complex,
1835 /// integer, or `Bool` input, or [`Error::RuntimeStateSource`] when the
1836 /// graph metadata registry is unavailable or poisoned while recording the
1837 /// unary result.
1838 pub fn erf(&self) -> Result<TracedTensor> {
1839 if !matches!(self.dtype, DType::F32 | DType::F64) {
1840 return Err(Error::unsupported(
1841 "TracedTensor::erf",
1842 ErrorPhase::GraphBuild,
1843 format!(
1844 "erf is defined for real F32/F64 tensors, got {:?}",
1845 self.dtype
1846 ),
1847 ));
1848 }
1849 self.apply_same_shape_unary(StdTensorOp::Erf)
1850 }
1851
1852 /// Convert the tensor to a different dtype using checked conversion.
1853 ///
1854 /// Use [`cast`](Self::cast) when a lossy dtype projection is intended.
1855 ///
1856 /// # Examples
1857 ///
1858 /// ```rust
1859 /// use tenferro_runtime::DType;
1860 /// # use tenferro_runtime::TracedTensor;
1861 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
1862 ///
1863 /// let y = x.convert(DType::C64)?;
1864 /// # Ok::<(), tenferro_runtime::Error>(())
1865 /// ```
1866 ///
1867 /// # Errors
1868 ///
1869 /// Returns [`tenferro_tensor::Error::UnsupportedDTypeConversion`] when the
1870 /// requested pair is outside tenferro's checked dtype-promotion lattice,
1871 /// or [`Error::Validation`] when graph metadata rejects the conversion.
1872 /// Use [`cast`](Self::cast) for explicit lossy dtype projection.
1873 pub fn convert(&self, to: DType) -> Result<TracedTensor> {
1874 tenferro_tensor::validate::validate_convert_dtype("TracedTensor::convert", self.dtype, to)?;
1875 self.cast(to)
1876 }
1877
1878 /// Cast the tensor to a different dtype using explicit dtype projection.
1879 ///
1880 /// `cast` may truncate, narrow precision, project complex values to their
1881 /// real component, or use boolean truthiness where the backend supports the
1882 /// requested projection.
1883 ///
1884 /// # Examples
1885 ///
1886 /// ```rust
1887 /// use tenferro_runtime::DType;
1888 /// # use tenferro_runtime::TracedTensor;
1889 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.2_f64, -2.8]).unwrap();
1890 ///
1891 /// let y = x.cast(DType::I32).unwrap();
1892 /// ```
1893 ///
1894 /// # Errors
1895 ///
1896 /// Returns [`Error::TensorRuntime`] containing
1897 /// `UnsupportedDTypeConversion` when the requested input-to-target
1898 /// projection is not supported, or [`Error::RuntimeStateSource`] when
1899 /// converted-output metadata cannot be registered.
1900 pub fn cast(&self, to: DType) -> Result<TracedTensor> {
1901 if self.dtype == to {
1902 return Ok(self.clone());
1903 }
1904
1905 apply_unary_with_dtype(
1906 StdTensorOp::Convert {
1907 from: self.dtype,
1908 to,
1909 },
1910 self,
1911 self.rank,
1912 self.shape_hint.clone(),
1913 to,
1914 )
1915 }
1916
1917 /// Generalized tensor contraction.
1918 ///
1919 /// The output layout is `[lhs free..., rhs free..., batch...]`: batch axes
1920 /// come last (see [`DotGeneralConfig`]).
1921 ///
1922 /// # Examples
1923 ///
1924 /// ```rust
1925 /// # use tenferro_runtime::{DotGeneralConfig, TracedTensor};
1926 /// # let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
1927 /// # let b = TracedTensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap();
1928 /// # let config = DotGeneralConfig {
1929 /// # lhs_contracting_dims: [1].as_slice().into(),
1930 /// # rhs_contracting_dims: [0].as_slice().into(),
1931 /// # lhs_batch_dims: [].as_slice().into(),
1932 /// # rhs_batch_dims: [].as_slice().into(),
1933 /// # };
1934 /// let y = a.dot_general(&b, config)?;
1935 /// # Ok::<(), tenferro_runtime::Error>(())
1936 /// ```
1937 ///
1938 /// # Errors
1939 ///
1940 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
1941 /// `DuplicateAxis`, or `AxisRoleConflict` when dimension numbers are
1942 /// invalid for the operand ranks, and [`Error::RuntimeStateSource`] when
1943 /// output metadata cannot be registered.
1944 ///
1945 /// # Deferred errors
1946 ///
1947 /// Contracting or batch dimensions whose sizes are symbolic are checked
1948 /// when concrete inputs reach compilation or execution. A mismatch is
1949 /// returned as [`Error::TensorRuntime`] with a typed `ShapeMismatch`
1950 /// source and its corresponding [`ErrorPhase`].
1951 pub fn dot_general(
1952 &self,
1953 other: &TracedTensor,
1954 config: DotGeneralConfig,
1955 ) -> Result<TracedTensor> {
1956 config
1957 .validate_dims_with_ranks(self.rank, other.rank)
1958 .map_err(|err| graph_tensor_error("dot_general", err))?;
1959 let lhs_free: Vec<usize> = (0..self.rank)
1960 .filter(|d| {
1961 !config.lhs_contracting_dims.contains(d) && !config.lhs_batch_dims.contains(d)
1962 })
1963 .collect();
1964 let rhs_free: Vec<usize> = (0..other.rank)
1965 .filter(|d| {
1966 !config.rhs_contracting_dims.contains(d) && !config.rhs_batch_dims.contains(d)
1967 })
1968 .collect();
1969 let out_rank = config.lhs_batch_dims.len() + lhs_free.len() + rhs_free.len();
1970 let out_shape_hint = match (&self.shape_hint, &other.shape_hint) {
1971 (Some(lhs_shape), Some(rhs_shape)) => {
1972 let mut out_shape = Vec::with_capacity(out_rank);
1973 for &d in &lhs_free {
1974 out_shape.push(lhs_shape[d].clone());
1975 }
1976 for &d in &rhs_free {
1977 out_shape.push(rhs_shape[d].clone());
1978 }
1979 for &d in &config.lhs_batch_dims {
1980 out_shape.push(lhs_shape[d].clone());
1981 }
1982 Some(out_shape)
1983 }
1984 _ => None,
1985 };
1986
1987 apply_binary(
1988 StdTensorOp::DotGeneral { config },
1989 self,
1990 other,
1991 out_rank,
1992 out_shape_hint,
1993 )
1994 }
1995
1996 /// Matrix multiplication for rank-2 tensors.
1997 ///
1998 /// # Errors
1999 ///
2000 /// Returns [`Error::Validation`] with `RankMismatch` when either operand is
2001 /// not rank 2, `ShapeMismatch::ContractedDimensions` when known matrix
2002 /// dimensions differ, or [`Error::RuntimeStateSource`] when output
2003 /// metadata cannot be registered.
2004 ///
2005 /// # Deferred errors
2006 ///
2007 /// If either contracted dimension is symbolic, the mismatch is discovered
2008 /// at compilation or execution and returned as [`Error::TensorRuntime`]
2009 /// with its typed `ShapeMismatch` source.
2010 pub fn matmul(&self, other: &TracedTensor) -> Result<TracedTensor> {
2011 if self.rank != 2 {
2012 return Err(graph_validation(
2013 "TracedTensor::matmul",
2014 ValidationError::RankMismatch {
2015 expected: 2,
2016 actual: self.rank,
2017 },
2018 ));
2019 }
2020 if other.rank != 2 {
2021 return Err(graph_validation(
2022 "TracedTensor::matmul",
2023 ValidationError::RankMismatch {
2024 expected: 2,
2025 actual: other.rank,
2026 },
2027 ));
2028 }
2029 if let (Some(lhs_shape), Some(rhs_shape)) = (&self.shape_hint, &other.shape_hint)
2030 && let (Some(lhs_cols), Some(rhs_rows)) =
2031 (lhs_shape[1].constant_value(), rhs_shape[0].constant_value())
2032 && lhs_cols != rhs_rows
2033 {
2034 return Err(graph_validation(
2035 "TracedTensor::matmul",
2036 ShapeMismatch::ContractedDimensions {
2037 lhs_axis: 1,
2038 lhs_size: lhs_cols,
2039 rhs_axis: 0,
2040 rhs_size: rhs_rows,
2041 },
2042 ));
2043 }
2044 self.dot_general(
2045 other,
2046 DotGeneralConfig {
2047 lhs_contracting_dims: [1].as_slice().into(),
2048 rhs_contracting_dims: [0].as_slice().into(),
2049 lhs_batch_dims: [].as_slice().into(),
2050 rhs_batch_dims: [].as_slice().into(),
2051 },
2052 )
2053 }
2054
2055 /// Sum over the given axes.
2056 ///
2057 /// # Examples
2058 ///
2059 /// ```rust
2060 /// # use tenferro_runtime::TracedTensor;
2061 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2062 /// let total = x.reduce_sum(None)?;
2063 /// let rows = x.reduce_sum(Some(&[1]))?;
2064 /// let identity = x.reduce_sum(Some(&[]))?;
2065 /// assert_eq!(total.rank, 0);
2066 /// assert_eq!(rows.rank, 1);
2067 /// assert_eq!(identity.rank, 2);
2068 /// # Ok::<(), tenferro_runtime::Error>(())
2069 /// ```
2070 ///
2071 /// # Errors
2072 ///
2073 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2074 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
2075 /// or [`Error::RuntimeStateSource`] when output metadata cannot be
2076 /// registered.
2077 pub fn reduce_sum(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
2078 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
2079 let (out_rank, out_shape_hint) =
2080 reduction_output_meta(self, &axes, "TracedTensor::reduce_sum")?;
2081 apply_unary(
2082 StdTensorOp::ReduceSum { axes },
2083 self,
2084 out_rank,
2085 out_shape_hint,
2086 )
2087 }
2088
2089 /// Sum elementwise squares over the requested axes.
2090 ///
2091 /// Each value is squared in its input dtype before reduction. The initial
2092 /// supported dtypes are `f32` and `f64`; other dtypes return a typed
2093 /// unsupported error during execution. `None` reduces every axis, like
2094 /// the rest of the reduction family; `Some(&[])` returns the elementwise
2095 /// square without reducing rank.
2096 ///
2097 /// This operation is useful when the squared sum is needed directly. Use
2098 /// the linalg norm APIs when a square root or complex magnitude semantics
2099 /// are required.
2100 ///
2101 /// # Errors
2102 ///
2103 /// Returns a typed validation error for invalid axes or a typed
2104 /// runtime-state error while registering output metadata.
2105 ///
2106 /// # Deferred errors
2107 ///
2108 /// Unsupported dtypes and backend execution failures are reported when the
2109 /// compiled graph is executed.
2110 /// # Examples
2111 ///
2112 /// ```rust
2113 /// # use tenferro_runtime::TracedTensor;
2114 /// # let x = TracedTensor::from_vec_col_major(
2115 /// # vec![2, 2],
2116 /// # vec![1.0_f64, 2.0, 3.0, 4.0],
2117 /// # )?;
2118 /// let squares = x.reduce_sum_squares(Some(&[1]))?;
2119 /// assert_eq!(squares.rank, 1);
2120 /// let total = x.reduce_sum_squares(None)?;
2121 /// assert_eq!(total.rank, 0);
2122 /// # Ok::<(), tenferro_runtime::Error>(())
2123 /// ```
2124 pub fn reduce_sum_squares(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
2125 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
2126 let (out_rank, out_shape_hint) =
2127 reduction_output_meta(self, &axes, "TracedTensor::reduce_sum_squares")?;
2128 apply_unary(
2129 StdTensorOp::ReduceSumSquares { axes },
2130 self,
2131 out_rank,
2132 out_shape_hint,
2133 )
2134 }
2135
2136 /// Reduce by taking the maximum along the given axes.
2137 ///
2138 /// Used by tropical (max-plus) compositions: a max-plus reduction over
2139 /// an axis is `ReduceMax` on that axis.
2140 ///
2141 /// # Examples
2142 ///
2143 /// ```rust
2144 /// # use tenferro_runtime::TracedTensor;
2145 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2146 /// let y = x.reduce_max(Some(&[0]))?;
2147 /// # Ok::<(), tenferro_runtime::Error>(())
2148 /// ```
2149 ///
2150 /// # Errors
2151 ///
2152 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2153 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
2154 /// [`Error::Unsupported`] when a non-empty maximum reduction receives a
2155 /// complex dtype, or [`Error::RuntimeStateSource`] when output metadata
2156 /// cannot be registered.
2157 pub fn reduce_max(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
2158 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
2159 let (out_rank, out_shape_hint) =
2160 reduction_output_meta(self, &axes, "TracedTensor::reduce_max")?;
2161 try_apply_unary(
2162 StdTensorOp::ReduceMax { axes },
2163 self,
2164 out_rank,
2165 out_shape_hint,
2166 "TracedTensor::reduce_max",
2167 )
2168 }
2169
2170 /// Reduce by taking the minimum along the given axes.
2171 ///
2172 /// Used by tropical (min-plus) compositions: a min-plus reduction over
2173 /// an axis is `ReduceMin` on that axis.
2174 ///
2175 /// # Examples
2176 ///
2177 /// ```rust
2178 /// # use tenferro_runtime::TracedTensor;
2179 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2180 /// let y = x.reduce_min(Some(&[0]))?;
2181 /// # Ok::<(), tenferro_runtime::Error>(())
2182 /// ```
2183 ///
2184 /// # Errors
2185 ///
2186 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2187 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
2188 /// [`Error::Unsupported`] when a non-empty minimum reduction receives a
2189 /// complex dtype, or [`Error::RuntimeStateSource`] when output metadata
2190 /// cannot be registered.
2191 pub fn reduce_min(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
2192 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
2193 let (out_rank, out_shape_hint) =
2194 reduction_output_meta(self, &axes, "TracedTensor::reduce_min")?;
2195 try_apply_unary(
2196 StdTensorOp::ReduceMin { axes },
2197 self,
2198 out_rank,
2199 out_shape_hint,
2200 "TracedTensor::reduce_min",
2201 )
2202 }
2203
2204 /// Reduce by taking the product along the given axes.
2205 ///
2206 /// # Examples
2207 ///
2208 /// ```rust
2209 /// # use tenferro_runtime::TracedTensor;
2210 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2211 /// let y = x.reduce_prod(Some(&[0]))?;
2212 /// # Ok::<(), tenferro_runtime::Error>(())
2213 /// ```
2214 ///
2215 /// # Errors
2216 ///
2217 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2218 /// outside the input rank or `DuplicateAxis` when `axes` repeats an axis,
2219 /// or [`Error::RuntimeStateSource`] when output metadata cannot be
2220 /// registered.
2221 pub fn reduce_prod(&self, axes: Option<&[usize]>) -> Result<TracedTensor> {
2222 let axes = axes.map_or_else(|| (0..self.rank).collect(), <[usize]>::to_vec);
2223 let (out_rank, out_shape_hint) =
2224 reduction_output_meta(self, &axes, "TracedTensor::reduce_prod")?;
2225 apply_unary(
2226 StdTensorOp::ReduceProd { axes },
2227 self,
2228 out_rank,
2229 out_shape_hint,
2230 )
2231 }
2232
2233 /// Reshape without changing element order.
2234 ///
2235 /// # Examples
2236 ///
2237 /// ```rust
2238 /// # use tenferro_runtime::TracedTensor;
2239 /// # let x = TracedTensor::from_vec_col_major(vec![4], vec![1.0_f64; 4]).unwrap();
2240 /// for y in [x.reshape([2, 2])?, x.reshape(vec![2, 2])?, x.reshape(&[2, 2][..])?] {
2241 /// assert_eq!(y.try_concrete_shape(), Some(vec![2, 2]));
2242 /// }
2243 /// assert!(x.reshape([3]).is_err());
2244 /// # Ok::<(), tenferro_runtime::Error>(())
2245 /// ```
2246 ///
2247 /// # Errors
2248 ///
2249 /// Returns [`Error::Validation`] with `ShapeMismatch::ReshapeElementCount`
2250 /// when a concrete input has a different element count, or
2251 /// `IntegerOverflow` when the target shape product overflows `usize`.
2252 pub fn reshape(&self, shape: impl IntoShapeVec) -> Result<TracedTensor> {
2253 let shape = shape.into_shape_vec();
2254 validate_concrete_reshape_shape(self, &shape)?;
2255 apply_unary_with_dtype(
2256 StdTensorOp::Reshape {
2257 to_shape: DimExpr::from_concrete(&shape),
2258 },
2259 self,
2260 shape.len(),
2261 Some(shape.iter().copied().map(SymDim::from).collect()),
2262 self.dtype,
2263 )
2264 }
2265
2266 /// Return a symbolic expression for the size of one axis, suitable as
2267 /// an `InputDim`-style reference when composing with
2268 /// [`TracedTensor::reshape_sym`].
2269 ///
2270 /// Semantics: if this tensor's `shape_hint` has a symbolic
2271 /// (non-constant) entry for `axis`, that entry is returned
2272 /// verbatim. Otherwise — including when `shape_hint[axis]` is a
2273 /// concrete `SymDim::Concrete(n)` — a
2274 /// `SymDim::tensor_axis(self.id, axis)` reference is returned so the
2275 /// resulting graph remains shape-polymorphic if the same graph is
2276 /// later evaluated against a differently-shaped binding.
2277 ///
2278 /// For a canonical "what is the size of this axis?" query that
2279 /// reports the concrete size when it is known, prefer
2280 /// [`Self::axis_sym_dim`].
2281 ///
2282 /// # Examples
2283 ///
2284 /// ```rust
2285 /// # use tenferro_runtime::TracedTensor;
2286 /// # let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2287 /// let rows = x.sym_size(0)?;
2288 /// let cols = x.sym_size(1)?;
2289 /// let y = x.reshape_sym(&[rows * cols]).unwrap();
2290 /// # Ok::<(), tenferro_runtime::Error>(())
2291 /// ```
2292 ///
2293 /// # Errors
2294 ///
2295 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2296 /// outside this tensor's rank.
2297 pub fn sym_size(&self, axis: usize) -> Result<SymDim> {
2298 validate_traced_axis(self, axis, "TracedTensor::sym_size")?;
2299 Ok(self
2300 .shape_hint
2301 .as_ref()
2302 .and_then(|shape| shape.get(axis))
2303 .filter(|dim| dim.constant_value().is_none())
2304 .cloned()
2305 .unwrap_or_else(|| SymDim::tensor_axis(self.id, axis)))
2306 }
2307
2308 /// Return the canonical `SymDim` for `axis` — the concrete
2309 /// `SymDim::Concrete(n)` when the size is known, otherwise a symbolic
2310 /// expression identifying this tensor's axis.
2311 ///
2312 /// Unlike [`Self::sym_size`], this method does **not** rewrite
2313 /// concrete axes into `TensorAxis` references. It is the accessor
2314 /// external composition wrappers should use when building mixed
2315 /// concrete/symbolic target shapes for operations like
2316 /// [`Self::broadcast_in_dim_sym`].
2317 ///
2318 /// # Examples
2319 ///
2320 /// ```
2321 /// use tenferro_tensor::DType;
2322 /// use tenferro_runtime::TracedTensor;
2323 ///
2324 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2325 /// // Concrete axis: reports the constant size.
2326 /// assert_eq!(a.axis_sym_dim(0).unwrap().constant_value(), Some(2));
2327 ///
2328 /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
2329 /// // Fully symbolic leaf: reports a TensorAxis reference.
2330 /// assert!(b.axis_sym_dim(0).unwrap().constant_value().is_none());
2331 /// ```
2332 ///
2333 /// # Errors
2334 ///
2335 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2336 /// outside this tensor's rank.
2337 pub fn axis_sym_dim(&self, axis: usize) -> Result<SymDim> {
2338 validate_traced_axis(self, axis, "TracedTensor::axis_sym_dim")?;
2339 match self.shape_hint.as_ref().and_then(|shape| shape.get(axis)) {
2340 Some(dim) => Ok(dim.clone()),
2341 None => Ok(SymDim::tensor_axis(self.id, axis)),
2342 }
2343 }
2344
2345 /// Return the full symbolic shape of this tensor when a `shape_hint`
2346 /// is present.
2347 ///
2348 /// Returns `None` for fully-symbolic placeholders produced via
2349 /// [`Self::input_symbolic_shape`] (where `shape_hint` is intentionally
2350 /// absent). For those, build the shape axis-by-axis via
2351 /// [`Self::axis_sym_dim`].
2352 ///
2353 /// # Examples
2354 ///
2355 /// ```
2356 /// use tenferro_tensor::DType;
2357 /// use tenferro_runtime::TracedTensor;
2358 ///
2359 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2360 /// assert!(a.sym_shape().is_some());
2361 /// assert_eq!(a.sym_shape().unwrap().len(), 2);
2362 ///
2363 /// let b = TracedTensor::input_symbolic_shape(DType::F64, 2).unwrap();
2364 /// assert!(b.sym_shape().is_none());
2365 /// ```
2366 pub fn sym_shape(&self) -> Option<&[SymDim]> {
2367 self.shape_hint.as_deref()
2368 }
2369
2370 /// Reshape using symbolic dimensions derived from traced tensor axes.
2371 ///
2372 /// # Examples
2373 ///
2374 /// ```rust
2375 /// # use tenferro_runtime::TracedTensor;
2376 /// # let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2377 /// let rows = x.sym_size(0)?;
2378 /// let cols = x.sym_size(1)?;
2379 /// let y = x.reshape_sym(&[rows * cols]).unwrap();
2380 /// # Ok::<(), tenferro_runtime::Error>(())
2381 /// ```
2382 ///
2383 /// # Errors
2384 ///
2385 /// Returns [`Error::SymbolicShapeConversion`] when a supplied symbolic
2386 /// dimension cannot be mapped to this graph, or [`Error::RuntimeStateSource`]
2387 /// when result metadata cannot be registered.
2388 ///
2389 /// # Deferred errors
2390 ///
2391 /// Element-count compatibility for symbolic dimensions is checked when
2392 /// concrete inputs reach compilation or execution. A mismatch is returned
2393 /// as [`Error::TensorRuntime`] with a typed `ShapeMismatch` source.
2394 pub fn reshape_sym(&self, shape: &[SymDim]) -> Result<TracedTensor> {
2395 let tensor_map = [(self.id, 0usize)];
2396 let to_shape = shape
2397 .iter()
2398 .map(|dim| {
2399 dim.to_dim_expr(&tensor_map)
2400 .map_err(|source| Error::SymbolicShapeConversion {
2401 op: "TracedTensor::reshape_sym",
2402 phase: ErrorPhase::GraphBuild,
2403 source,
2404 })
2405 })
2406 .collect::<Result<Vec<_>>>()?;
2407 let out_shape_hint = Some(shape.to_vec());
2408 apply_unary(
2409 StdTensorOp::Reshape { to_shape },
2410 self,
2411 shape.len(),
2412 out_shape_hint,
2413 )
2414 }
2415
2416 /// Broadcast into a larger shape with explicit dimension placement.
2417 ///
2418 /// # Examples
2419 ///
2420 /// ```rust
2421 /// # use tenferro_runtime::TracedTensor;
2422 /// # let x = TracedTensor::from_vec_col_major(vec![3], vec![1.0_f64; 3]).unwrap();
2423 /// let y = x.broadcast_in_dim(&[2, 3], &[1])?;
2424 /// # Ok::<(), tenferro_runtime::Error>(())
2425 /// ```
2426 ///
2427 /// # Errors
2428 ///
2429 /// Returns [`Error::Validation`] with `RankMismatch` when `dims` does not
2430 /// have one entry per input axis, `AxisOutOfBounds` or `DuplicateAxis` for
2431 /// an invalid output mapping, or `InvalidArgument` when known dimensions
2432 /// cannot broadcast. [`Error::RuntimeStateSource`] reports failure to
2433 /// register the result metadata.
2434 pub fn broadcast_in_dim(&self, shape: &[usize], dims: &[usize]) -> Result<TracedTensor> {
2435 let out_shape_hint: Vec<SymDim> = shape.iter().copied().map(SymDim::from).collect();
2436 validate_broadcast_in_dim_args(
2437 self,
2438 &out_shape_hint,
2439 dims,
2440 "TracedTensor::broadcast_in_dim",
2441 )?;
2442 apply_unary(
2443 StdTensorOp::BroadcastInDim {
2444 shape: DimExpr::from_concrete(shape),
2445 dims: dims.to_vec(),
2446 },
2447 self,
2448 shape.len(),
2449 Some(out_shape_hint),
2450 )
2451 }
2452
2453 /// Broadcast into a symbolic target shape with explicit dimension
2454 /// placement.
2455 ///
2456 /// Unlike [`Self::broadcast_in_dim`], each axis of `shape` is a
2457 /// [`SymDim`], so the target shape can mix concrete sizes (via
2458 /// `SymDim::from(n)`) with symbolic references to this tensor's axes
2459 /// (via [`Self::axis_sym_dim`]) or to axes of other traced tensors.
2460 ///
2461 /// When `shape` contains a `SymDim` that references a traced tensor
2462 /// other than `self`, the referenced tensor(s) must be supplied in
2463 /// `shape_refs`. They are wired into the built op as auxiliary
2464 /// shape-reference inputs — the op does not read their data, only
2465 /// their runtime shape. `shape_refs` must be listed in the same order
2466 /// in which their tensor IDs first appear when walking `shape` after
2467 /// any references to `self`. Usually the simplest correct thing is to
2468 /// pass each unique non-self reference tensor once.
2469 ///
2470 /// # Examples
2471 ///
2472 /// ```
2473 /// use tenferro_runtime::TracedTensor;
2474 ///
2475 /// let a = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2476 /// let b = TracedTensor::from_vec_col_major(vec![3, 4], vec![1.0_f64; 12]).unwrap();
2477 /// let m = a.axis_sym_dim(0)?;
2478 /// let k = a.axis_sym_dim(1)?;
2479 /// let n = b.axis_sym_dim(1)?;
2480 /// // Broadcast `a[m, k]` to `[m, k, n]`, placing `a`'s axes at 0, 1
2481 /// // and taking `n` from `b` as an auxiliary shape reference.
2482 /// let a_b = a.broadcast_in_dim_sym(&[m, k, n], &[0, 1], &[&b])?;
2483 /// assert_eq!(a_b.rank, 3);
2484 /// # Ok::<(), tenferro_runtime::Error>(())
2485 /// ```
2486 ///
2487 /// # Errors
2488 ///
2489 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2490 /// `DuplicateAxis`, or `InvalidArgument` when the output mapping or shape
2491 /// references are invalid, [`Error::SymbolicShapeConversion`] for an
2492 /// unmappable symbolic dimension, or [`Error::RuntimeStateSource`] when
2493 /// metadata cannot be registered.
2494 ///
2495 /// # Deferred errors
2496 ///
2497 /// If a symbolic output dimension is smaller than a non-unit input axis,
2498 /// the concrete broadcast check is deferred to compilation or execution
2499 /// and is returned as [`Error::TensorRuntime`] with a typed validation
2500 /// source.
2501 pub fn broadcast_in_dim_sym(
2502 &self,
2503 shape: &[SymDim],
2504 dims: &[usize],
2505 shape_refs: &[&TracedTensor],
2506 ) -> Result<TracedTensor> {
2507 validate_broadcast_in_dim_args(self, shape, dims, "TracedTensor::broadcast_in_dim_sym")?;
2508
2509 // Build a dedup'd list of shape-reference tensors (first occurrence
2510 // wins) and index them starting at 1 — the primary input `self`
2511 // is at 0.
2512 let mut dedup_refs: Vec<&TracedTensor> = Vec::with_capacity(shape_refs.len());
2513 let mut tensor_map: Vec<(u64, usize)> = vec![(self.id, 0)];
2514 for &t in shape_refs {
2515 if !tensor_map.iter().any(|(id, _)| *id == t.id) {
2516 let idx = tensor_map.len();
2517 tensor_map.push((t.id, idx));
2518 dedup_refs.push(t);
2519 }
2520 }
2521
2522 let to_shape: Vec<DimExpr> = shape
2523 .iter()
2524 .map(|dim| {
2525 dim.to_dim_expr(&tensor_map)
2526 .map_err(|source| Error::SymbolicShapeConversion {
2527 op: "broadcast_in_dim_sym",
2528 phase: ErrorPhase::GraphBuild,
2529 source,
2530 })
2531 })
2532 .collect::<Result<Vec<_>>>()?;
2533
2534 // Trim auxiliary shape-reference inputs down to those actually
2535 // used by the generated `DimExpr`s. If the target shape resolved
2536 // to all constants (the concrete-shape case) the op is a plain
2537 // unary broadcast with no extra parents. Otherwise the op needs
2538 // a contiguous prefix of shape-ref inputs covering every
2539 // referenced `input_idx`.
2540 let max_used_idx = DimExpr::max_input_idx_all(&to_shape).unwrap_or(0);
2541 let used_refs: Vec<&TracedTensor> = dedup_refs.into_iter().take(max_used_idx).collect();
2542
2543 let out_shape_hint = Some(shape.to_vec());
2544 apply_unary_with_shape_refs(
2545 StdTensorOp::BroadcastInDim {
2546 shape: to_shape,
2547 dims: dims.to_vec(),
2548 },
2549 self,
2550 &used_refs,
2551 shape.len(),
2552 out_shape_hint,
2553 )
2554 }
2555
2556 /// Slice with explicit start, limit, and stride per axis.
2557 ///
2558 /// # Errors
2559 ///
2560 /// Returns [`Error::Validation`] with `RankMismatch` when the start/limit/
2561 /// stride vectors do not match the input rank, `InvalidSliceStep` when a
2562 /// stride is zero, `InvalidSliceBounds` when a limit precedes its start or
2563 /// exceeds a concrete input extent, or [`Error::RuntimeStateSource`] when
2564 /// output metadata cannot be registered.
2565 pub fn slice(&self, config: SliceConfig) -> Result<TracedTensor> {
2566 let op = StdTensorOp::Slice(config);
2567 let (out_rank, out_shape_hint) =
2568 infer_traced_single_output_shape("TracedTensor::slice", &op, &[self])?;
2569 apply_unary(op, self, out_rank, out_shape_hint)
2570 }
2571
2572 /// Pad with zeros using StableHLO-style edge and interior padding.
2573 ///
2574 /// # Errors
2575 ///
2576 /// Returns [`Error::Validation`] with `RankMismatch` when padding vectors
2577 /// do not match the input rank, `InvalidArgument` for negative interior
2578 /// padding, or `IntegerOverflow` when the padded extent exceeds `usize`.
2579 /// [`Error::RuntimeStateSource`] is returned when output metadata cannot be
2580 /// registered.
2581 pub fn pad(&self, config: PadConfig) -> Result<TracedTensor> {
2582 let op = StdTensorOp::Pad(config);
2583 let (out_rank, out_shape_hint) =
2584 infer_traced_single_output_shape("TracedTensor::pad", &op, &[self])?;
2585 apply_unary(op, self, out_rank, out_shape_hint)
2586 }
2587
2588 /// Reverse the order of elements along the requested axes.
2589 ///
2590 /// # Errors
2591 ///
2592 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when an axis is
2593 /// outside the input rank or `DuplicateAxis` when `axes` repeats one, or
2594 /// [`Error::RuntimeStateSource`] when result metadata cannot be
2595 /// registered.
2596 pub fn reverse(&self, axes: &[usize]) -> Result<TracedTensor> {
2597 validate_traced_axes(self.rank, axes, "TracedTensor::reverse")?;
2598 apply_unary(
2599 StdTensorOp::Reverse {
2600 axes: axes.to_vec(),
2601 },
2602 self,
2603 self.rank,
2604 self.shape_hint.clone(),
2605 )
2606 }
2607
2608 /// Gather slices from `self` using integer start indices.
2609 ///
2610 /// # Errors
2611 ///
2612 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2613 /// `DuplicateAxis`, or `ShapeMismatch` when indices or the gather
2614 /// configuration is incompatible with the input, and
2615 /// [`Error::RuntimeStateSource`] when output metadata cannot be
2616 /// registered.
2617 ///
2618 /// # Clamping
2619 ///
2620 /// Index values are clamped with StableHLO gather clamp semantics: an
2621 /// out-of-range or negative index is clamped into `[0, dim - window]` rather than rejected.
2622 /// Only a `slice_sizes` window that exceeds its operand dimension is rejected,
2623 /// which is a graph-build failure.
2624 pub fn gather(&self, indices: &TracedTensor, config: GatherConfig) -> Result<TracedTensor> {
2625 let op = StdTensorOp::Gather(config);
2626 let (out_rank, out_shape_hint) =
2627 infer_traced_single_output_shape("TracedTensor::gather", &op, &[self, indices])?;
2628 apply_binary_preserve_input_dtypes(op, self, indices, out_rank, out_shape_hint, self.dtype)
2629 }
2630
2631 /// Scatter updates into `self` using StableHLO scatter semantics.
2632 ///
2633 /// # Errors
2634 ///
2635 /// Returns [`Error::Validation`] with `RankMismatch`, `AxisOutOfBounds`,
2636 /// `DuplicateAxis`, or `ShapeMismatch` when indices, updates, or the
2637 /// scatter configuration is incompatible, [`Error::TensorRuntime`] with
2638 /// `UnsupportedDTypeConversion` when dtype promotion cannot be
2639 /// represented, or [`Error::RuntimeStateSource`] when output metadata
2640 /// cannot be registered.
2641 ///
2642 /// # Clamping
2643 ///
2644 /// Index *values* clamp with StableHLO scatter semantics: an out-of-range or
2645 /// negative index is clamped rather than rejected, into the operand's extent.
2646 ///
2647 /// # Deferred errors
2648 ///
2649 /// The index and update *shapes* and the scatter configuration are checked
2650 /// after binding: an incompatible one is returned as [`Error::TensorRuntime`]
2651 /// with its typed validation source and [`ErrorPhase::Execution`]. An update
2652 /// window that exceeds the operand is the one exception: the CPU backend
2653 /// rejects it, while the CUDA kernel drops the update, so it is not part of
2654 /// this contract.
2655 pub fn scatter(
2656 &self,
2657 indices: &TracedTensor,
2658 updates: &TracedTensor,
2659 config: ScatterConfig,
2660 ) -> Result<TracedTensor> {
2661 let op = StdTensorOp::Scatter(config);
2662 let (out_rank, out_shape_hint) = infer_traced_single_output_shape(
2663 "TracedTensor::scatter",
2664 &op,
2665 &[self, indices, updates],
2666 )?;
2667 let out_dtype = crate::shape_infer::promote_dtype(self.dtype, updates.dtype);
2668 let operand = if self.dtype != out_dtype {
2669 self.cast(out_dtype)?
2670 } else {
2671 self.clone()
2672 };
2673 let updates = if updates.dtype != out_dtype {
2674 updates.cast(out_dtype)?
2675 } else {
2676 updates.clone()
2677 };
2678 apply_ternary_with_output_dtype(
2679 op,
2680 &operand,
2681 indices,
2682 &updates,
2683 out_rank,
2684 out_shape_hint,
2685 out_dtype,
2686 )
2687 }
2688
2689 /// Slice using runtime start indices.
2690 ///
2691 /// # Errors
2692 ///
2693 /// Returns [`Error::Validation`] with `RankMismatch` when `sizes` has a rank
2694 /// other than the operand's, with `ShapeMismatch` when a `sizes` window
2695 /// exceeds a concrete operand dimension, and with `InvalidArgument` when
2696 /// `starts` is not a rank-1 vector holding one entry per operand axis. These
2697 /// are graph-build checks (`ErrorPhase::GraphBuild`, #2160) mirroring the
2698 /// `gather` sibling. The eager surface reaches the same conditions at
2699 /// execution, where the backends name the oversized window `InvalidArgument`
2700 /// (`"configuration"` on CPU, `"slice_sizes"` on CUDA) rather than
2701 /// `ShapeMismatch`. Also returns [`Error::RuntimeStateSource`] when output
2702 /// metadata cannot be registered.
2703 ///
2704 /// # Clamping
2705 ///
2706 /// Start *values* clamp with StableHLO `DynamicSlice` semantics: an
2707 /// out-of-range or negative start is clamped rather than rejected, into
2708 /// `[0, dim - window]`. Only the `slice_sizes` window itself is rejected, and
2709 /// it is rejected when the graph is built. An operand extent or a `starts`
2710 /// length that is still symbolic at that point defers its check to the phase
2711 /// that supplies the extent, and the reported [`ErrorPhase`] says which.
2712 pub fn dynamic_slice(&self, starts: &TracedTensor, sizes: &[usize]) -> Result<TracedTensor> {
2713 let op = StdTensorOp::DynamicSlice {
2714 slice_sizes: sizes.to_vec(),
2715 };
2716 let (out_rank, out_shape_hint) =
2717 infer_traced_single_output_shape("TracedTensor::dynamic_slice", &op, &[self, starts])?;
2718 apply_binary_preserve_input_dtypes(op, self, starts, out_rank, out_shape_hint, self.dtype)
2719 }
2720
2721 /// Keep the lower triangle and zero the rest.
2722 ///
2723 /// # Examples
2724 ///
2725 /// ```rust
2726 /// # use tenferro_runtime::TracedTensor;
2727 /// let matrix = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4])?;
2728 /// let lower = matrix.tril(0)?;
2729 /// assert_eq!(lower.rank, 2);
2730 /// # Ok::<(), tenferro_runtime::Error>(())
2731 /// ```
2732 ///
2733 /// # Errors
2734 ///
2735 /// Returns [`Error::RuntimeStateSource`] when traced output metadata
2736 /// registration is unavailable or inconsistent with the graph.
2737 pub fn tril(&self, k: i64) -> Result<TracedTensor> {
2738 apply_unary(
2739 StdTensorOp::Tril { k },
2740 self,
2741 self.rank,
2742 self.shape_hint.clone(),
2743 )
2744 }
2745
2746 /// Keep the upper triangle and zero the rest.
2747 ///
2748 /// # Examples
2749 ///
2750 /// ```rust
2751 /// # use tenferro_runtime::TracedTensor;
2752 /// let matrix = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4])?;
2753 /// let upper = matrix.triu(0)?;
2754 /// assert_eq!(upper.rank, 2);
2755 /// # Ok::<(), tenferro_runtime::Error>(())
2756 /// ```
2757 ///
2758 /// # Errors
2759 ///
2760 /// Returns [`Error::RuntimeStateSource`] when traced output metadata
2761 /// registration is unavailable or inconsistent with the graph.
2762 pub fn triu(&self, k: i64) -> Result<TracedTensor> {
2763 apply_unary(
2764 StdTensorOp::Triu { k },
2765 self,
2766 self.rank,
2767 self.shape_hint.clone(),
2768 )
2769 }
2770
2771 /// Permute tensor axes.
2772 ///
2773 /// # Examples
2774 ///
2775 /// ```rust
2776 /// # use tenferro_runtime::TracedTensor;
2777 /// # let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64; 6]).unwrap();
2778 /// let y = x.transpose(&[1, 0])?;
2779 /// # Ok::<(), tenferro_runtime::Error>(())
2780 /// ```
2781 ///
2782 /// # Errors
2783 ///
2784 /// Returns [`Error::Validation`] with `InvalidPermutationLength`,
2785 /// `AxisOutOfBounds`, or `DuplicateAxis` when `perm` is not a valid
2786 /// permutation of the tensor axes, or [`Error::RuntimeStateSource`] when
2787 /// output metadata registration fails.
2788 pub fn transpose(&self, perm: &[usize]) -> Result<TracedTensor> {
2789 validate_traced_perm(self.rank, perm, "TracedTensor::transpose")?;
2790 let out_shape_hint = self
2791 .shape_hint
2792 .as_ref()
2793 .map(|shape| perm.iter().map(|&p| shape[p].clone()).collect());
2794 apply_unary(
2795 StdTensorOp::Transpose {
2796 perm: perm.to_vec(),
2797 },
2798 self,
2799 self.rank,
2800 out_shape_hint,
2801 )
2802 }
2803
2804 /// Extract the diagonal along two axes.
2805 ///
2806 /// # Examples
2807 ///
2808 /// ```rust
2809 /// # use tenferro_runtime::TracedTensor;
2810 /// # let x = TracedTensor::from_vec_col_major(vec![2, 2], vec![1.0_f64; 4]).unwrap();
2811 /// let y = x.extract_diag(0, 1)?;
2812 /// # Ok::<(), tenferro_runtime::Error>(())
2813 /// ```
2814 ///
2815 /// # Errors
2816 ///
2817 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when either axis
2818 /// is outside the input rank or `InvalidArgument` when `axis_a == axis_b`.
2819 pub fn extract_diag(&self, axis_a: usize, axis_b: usize) -> Result<TracedTensor> {
2820 validate_traced_axis(self, axis_a, "TracedTensor::extract_diag")?;
2821 validate_traced_axis(self, axis_b, "TracedTensor::extract_diag")?;
2822 if axis_a == axis_b {
2823 return Err(graph_invalid_argument(
2824 "TracedTensor::extract_diag",
2825 "axes",
2826 "diagonal axes must be distinct",
2827 ));
2828 }
2829 let op = StdTensorOp::ExtractDiag { axis_a, axis_b };
2830 let (out_rank, out_shape_hint) =
2831 infer_traced_single_output_shape("TracedTensor::extract_diag", &op, &[self])?;
2832 apply_unary(op, self, out_rank, out_shape_hint)
2833 }
2834
2835 /// Embed a vector or lower-rank tensor along a diagonal.
2836 ///
2837 /// # Examples
2838 ///
2839 /// ```rust
2840 /// # use tenferro_runtime::TracedTensor;
2841 /// # let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64; 2]).unwrap();
2842 /// let y = x.embed_diag(0, 1)?;
2843 /// # Ok::<(), tenferro_runtime::Error>(())
2844 /// ```
2845 ///
2846 /// # Errors
2847 ///
2848 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis_a` is
2849 /// outside the input rank or `InvalidArgument` when `axis_b` is not a
2850 /// valid insertion axis.
2851 pub fn embed_diag(&self, axis_a: usize, axis_b: usize) -> Result<TracedTensor> {
2852 validate_traced_axis(self, axis_a, "TracedTensor::embed_diag")?;
2853 validate_traced_insert_axis(self.rank, axis_b, "TracedTensor::embed_diag")?;
2854 let out_shape_hint = self.shape_hint.as_ref().map(|shape| {
2855 let mut out_shape = shape.clone();
2856 out_shape.insert(axis_b, shape[axis_a].clone());
2857 out_shape
2858 });
2859 apply_unary(
2860 StdTensorOp::EmbedDiag { axis_a, axis_b },
2861 self,
2862 self.rank + 1,
2863 out_shape_hint,
2864 )
2865 }
2866
2867 /// Return the runtime size of one axis as a scalar `f64` tensor.
2868 ///
2869 /// The result is metadata-derived and therefore has no gradient.
2870 ///
2871 /// # Examples
2872 ///
2873 /// ```
2874 /// use tenferro_cpu::CpuEngine;
2875 /// use tenferro_runtime::{ExecutionEngine, GraphCompiler, Runtime, TracedTensor};
2876 ///
2877 /// let x = TracedTensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
2878 /// let cols = x.shape_of(1)?;
2879 /// let mut compiler = GraphCompiler::new();
2880 /// let program = compiler.compile(&cols).unwrap();
2881 /// let mut engine = CpuEngine::serial();
2882 /// let mut builder = Runtime::builder();
2883 /// builder
2884 /// .register_engine(tenferro_cpu::runtime_engine_registration(&engine).unwrap())
2885 /// .unwrap();
2886 /// let runtime = builder.build().unwrap();
2887 /// let prepared = runtime
2888 /// .prepare(&program, &[], &[engine.target()])
2889 /// .unwrap();
2890 /// let mut outputs = runtime
2891 /// .run(&mut [&mut engine], &prepared, &[])
2892 /// .unwrap();
2893 /// let out = outputs.remove(0).into_tensor(&mut engine).unwrap();
2894 /// assert_eq!(out.shape(), &[] as &[usize]);
2895 /// # Ok::<(), tenferro_runtime::Error>(())
2896 /// ```
2897 ///
2898 /// # Errors
2899 ///
2900 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2901 /// outside the input rank, or [`Error::RuntimeStateSource`] when scalar
2902 /// output metadata cannot be registered.
2903 pub fn shape_of(&self, axis: usize) -> Result<TracedTensor> {
2904 validate_traced_axis(self, axis, "TracedTensor::shape_of")?;
2905 apply_unary_with_dtype(
2906 StdTensorOp::ShapeOf { axis },
2907 self,
2908 0,
2909 Some(vec![]),
2910 DType::F64,
2911 )
2912 }
2913
2914 /// Truncate this tensor along `axis` to the first `size` elements.
2915 ///
2916 /// `size` is read at runtime from a scalar traced tensor. Values are
2917 /// rounded to the nearest integer, clamped to `[0, self.shape[axis]]`,
2918 /// and the output keeps the same element dtype as the input.
2919 ///
2920 /// # Examples
2921 ///
2922 /// ```
2923 /// use tenferro_cpu::CpuEngine;
2924 /// use tenferro_runtime::{ExecutionEngine, GraphCompiler, Runtime, TracedTensor};
2925 ///
2926 /// let x = TracedTensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap();
2927 /// let size = TracedTensor::from_vec_col_major(vec![], vec![2.0_f64]).unwrap();
2928 /// let y = x.dynamic_truncate(&size, 0)?;
2929 /// let mut compiler = GraphCompiler::new();
2930 /// let program = compiler.compile(&y).unwrap();
2931 /// let mut engine = CpuEngine::serial();
2932 /// let mut builder = Runtime::builder();
2933 /// builder
2934 /// .register_engine(tenferro_cpu::runtime_engine_registration(&engine).unwrap())
2935 /// .unwrap();
2936 /// let runtime = builder.build().unwrap();
2937 /// let prepared = runtime
2938 /// .prepare(&program, &[], &[engine.target()])
2939 /// .unwrap();
2940 /// let mut outputs = runtime
2941 /// .run(&mut [&mut engine], &prepared, &[])
2942 /// .unwrap();
2943 /// let out = outputs.remove(0).into_tensor(&mut engine).unwrap();
2944 /// assert_eq!(out.shape(), &[2]);
2945 /// # Ok::<(), tenferro_runtime::Error>(())
2946 /// ```
2947 ///
2948 /// # Errors
2949 ///
2950 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
2951 /// outside the input rank or `RankMismatch` when `size` is not scalar.
2952 ///
2953 /// # Deferred errors
2954 ///
2955 /// At execution, non-`f32`/`f64`/`i64` size dtypes return
2956 /// [`Error::TensorRuntime`] with `Unsupported`, non-finite size values
2957 /// return a typed `InvalidArgument`, and an empty scalar buffer returns a
2958 /// typed runtime-state source.
2959 pub fn dynamic_truncate(&self, size: &TracedTensor, axis: usize) -> Result<TracedTensor> {
2960 validate_traced_axis(self, axis, "TracedTensor::dynamic_truncate")?;
2961 if size.rank != 0 {
2962 return Err(graph_validation(
2963 "TracedTensor::dynamic_truncate",
2964 ValidationError::RankMismatch {
2965 expected: 0,
2966 actual: size.rank,
2967 },
2968 ));
2969 }
2970 apply_binary_preserve_input_dtypes(
2971 StdTensorOp::DynamicTruncate { axis },
2972 self,
2973 size,
2974 self.rank,
2975 None,
2976 self.dtype,
2977 )
2978 }
2979
2980 /// Pad this tensor with zeros along `axis` to match `reference.shape[axis]`.
2981 ///
2982 /// If `reference` is smaller along that axis, this is a no-op.
2983 ///
2984 /// # Examples
2985 ///
2986 /// ```
2987 /// use tenferro_cpu::CpuEngine;
2988 /// use tenferro_runtime::{ExecutionEngine, GraphCompiler, Runtime, TracedTensor};
2989 ///
2990 /// let x = TracedTensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
2991 /// let reference = TracedTensor::from_vec_col_major(vec![4], vec![0.0_f64, 0.0, 0.0, 0.0]).unwrap();
2992 /// let y = x.pad_to_match(&reference, 0)?;
2993 /// let mut compiler = GraphCompiler::new();
2994 /// let program = compiler.compile(&y).unwrap();
2995 /// let mut engine = CpuEngine::serial();
2996 /// let mut builder = Runtime::builder();
2997 /// builder
2998 /// .register_engine(tenferro_cpu::runtime_engine_registration(&engine).unwrap())
2999 /// .unwrap();
3000 /// let runtime = builder.build().unwrap();
3001 /// let prepared = runtime
3002 /// .prepare(&program, &[], &[engine.target()])
3003 /// .unwrap();
3004 /// let mut outputs = runtime
3005 /// .run(&mut [&mut engine], &prepared, &[])
3006 /// .unwrap();
3007 /// let out = outputs.remove(0).into_tensor(&mut engine).unwrap();
3008 /// assert_eq!(out.shape(), &[4]);
3009 /// # Ok::<(), tenferro_runtime::Error>(())
3010 /// ```
3011 ///
3012 /// # Errors
3013 ///
3014 /// Returns [`Error::Validation`] with `AxisOutOfBounds` when `axis` is
3015 /// outside either tensor's rank, or [`Error::RuntimeStateSource`] when
3016 /// output metadata cannot be registered.
3017 pub fn pad_to_match(&self, reference: &TracedTensor, axis: usize) -> Result<TracedTensor> {
3018 validate_traced_axis(self, axis, "TracedTensor::pad_to_match")?;
3019 validate_traced_axis(reference, axis, "TracedTensor::pad_to_match")?;
3020 let op = StdTensorOp::PadToMatch { axis };
3021 let (out_rank, out_shape_hint) = infer_traced_single_output_shape(
3022 "TracedTensor::pad_to_match",
3023 &op,
3024 &[self, reference],
3025 )?;
3026 apply_binary_preserve_input_dtypes(
3027 op,
3028 self,
3029 reference,
3030 out_rank,
3031 out_shape_hint,
3032 self.dtype,
3033 )
3034 }
3035}
3036
3037pub(crate) fn apply_unary(
3038 op: StdTensorOp,
3039 input: &TracedTensor,
3040 out_rank: usize,
3041 out_shape_hint: Option<Vec<SymDim>>,
3042) -> Result<TracedTensor> {
3043 let out_dtype = try_inferred_output_dtype(&op, &[input.dtype], "apply_unary")?;
3044 apply_unary_with_dtype(op, input, out_rank, out_shape_hint, out_dtype)
3045}
3046
3047fn try_apply_unary(
3048 op: StdTensorOp,
3049 input: &TracedTensor,
3050 out_rank: usize,
3051 out_shape_hint: Option<Vec<SymDim>>,
3052 context: &'static str,
3053) -> Result<TracedTensor> {
3054 let out_dtype = try_inferred_output_dtype(&op, &[input.dtype], context)?;
3055 apply_unary_with_dtype(op, input, out_rank, out_shape_hint, out_dtype)
3056}
3057
3058pub(crate) fn apply_unary_with_dtype(
3059 op: StdTensorOp,
3060 input: &TracedTensor,
3061 out_rank: usize,
3062 out_shape_hint: Option<Vec<SymDim>>,
3063 out_dtype: DType,
3064) -> Result<TracedTensor> {
3065 let mut builder = GraphBuilder::new();
3066 builder.add_parent(input.graph.clone());
3067 let input_ref = ValueRef::External(input.graph.values()[input.val].key.clone());
3068 let outputs = builder.add_operation(op, vec![input_ref], OperationRole::Primary);
3069 builder.set_outputs(outputs.clone());
3070 let graph = Arc::new(builder.build());
3071 let metadata_scope =
3072 register_single_output_metadata(graph.as_ref(), outputs[0], out_dtype, &out_shape_hint)?;
3073
3074 Ok(TracedTensor {
3075 id: next_traced_id(),
3076 rank: out_rank,
3077 dtype: out_dtype,
3078 graph,
3079 val: outputs[0],
3080 data: None,
3081 shape_hint: out_shape_hint,
3082 inputs_map: input.inputs_map.clone(),
3083 leaf_metas: input.leaf_metas.clone(),
3084 extra_roots: input.extra_roots.clone(),
3085 checkpoint_chain: input.checkpoint_chain.clone(),
3086 metadata_scopes: MetadataScopeChain::with_new(metadata_scope, [&input.metadata_scopes]),
3087 constraint_scopes: input.constraint_scopes.clone(),
3088 })
3089}
3090
3091/// Apply a unary-primary op that additionally references one or more
3092/// tensors for shape resolution only.
3093///
3094/// The primary `input` becomes op input 0; each tensor in `shape_refs`
3095/// becomes op input 1, 2, … in order. Used by
3096/// [`TracedTensor::broadcast_in_dim_sym`] when the target shape
3097/// references axes of tensors other than the primary input; the op
3098/// reads only their runtime shape, not their data.
3099pub(crate) fn apply_unary_with_shape_refs(
3100 op: StdTensorOp,
3101 input: &TracedTensor,
3102 shape_refs: &[&TracedTensor],
3103 out_rank: usize,
3104 out_shape_hint: Option<Vec<SymDim>>,
3105) -> Result<TracedTensor> {
3106 let mut builder = GraphBuilder::new();
3107 builder.add_parent(input.graph.clone());
3108 for t in shape_refs {
3109 builder.add_parent(t.graph.clone());
3110 }
3111 let mut op_inputs: Vec<ValueRef<StdTensorOp>> = Vec::with_capacity(1 + shape_refs.len());
3112 op_inputs.push(ValueRef::External(
3113 input.graph.values()[input.val].key.clone(),
3114 ));
3115 for t in shape_refs {
3116 op_inputs.push(ValueRef::External(t.graph.values()[t.val].key.clone()));
3117 }
3118 let outputs = builder.add_operation(op, op_inputs, OperationRole::Primary);
3119 builder.set_outputs(outputs.clone());
3120 let graph = Arc::new(builder.build());
3121 let metadata_scope =
3122 register_single_output_metadata(graph.as_ref(), outputs[0], input.dtype, &out_shape_hint)?;
3123
3124 let inputs_map =
3125 merge_traced_inputs_map(std::iter::once(input).chain(shape_refs.iter().copied()));
3126 let leaf_metas =
3127 merge_traced_leaf_metas(std::iter::once(input).chain(shape_refs.iter().copied()));
3128
3129 let mut extra_roots = input.extra_roots.clone();
3130 for t in shape_refs {
3131 extra_roots.extend(t.extra_roots.iter().cloned());
3132 }
3133
3134 let mut checkpoint_chain = input.checkpoint_chain.clone();
3135 for t in shape_refs {
3136 checkpoint_chain =
3137 CheckpointNode::merge_chains(checkpoint_chain, t.checkpoint_chain.clone());
3138 }
3139
3140 Ok(TracedTensor {
3141 id: next_traced_id(),
3142 rank: out_rank,
3143 dtype: input.dtype,
3144 graph,
3145 val: outputs[0],
3146 data: None,
3147 shape_hint: out_shape_hint,
3148 inputs_map,
3149 leaf_metas,
3150 extra_roots,
3151 checkpoint_chain,
3152 metadata_scopes: MetadataScopeChain::with_new(
3153 metadata_scope,
3154 std::iter::once(&input.metadata_scopes)
3155 .chain(shape_refs.iter().map(|tensor| &tensor.metadata_scopes)),
3156 ),
3157 constraint_scopes: ConstraintScopeChain::merge(
3158 std::iter::once(&input.constraint_scopes)
3159 .chain(shape_refs.iter().map(|tensor| &tensor.constraint_scopes)),
3160 ),
3161 })
3162}
3163
3164pub(crate) fn apply_nullary(
3165 op: StdTensorOp,
3166 rank: usize,
3167 dtype: DType,
3168 shape_hint: Option<Vec<SymDim>>,
3169) -> Result<TracedTensor> {
3170 let mut builder = GraphBuilder::new();
3171 let outputs = builder.add_operation(op, vec![], OperationRole::Primary);
3172 builder.set_outputs(outputs.clone());
3173 let graph = Arc::new(builder.build());
3174 let metadata_scope =
3175 register_single_output_metadata(graph.as_ref(), outputs[0], dtype, &shape_hint)?;
3176
3177 Ok(TracedTensor {
3178 id: next_traced_id(),
3179 rank,
3180 dtype,
3181 graph,
3182 val: outputs[0],
3183 data: None,
3184 shape_hint,
3185 inputs_map: Arc::new(HashMap::new()),
3186 leaf_metas: Arc::new(HashMap::new()),
3187 extra_roots: Vec::new(),
3188 checkpoint_chain: None,
3189 metadata_scopes: MetadataScopeChain::from_scope(metadata_scope),
3190 constraint_scopes: ConstraintScopeChain::empty(),
3191 })
3192}
3193
3194pub(crate) fn apply_binary(
3195 op: StdTensorOp,
3196 lhs: &TracedTensor,
3197 rhs: &TracedTensor,
3198 out_rank: usize,
3199 out_shape_hint: Option<Vec<SymDim>>,
3200) -> Result<TracedTensor> {
3201 let input_dtype = crate::shape_infer::promote_dtype_for_binary_op(&op, lhs.dtype, rhs.dtype);
3202 let out_dtype = try_inferred_output_dtype(&op, &[lhs.dtype, rhs.dtype], "apply_binary")?;
3203
3204 // Insert Convert ops when an input dtype differs from the primitive input dtype.
3205 let lhs = if lhs.dtype != input_dtype {
3206 lhs.cast(input_dtype)?
3207 } else {
3208 lhs.clone()
3209 };
3210 let rhs = if rhs.dtype != input_dtype {
3211 rhs.cast(input_dtype)?
3212 } else {
3213 rhs.clone()
3214 };
3215
3216 apply_binary_with_output_dtype(op, &lhs, &rhs, out_rank, out_shape_hint, out_dtype)
3217}
3218
3219fn try_apply_binary(
3220 op: StdTensorOp,
3221 lhs: &TracedTensor,
3222 rhs: &TracedTensor,
3223 out_rank: usize,
3224 out_shape_hint: Option<Vec<SymDim>>,
3225 context: &'static str,
3226) -> Result<TracedTensor> {
3227 let input_dtype = crate::shape_infer::promote_dtype_for_binary_op(&op, lhs.dtype, rhs.dtype);
3228 let out_dtype = try_inferred_output_dtype(&op, &[lhs.dtype, rhs.dtype], context)?;
3229
3230 let lhs = if lhs.dtype != input_dtype {
3231 lhs.cast(input_dtype)?
3232 } else {
3233 lhs.clone()
3234 };
3235 let rhs = if rhs.dtype != input_dtype {
3236 rhs.cast(input_dtype)?
3237 } else {
3238 rhs.clone()
3239 };
3240
3241 apply_binary_with_output_dtype(op, &lhs, &rhs, out_rank, out_shape_hint, out_dtype)
3242}
3243
3244pub(crate) fn apply_binary_preserve_input_dtypes(
3245 op: StdTensorOp,
3246 lhs: &TracedTensor,
3247 rhs: &TracedTensor,
3248 out_rank: usize,
3249 out_shape_hint: Option<Vec<SymDim>>,
3250 out_dtype: DType,
3251) -> Result<TracedTensor> {
3252 apply_binary_with_output_dtype(op, lhs, rhs, out_rank, out_shape_hint, out_dtype)
3253}
3254
3255pub(crate) fn apply_broadcast_binary_op(
3256 op: StdTensorOp,
3257 lhs: &TracedTensor,
3258 rhs: &TracedTensor,
3259) -> Result<TracedTensor> {
3260 let (lhs, rhs) = broadcast_binary(lhs, rhs)?;
3261 try_apply_binary(
3262 op,
3263 &lhs,
3264 &rhs,
3265 lhs.rank,
3266 lhs.shape_hint.clone(),
3267 "broadcast_binary",
3268 )
3269}
3270
3271pub(crate) fn apply_broadcast_ternary_op(
3272 op: StdTensorOp,
3273 first: &TracedTensor,
3274 second: &TracedTensor,
3275 third: &TracedTensor,
3276) -> Result<TracedTensor> {
3277 let (first, second, third) = broadcast_ternary(first, second, third)?;
3278 try_apply_ternary(
3279 op,
3280 &first,
3281 &second,
3282 &third,
3283 first.rank,
3284 first.shape_hint.clone(),
3285 "broadcast_ternary",
3286 )
3287}
3288
3289fn try_apply_ternary(
3290 op: StdTensorOp,
3291 first: &TracedTensor,
3292 second: &TracedTensor,
3293 third: &TracedTensor,
3294 out_rank: usize,
3295 out_shape_hint: Option<Vec<SymDim>>,
3296 context: &'static str,
3297) -> Result<TracedTensor> {
3298 let out_dtype =
3299 try_inferred_output_dtype(&op, &[first.dtype, second.dtype, third.dtype], context)?;
3300 let (first, second, third) = match op {
3301 StdTensorOp::Select => {
3302 let value_dtype = crate::shape_infer::promote_dtype(second.dtype, third.dtype);
3303 let second = if second.dtype != value_dtype {
3304 second.cast(value_dtype)?
3305 } else {
3306 second.clone()
3307 };
3308 let third = if third.dtype != value_dtype {
3309 third.cast(value_dtype)?
3310 } else {
3311 third.clone()
3312 };
3313 (first.clone(), second, third)
3314 }
3315 _ => {
3316 let input_dtype =
3317 crate::shape_infer::promote_dtypes([first.dtype, second.dtype, third.dtype]);
3318 let first = if first.dtype != input_dtype {
3319 first.cast(input_dtype)?
3320 } else {
3321 first.clone()
3322 };
3323 let second = if second.dtype != input_dtype {
3324 second.cast(input_dtype)?
3325 } else {
3326 second.clone()
3327 };
3328 let third = if third.dtype != input_dtype {
3329 third.cast(input_dtype)?
3330 } else {
3331 third.clone()
3332 };
3333 (first, second, third)
3334 }
3335 };
3336 apply_ternary_with_output_dtype(
3337 op,
3338 &first,
3339 &second,
3340 &third,
3341 out_rank,
3342 out_shape_hint,
3343 out_dtype,
3344 )
3345}
3346
3347fn apply_binary_with_output_dtype(
3348 op: StdTensorOp,
3349 lhs: &TracedTensor,
3350 rhs: &TracedTensor,
3351 out_rank: usize,
3352 out_shape_hint: Option<Vec<SymDim>>,
3353 out_dtype: DType,
3354) -> Result<TracedTensor> {
3355 let lhs_ref = ValueRef::External(lhs.graph.values()[lhs.val].key.clone());
3356 let rhs_ref = ValueRef::External(rhs.graph.values()[rhs.val].key.clone());
3357
3358 let mut builder = GraphBuilder::new();
3359 builder.add_parent(lhs.graph.clone());
3360 builder.add_parent(rhs.graph.clone());
3361 let outputs = builder.add_operation(op, vec![lhs_ref, rhs_ref], OperationRole::Primary);
3362 builder.set_outputs(outputs.clone());
3363 let graph = Arc::new(builder.build());
3364 let metadata_scope =
3365 register_single_output_metadata(graph.as_ref(), outputs[0], out_dtype, &out_shape_hint)?;
3366
3367 let mut extra_roots = lhs.extra_roots.clone();
3368 extra_roots.extend(rhs.extra_roots.iter().cloned());
3369
3370 Ok(TracedTensor {
3371 id: next_traced_id(),
3372 rank: out_rank,
3373 dtype: out_dtype,
3374 graph,
3375 val: outputs[0],
3376 data: None,
3377 shape_hint: out_shape_hint,
3378 inputs_map: merge_traced_inputs_map([lhs, rhs]),
3379 leaf_metas: merge_traced_leaf_metas([lhs, rhs]),
3380 extra_roots,
3381 checkpoint_chain: CheckpointNode::merge_chains(
3382 lhs.checkpoint_chain.clone(),
3383 rhs.checkpoint_chain.clone(),
3384 ),
3385 metadata_scopes: MetadataScopeChain::with_new(
3386 metadata_scope,
3387 [&lhs.metadata_scopes, &rhs.metadata_scopes],
3388 ),
3389 constraint_scopes: ConstraintScopeChain::merge([
3390 &lhs.constraint_scopes,
3391 &rhs.constraint_scopes,
3392 ]),
3393 })
3394}
3395
3396fn apply_ternary_with_output_dtype(
3397 op: StdTensorOp,
3398 first: &TracedTensor,
3399 second: &TracedTensor,
3400 third: &TracedTensor,
3401 out_rank: usize,
3402 out_shape_hint: Option<Vec<SymDim>>,
3403 out_dtype: DType,
3404) -> Result<TracedTensor> {
3405 let first_ref = ValueRef::External(first.graph.values()[first.val].key.clone());
3406 let second_ref = ValueRef::External(second.graph.values()[second.val].key.clone());
3407 let third_ref = ValueRef::External(third.graph.values()[third.val].key.clone());
3408
3409 let mut builder = GraphBuilder::new();
3410 builder.add_parent(first.graph.clone());
3411 builder.add_parent(second.graph.clone());
3412 builder.add_parent(third.graph.clone());
3413 let outputs = builder.add_operation(
3414 op,
3415 vec![first_ref, second_ref, third_ref],
3416 OperationRole::Primary,
3417 );
3418 builder.set_outputs(outputs.clone());
3419 let graph = Arc::new(builder.build());
3420 let metadata_scope =
3421 register_single_output_metadata(graph.as_ref(), outputs[0], out_dtype, &out_shape_hint)?;
3422
3423 let mut extra_roots = first.extra_roots.clone();
3424 extra_roots.extend(second.extra_roots.iter().cloned());
3425 extra_roots.extend(third.extra_roots.iter().cloned());
3426
3427 let checkpoint_chain = CheckpointNode::merge_chains(
3428 CheckpointNode::merge_chains(
3429 first.checkpoint_chain.clone(),
3430 second.checkpoint_chain.clone(),
3431 ),
3432 third.checkpoint_chain.clone(),
3433 );
3434
3435 Ok(TracedTensor {
3436 id: next_traced_id(),
3437 rank: out_rank,
3438 dtype: out_dtype,
3439 graph,
3440 val: outputs[0],
3441 data: None,
3442 shape_hint: out_shape_hint,
3443 inputs_map: merge_traced_inputs_map([first, second, third]),
3444 leaf_metas: merge_traced_leaf_metas([first, second, third]),
3445 extra_roots,
3446 checkpoint_chain,
3447 metadata_scopes: MetadataScopeChain::with_new(
3448 metadata_scope,
3449 [
3450 &first.metadata_scopes,
3451 &second.metadata_scopes,
3452 &third.metadata_scopes,
3453 ],
3454 ),
3455 constraint_scopes: ConstraintScopeChain::merge([
3456 &first.constraint_scopes,
3457 &second.constraint_scopes,
3458 &third.constraint_scopes,
3459 ]),
3460 })
3461}
3462
3463fn register_single_output_metadata(
3464 graph: &Graph<StdTensorOp>,
3465 output: LocalValueId,
3466 dtype: DType,
3467 shape_hint: &Option<Vec<SymDim>>,
3468) -> Result<GlobalMetadataScope> {
3469 if let Some(shape) = shape_hint {
3470 // Fresh graph output keys are generated in this builder, so metadata
3471 // registration failure would indicate a global metadata invariant bug.
3472 register_metadata_or_runtime_state(register_scoped_value_metadata(
3473 graph.values()[output].key.clone(),
3474 tensor_meta(dtype, shape.clone()),
3475 ))
3476 } else {
3477 // Fresh graph output keys are generated in this builder, so metadata
3478 // registration failure would indicate a global metadata invariant bug.
3479 register_metadata_or_runtime_state(register_scoped_graph_metadata(
3480 graph,
3481 std::iter::empty(),
3482 ))
3483 }
3484}
3485
3486impl TracedTensor {
3487 pub(crate) fn resolve_roots(&self) -> Vec<Arc<Graph<StdTensorOp>>> {
3488 let mut roots = Vec::with_capacity(1 + self.extra_roots.len());
3489 roots.push(self.graph.clone());
3490 roots.extend(self.extra_roots.iter().cloned());
3491 roots
3492 }
3493}
3494
3495mod composite_ops;
3496
3497#[cfg(test)]
3498mod tests;