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