tenferro_ad/eager_ops.rs
1use std::sync::Arc;
2
3use computegraph::GraphOperation;
4use num_complex::{Complex32, Complex64};
5use tenferro_ops::broadcast::{
6 broadcast_error_to_validation, broadcast_in_dim_extent_error, broadcast_input_plan,
7 broadcast_shape, broadcast_shapes,
8};
9use tenferro_ops::dim_expr::DimExpr;
10use tenferro_ops::std_tensor_op::StdTensorOp;
11use tenferro_tensor::{
12 DType, DotGeneralConfig, GatherConfig, PadConfig, ScatterConfig, SliceConfig, Tensor,
13 TensorValue,
14};
15
16use crate::eager::{
17 eager_capture_active, eager_grad_recording_enabled, eager_op_profile_start, exec_single_output,
18 exec_single_output_read, maybe_print_eager_op_profile, profile_eager_op_section,
19 record_eager_op_profile, record_eager_outputs, record_eager_value_outputs, EagerTensor,
20};
21use crate::eager_exec::exec_dot_general_with_conj_on_tensor_reads;
22use crate::error::{Error, Result};
23
24pub(crate) fn broadcast_binary(
25 op: &'static str,
26 lhs: &EagerTensor,
27 rhs: &EagerTensor,
28) -> Result<(EagerTensor, EagerTensor)> {
29 ensure_same_context(lhs, rhs)?;
30 let shape =
31 broadcast_shape(lhs.shape(), rhs.shape()).map_err(|err| broadcast_error(op, err))?;
32 Ok((
33 broadcast_to(op, lhs, &shape)?,
34 broadcast_to(op, rhs, &shape)?,
35 ))
36}
37
38pub(crate) fn broadcast_ternary(
39 op: &'static str,
40 first: &EagerTensor,
41 second: &EagerTensor,
42 third: &EagerTensor,
43) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
44 ensure_same_context(first, second)?;
45 ensure_same_context(first, third)?;
46 let shape = broadcast_shapes([first.shape(), second.shape(), third.shape()])
47 .map_err(|err| broadcast_error(op, err))?;
48 Ok((
49 broadcast_to(op, first, &shape)?,
50 broadcast_to(op, second, &shape)?,
51 broadcast_to(op, third, &shape)?,
52 ))
53}
54
55fn broadcast_to(
56 op: &'static str,
57 input: &EagerTensor,
58 target_shape: &[usize],
59) -> Result<EagerTensor> {
60 let input_shape = input.shape();
61 if input_shape == target_shape {
62 return Ok(input.clone());
63 }
64
65 let plan =
66 broadcast_input_plan(input_shape, target_shape).map_err(|err| broadcast_error(op, err))?;
67 let source = if plan.source_shape == input_shape {
68 input.clone()
69 } else {
70 input.reshape(&plan.source_shape)?
71 };
72 source.broadcast_in_dim(target_shape, &plan.dims)
73}
74
75fn broadcast_error(op: &'static str, err: tenferro_ops::broadcast::BroadcastError) -> Error {
76 tenferro_tensor::Error::validation(op, broadcast_error_to_validation(err)).into()
77}
78
79fn ensure_same_context(lhs: &EagerTensor, rhs: &EagerTensor) -> Result<()> {
80 if !lhs.same_context(rhs) {
81 return Err(Error::ContextMismatch {
82 lhs: lhs.ctx_id(),
83 rhs: rhs.ctx_id(),
84 });
85 }
86 Ok(())
87}
88
89impl std::ops::Add for &EagerTensor {
90 type Output = Result<EagerTensor>;
91
92 fn add(self, rhs: &EagerTensor) -> Result<EagerTensor> {
93 EagerTensor::add(self, rhs)
94 }
95}
96
97impl std::ops::Sub for &EagerTensor {
98 type Output = Result<EagerTensor>;
99
100 fn sub(self, rhs: &EagerTensor) -> Result<EagerTensor> {
101 EagerTensor::sub(self, rhs)
102 }
103}
104
105impl std::ops::Mul for &EagerTensor {
106 type Output = Result<EagerTensor>;
107
108 fn mul(self, rhs: &EagerTensor) -> Result<EagerTensor> {
109 EagerTensor::mul(self, rhs)
110 }
111}
112
113impl std::ops::Div for &EagerTensor {
114 type Output = Result<EagerTensor>;
115
116 fn div(self, rhs: &EagerTensor) -> Result<EagerTensor> {
117 EagerTensor::div(self, rhs)
118 }
119}
120
121impl std::ops::Rem for &EagerTensor {
122 type Output = Result<EagerTensor>;
123
124 fn rem(self, rhs: &EagerTensor) -> Result<EagerTensor> {
125 EagerTensor::rem(self, rhs)
126 }
127}
128
129impl std::ops::Neg for &EagerTensor {
130 type Output = Result<EagerTensor>;
131
132 fn neg(self) -> Result<EagerTensor> {
133 EagerTensor::neg(self)
134 }
135}
136
137impl EagerTensor {
138 /// Elementwise addition.
139 ///
140 /// # Examples
141 ///
142 /// ```
143 /// use tenferro_cpu::CpuBackend;
144 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
145 ///
146 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
147 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
148 /// let y = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap(), ctx.clone()).unwrap();
149 /// let z = x.add(&y).unwrap();
150 ///
151 /// assert_eq!(z.value().unwrap().as_slice::<f64>().unwrap(), &[4.0, 6.0]);
152 /// # Ok::<(), tenferro_ad::Error>(())
153 /// ```
154 ///
155 /// # Errors
156 ///
157 /// Returns [`Error::ContextMismatch`] for tensors from different eager
158 /// runtimes, [`tenferro_tensor::Error::Validation`] with
159 /// `ShapeMismatch`/`DTypeMismatch` for incompatible operands, or a typed
160 /// backend/runtime-state error during execution.
161 pub fn add(&self, other: &Self) -> Result<Self> {
162 let (lhs, rhs) = broadcast_binary("add", self, other)?;
163 lhs.binary_op(&rhs, StdTensorOp::Add)
164 }
165
166 /// Elementwise subtraction.
167 ///
168 /// # Errors
169 ///
170 /// Returns [`Error::ContextMismatch`] for tensors from different eager
171 /// runtimes, [`tenferro_tensor::Error::Validation`] with
172 /// `ShapeMismatch`/`DTypeMismatch` for incompatible operands, or a typed
173 /// backend/runtime-state error during execution.
174 pub fn sub(&self, other: &Self) -> Result<Self> {
175 let (lhs, rhs) = broadcast_binary("sub", self, other)?;
176 lhs.binary_op(&rhs, StdTensorOp::Sub)
177 }
178
179 /// Elementwise multiplication.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// use tenferro_cpu::CpuBackend;
185 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
186 ///
187 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
188 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
189 /// let y = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap(), ctx.clone()).unwrap();
190 /// let z = x.mul(&y).unwrap();
191 ///
192 /// assert_eq!(z.value().unwrap().as_slice::<f64>().unwrap(), &[3.0, 8.0]);
193 /// # Ok::<(), tenferro_ad::Error>(())
194 /// ```
195 ///
196 /// # Errors
197 ///
198 /// Returns [`Error::ContextMismatch`] for tensors from different eager
199 /// runtimes, [`tenferro_tensor::Error::Validation`] with
200 /// `ShapeMismatch`/`DTypeMismatch` for incompatible operands, or a typed
201 /// backend/runtime-state error during execution.
202 pub fn mul(&self, other: &Self) -> Result<Self> {
203 let (lhs, rhs) = broadcast_binary("mul", self, other)?;
204 lhs.binary_op(&rhs, StdTensorOp::Mul)
205 }
206
207 /// Negate the tensor.
208 ///
209 /// # Examples
210 ///
211 /// ```
212 /// use tenferro_cpu::CpuBackend;
213 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
214 ///
215 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
216 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, -2.0]).unwrap(), ctx.clone()).unwrap();
217 /// let y = x.neg().unwrap();
218 ///
219 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[-1.0, 2.0]);
220 /// # Ok::<(), tenferro_ad::Error>(())
221 /// ```
222 ///
223 /// # Errors
224 ///
225 /// Returns [`tenferro_tensor::Error::Unsupported`] when the backend does
226 /// not implement negation for the dtype, or a typed backend/runtime-state
227 /// error during execution.
228 pub fn neg(&self) -> Result<Self> {
229 self.unary_op(StdTensorOp::Neg)
230 }
231
232 /// Elementwise exponential.
233 ///
234 /// # Examples
235 ///
236 /// ```
237 /// use tenferro_cpu::CpuBackend;
238 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
239 ///
240 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
241 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![0.0_f64]).unwrap(), ctx.clone()).unwrap();
242 /// let y = x.exp().unwrap();
243 ///
244 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0]);
245 /// # Ok::<(), tenferro_ad::Error>(())
246 /// ```
247 ///
248 /// # Errors
249 ///
250 /// Returns [`tenferro_tensor::Error::Unsupported`] when the backend does
251 /// not implement exponentiation for the dtype, or a typed backend/
252 /// runtime-state error during execution.
253 pub fn exp(&self) -> Result<Self> {
254 self.unary_op(StdTensorOp::Exp)
255 }
256
257 /// Reduce sum over the requested axes.
258 ///
259 /// # Examples
260 ///
261 /// ```
262 /// use tenferro_cpu::CpuBackend;
263 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
264 ///
265 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
266 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
267 /// let y = x.reduce_sum(None).unwrap();
268 ///
269 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[10.0]);
270 /// # Ok::<(), tenferro_ad::Error>(())
271 /// ```
272 ///
273 /// # Errors
274 ///
275 /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
276 /// `DuplicateAxis` for an invalid reduction axis, or a typed
277 /// unsupported/backend/runtime-state error for the selected dtype.
278 pub fn reduce_sum(&self, axes: Option<&[usize]>) -> Result<Self> {
279 let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
280 validate_eager_axes("EagerTensor::reduce_sum", self.shape().len(), &axes)?;
281 self.unary_op(StdTensorOp::ReduceSum { axes })
282 }
283
284 /// Sum elementwise squares over the requested axes.
285 ///
286 /// Each value is squared in its input dtype before reduction. The initial
287 /// supported dtypes are `f32` and `f64`; other dtypes return a typed
288 /// unsupported error. Passing an empty axis slice returns the elementwise
289 /// square without reducing rank.
290 ///
291 /// This operation is useful when the squared sum is needed directly. Use
292 /// the linalg norm APIs when a square root or complex magnitude semantics
293 /// are required.
294 ///
295 /// # Errors
296 ///
297 /// Returns a typed validation error for invalid axes, a typed unsupported
298 /// error for other dtypes, or a typed backend or runtime-state error during
299 /// execution.
300 pub fn reduce_sum_squares(&self, axes: &[usize]) -> Result<Self> {
301 validate_eager_axes("EagerTensor::reduce_sum_squares", self.shape().len(), axes)?;
302 self.unary_op(StdTensorOp::ReduceSumSquares {
303 axes: axes.to_vec(),
304 })
305 }
306
307 /// Execute a dot-general contraction eagerly.
308 ///
309 /// # Examples
310 ///
311 /// ```
312 /// use tenferro_cpu::CpuBackend;
313 /// use tenferro_ad::{DotGeneralConfig, EagerRuntime, EagerTensor, Tensor};
314 ///
315 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
316 /// let a = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 3], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(), ctx.clone()).unwrap();
317 /// let b = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3, 2], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(), ctx.clone()).unwrap();
318 /// let c = a.dot_general(&b, DotGeneralConfig {
319 /// lhs_contracting_dims: vec![1],
320 /// rhs_contracting_dims: vec![0],
321 /// lhs_batch_dims: vec![],
322 /// rhs_batch_dims: vec![],
323 /// }).unwrap();
324 ///
325 /// assert_eq!(c.shape(), &[2, 2]);
326 /// # Ok::<(), tenferro_ad::Error>(())
327 /// ```
328 ///
329 /// # Errors
330 ///
331 /// Returns [`tenferro_tensor::Error::Validation`] with `RankMismatch`,
332 /// `AxisOutOfBounds`, `DuplicateAxis`, `ShapeMismatch`, or `DTypeMismatch`
333 /// when `config` or the operands are invalid; backend and runtime-state
334 /// failures retain their typed sources.
335 pub fn dot_general(&self, other: &Self, config: DotGeneralConfig) -> Result<Self> {
336 validate_eager_dot_general_config(
337 "EagerTensor::dot_general",
338 &config,
339 self.shape().len(),
340 other.shape().len(),
341 )?;
342 self.binary_op(other, StdTensorOp::DotGeneral { config })
343 }
344
345 /// Execute a dot-general contraction, optionally conjugating either operand.
346 ///
347 /// Untracked tensors route the conjugation flags directly to the backend so
348 /// the conjugated operand does not need to be materialized. Tracked tensors
349 /// fall back to explicit `Conj` plus `DotGeneral` so reverse-mode AD keeps
350 /// the same graph semantics as the standard eager ops.
351 ///
352 /// # Errors
353 ///
354 /// Returns [`Error::ContextMismatch`] for operands from different eager
355 /// runtimes, [`tenferro_tensor::Error::Validation`] for rank/axis/shape or
356 /// dtype mismatches in `config`, or a typed backend/runtime-state error.
357 pub fn dot_general_with_conj(
358 &self,
359 other: &Self,
360 config: DotGeneralConfig,
361 lhs_conj: bool,
362 rhs_conj: bool,
363 ) -> Result<Self> {
364 if !self.same_context(other) {
365 return Err(Error::ContextMismatch {
366 lhs: self.ctx_id(),
367 rhs: other.ctx_id(),
368 });
369 }
370 validate_eager_dot_general_config(
371 "EagerTensor::dot_general_with_conj",
372 &config,
373 self.shape().len(),
374 other.shape().len(),
375 )?;
376
377 if !self.requires_grad && !other.requires_grad {
378 let ctx = Arc::clone(&self.ctx);
379 let mut backend = ctx.lock_backend()?;
380 let output = exec_dot_general_with_conj_on_tensor_reads(
381 self.tensor_read(),
382 other.tensor_read(),
383 &config,
384 lhs_conj,
385 rhs_conj,
386 &mut *backend,
387 )?;
388 drop(backend);
389 return Self::new_untracked_result(ctx, output);
390 }
391
392 match (lhs_conj, rhs_conj) {
393 (false, false) => self.dot_general(other, config),
394 (true, false) => self.conj()?.dot_general(other, config),
395 (false, true) => {
396 let rhs = other.conj()?;
397 self.dot_general(&rhs, config)
398 }
399 (true, true) => {
400 let lhs = self.conj()?;
401 let rhs = other.conj()?;
402 lhs.dot_general(&rhs, config)
403 }
404 }
405 }
406
407 /// Scale by a real scalar: `y = factor * x`.
408 ///
409 /// Integer factors are rounded to the nearest integer before multiplication,
410 /// boolean factors map finite zero to `false` and other finite values to
411 /// `true`, and complex tensors receive a zero-imaginary scalar.
412 ///
413 /// # Errors
414 ///
415 /// Returns [`Error::TensorRuntime`] with
416 /// [`tenferro_tensor::ValidationError::InvalidArgument`] when an integer or
417 /// boolean factor is non-finite or outside the input dtype's range. Backend
418 /// and runtime execution failures retain their typed source variants.
419 /// # Examples
420 ///
421 /// ```rust
422 /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
423 /// # use tenferro_cpu::CpuBackend;
424 /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
425 /// # let x = EagerTensor::from_tensor_in(
426 /// # Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(),
427 /// # ctx,
428 /// # )?;
429 /// let scaled = x.scale_real(2.0)?;
430 /// assert_eq!(scaled.value()?.as_slice::<f64>()?, &[2.0, 4.0]);
431 /// # Ok::<(), tenferro_ad::Error>(())
432 /// ```
433 pub fn scale_real(&self, factor: f64) -> Result<Self> {
434 let scalar = match self.dtype() {
435 DType::F64 => Tensor::from_vec_col_major(vec![], vec![factor])?,
436 DType::F32 => Tensor::from_vec_col_major(vec![], vec![factor as f32])?,
437 DType::I32 => Tensor::from_vec_col_major(vec![], vec![round_real_to_i32(factor)?])?,
438 DType::I64 => Tensor::from_vec_col_major(vec![], vec![round_real_to_i64(factor)?])?,
439 DType::Bool => Tensor::from_vec_col_major(vec![], vec![bool_from_real(factor)?])?,
440 DType::C64 => Tensor::from_vec_col_major(vec![], vec![Complex64::new(factor, 0.0)])?,
441 DType::C32 => {
442 Tensor::from_vec_col_major(vec![], vec![Complex32::new(factor as f32, 0.0)])?
443 }
444 };
445 let scalar = EagerTensor::from_tensor_in(scalar, Arc::clone(&self.ctx))?;
446 self.mul(&scalar)
447 }
448
449 /// Scale a complex tensor by a complex scalar: `y = factor * x`.
450 ///
451 /// # Errors
452 ///
453 /// Returns [`Error::TensorRuntime`] with
454 /// [`tenferro_tensor::ValidationError::InvalidArgument`] for a non-complex
455 /// input dtype. Backend and runtime execution failures retain their typed
456 /// source variants.
457 /// # Examples
458 ///
459 /// ```rust
460 /// # use num_complex::Complex64;
461 /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
462 /// # use tenferro_cpu::CpuBackend;
463 /// # let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
464 /// # let x = EagerTensor::from_tensor_in(
465 /// # Tensor::from_vec_col_major(
466 /// # vec![2],
467 /// # vec![Complex64::new(1.0, 2.0), Complex64::new(3.0, 4.0)],
468 /// # )
469 /// # .unwrap(),
470 /// # ctx,
471 /// # )?;
472 /// let scaled = x.scale_complex(Complex64::new(0.0, 1.0))?;
473 /// assert_eq!(
474 /// scaled.value()?.as_slice::<Complex64>()?,
475 /// &[Complex64::new(-2.0, 1.0), Complex64::new(-4.0, 3.0)],
476 /// );
477 /// # Ok::<(), tenferro_ad::Error>(())
478 /// ```
479 pub fn scale_complex(&self, factor: Complex64) -> Result<Self> {
480 let scalar = match self.dtype() {
481 DType::C64 => Tensor::from_vec_col_major(vec![], vec![factor])?,
482 DType::C32 => Tensor::from_vec_col_major(
483 vec![],
484 vec![Complex32::new(factor.re as f32, factor.im as f32)],
485 )?,
486 dtype => {
487 return Err(Error::TensorRuntime(
488 tenferro_tensor::Error::invalid_argument(
489 "scale_complex",
490 "dtype",
491 format!("requires complex tensor dtype, got {dtype:?}"),
492 ),
493 ));
494 }
495 };
496 let scalar = EagerTensor::from_tensor_in(scalar, Arc::clone(&self.ctx))?;
497 self.mul(&scalar)
498 }
499
500 /// Matrix multiplication for rank-2 tensors.
501 ///
502 /// This is a convenience wrapper over [`Self::dot_general`] that
503 /// contracts the left matrix's column axis with the right matrix's row
504 /// axis.
505 ///
506 /// # Examples
507 ///
508 /// ```
509 /// use tenferro_cpu::CpuBackend;
510 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
511 ///
512 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
513 /// let a = EagerTensor::from_tensor_in(
514 /// Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(),
515 /// ctx.clone(),
516 /// ).unwrap();
517 /// let b = EagerTensor::from_tensor_in(
518 /// Tensor::from_vec_col_major(vec![2, 1], vec![5.0_f64, 6.0]).unwrap(),
519 /// ctx,
520 /// ).unwrap();
521 /// let c = a.matmul(&b).unwrap();
522 ///
523 /// assert_eq!(c.shape(), &[2, 1]);
524 /// assert_eq!(c.value().unwrap().as_slice::<f64>().unwrap(), &[23.0, 34.0]);
525 /// # Ok::<(), tenferro_ad::Error>(())
526 /// ```
527 ///
528 /// # Errors
529 ///
530 /// Returns [`tenferro_tensor::ValidationError::RankMismatch`] when either operand is
531 /// not rank 2, `ShapeMismatch` when the inner dimensions differ, or a typed
532 /// dtype/backend/runtime-state error during the contraction.
533 pub fn matmul(&self, other: &Self) -> Result<Self> {
534 let lhs_shape = self.shape();
535 let rhs_shape = other.shape();
536 if lhs_shape.len() != 2 {
537 return Err(tenferro_tensor::Error::rank_mismatch("matmul", 2, lhs_shape.len()).into());
538 }
539 if rhs_shape.len() != 2 {
540 return Err(tenferro_tensor::Error::rank_mismatch("matmul", 2, rhs_shape.len()).into());
541 }
542 if lhs_shape[1] != rhs_shape[0] {
543 return Err(
544 tenferro_tensor::Error::shape_mismatch("matmul", lhs_shape, rhs_shape).into(),
545 );
546 }
547 self.dot_general(
548 other,
549 DotGeneralConfig {
550 lhs_contracting_dims: vec![1],
551 rhs_contracting_dims: vec![0],
552 lhs_batch_dims: vec![],
553 rhs_batch_dims: vec![],
554 },
555 )
556 }
557
558 /// Permute tensor axes.
559 ///
560 /// # Examples
561 ///
562 /// ```
563 /// use tenferro_cpu::CpuBackend;
564 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
565 ///
566 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
567 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
568 /// vec![2, 3],
569 /// vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0],
570 /// ).unwrap(), ctx.clone()).unwrap();
571 /// let y = x.transpose(&[1, 0]).unwrap();
572 ///
573 /// assert_eq!(y.shape(), &[3, 2]);
574 /// let materialized = y.to_tensor().unwrap();
575 /// assert_eq!(materialized.as_slice::<f64>().unwrap(), &[1.0, 3.0, 5.0, 2.0, 4.0, 6.0]);
576 /// # Ok::<(), tenferro_ad::Error>(())
577 /// ```
578 ///
579 /// # Errors
580 ///
581 /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds`
582 /// or `DuplicateAxis` when `perm` is not a permutation, or a typed
583 /// backend/runtime-state error while creating the view.
584 pub fn transpose(&self, perm: &[usize]) -> Result<Self> {
585 let op = StdTensorOp::Transpose {
586 perm: perm.to_vec(),
587 };
588 // INVARIANT: the result must own a group independent of `self`; the
589 // explicit duplicate is the ownership boundary before making a view.
590 let base = self.to_tensor()?;
591 let value = TensorValue::from_tensor(base)
592 .transpose_view(perm)
593 .map_err(Error::TensorRuntime)?;
594 Self::nary_value_op(&[self], op, value)
595 }
596
597 /// Reshape without changing element order.
598 ///
599 /// # Examples
600 ///
601 /// ```
602 /// use tenferro_cpu::CpuBackend;
603 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
604 ///
605 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
606 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
607 /// vec![2, 3],
608 /// vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0],
609 /// ).unwrap(), ctx.clone()).unwrap();
610 /// // Arrays, vectors, and slices use the same dynamic-shape contract.
611 /// for y in [x.reshape([6])?, x.reshape(vec![6])?, x.reshape(&[6][..])?] {
612 /// assert_eq!(y.shape(), &[6]);
613 /// assert_eq!(y.value()?.as_slice::<f64>()?, &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
614 /// }
615 /// # Ok::<(), tenferro_ad::Error>(())
616 /// ```
617 ///
618 /// # Errors
619 ///
620 /// Returns [`tenferro_tensor::ValidationError::ShapeMismatch`] when the element count
621 /// changes, `InvalidArgument` when the target shape product overflows, or a
622 /// typed backend/runtime-state error.
623 pub fn reshape(&self, shape: impl tenferro_tensor::IntoShapeVec) -> Result<Self> {
624 let shape = shape.into_shape_vec();
625 let op = StdTensorOp::Reshape {
626 to_shape: DimExpr::from_concrete(&shape),
627 };
628 // INVARIANT: a returned eager tensor cannot borrow `self`'s group, so
629 // the explicit duplicate precedes metadata-only view construction.
630 let base = self.to_tensor()?;
631 if let Ok(value) = TensorValue::from_tensor(base).reshape_view(shape) {
632 return Self::nary_value_op(&[self], op, value);
633 }
634 self.unary_op(op)
635 }
636
637 /// Slice with explicit start, limit, and stride per axis.
638 ///
639 /// # Examples
640 ///
641 /// ```
642 /// use tenferro_cpu::CpuBackend;
643 /// use tenferro_ad::{EagerRuntime, EagerTensor, SliceConfig, Tensor};
644 ///
645 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
646 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
647 /// let y = x
648 /// .slice(SliceConfig {
649 /// starts: vec![1],
650 /// limits: vec![3],
651 /// strides: vec![1],
652 /// })
653 /// .unwrap();
654 ///
655 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[2.0, 3.0]);
656 /// # Ok::<(), tenferro_ad::Error>(())
657 /// ```
658 ///
659 /// # Errors
660 ///
661 /// Returns [`tenferro_tensor::Error::Validation`] with
662 /// `AxisOutOfBounds`/`InvalidArgument` when starts, limits, or strides are
663 /// invalid, or a typed backend/runtime-state error while creating the view.
664 pub fn slice(&self, config: SliceConfig) -> Result<Self> {
665 // INVARIANT: the result must retain an independent owner while the
666 // input handle remains live; this is an explicit duplicate, not an
667 // implicit backend transfer.
668 let base = self.to_tensor()?;
669 let value = TensorValue::from_tensor(base)
670 .slice_view(&config)
671 .map_err(Error::TensorRuntime)?;
672 Self::nary_value_op(&[self], StdTensorOp::Slice(config), value)
673 }
674
675 /// Broadcast into a larger shape with explicit dimension placement.
676 ///
677 /// # Examples
678 ///
679 /// ```
680 /// use tenferro_cpu::CpuBackend;
681 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
682 ///
683 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
684 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(), ctx.clone()).unwrap();
685 /// let y = x.broadcast_in_dim(&[3, 2], &[0]).unwrap();
686 ///
687 /// assert_eq!(y.shape(), &[3, 2]);
688 /// # Ok::<(), tenferro_ad::Error>(())
689 /// ```
690 ///
691 /// # Errors
692 ///
693 /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds`,
694 /// `DuplicateAxis`, or `ShapeMismatch` when `shape`/`dims` cannot broadcast
695 /// the input, or a typed backend/runtime-state error.
696 pub fn broadcast_in_dim(&self, shape: &[usize], dims: &[usize]) -> Result<Self> {
697 if let Some(error) = broadcast_in_dim_extent_error(self.shape(), shape, dims) {
698 return Err(broadcast_error("EagerTensor::broadcast_in_dim", error));
699 }
700 let op = StdTensorOp::BroadcastInDim {
701 shape: DimExpr::from_concrete(shape),
702 dims: dims.to_vec(),
703 };
704 // INVARIANT: output descriptors cannot borrow the input's move-only
705 // allocation group, so this explicit duplicate owns the view's root.
706 let base = self.to_tensor()?;
707 let value = TensorValue::from_tensor(base)
708 .broadcast_in_dim_view(shape, dims)
709 .map_err(Error::TensorRuntime)?;
710 Self::nary_value_op(&[self], op, value)
711 }
712
713 /// Convert the tensor to a different dtype using checked conversion.
714 ///
715 /// Use [`cast`](Self::cast) when a lossy dtype projection is intended.
716 ///
717 /// # Examples
718 ///
719 /// ```
720 /// use tenferro_cpu::CpuBackend;
721 /// use tenferro_ad::{DType, EagerRuntime, EagerTensor, Tensor};
722 ///
723 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
724 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, -2.0]).unwrap(), ctx.clone()).unwrap();
725 /// let y = x.convert(DType::C64).unwrap();
726 ///
727 /// assert_eq!(y.dtype(), DType::C64);
728 /// assert_eq!(y.shape(), &[2]);
729 /// # Ok::<(), tenferro_ad::Error>(())
730 /// ```
731 ///
732 /// # Errors
733 ///
734 /// Returns [`tenferro_tensor::Error::UnsupportedDTypeConversion`] when the
735 /// requested pair is outside tenferro's checked dtype-promotion lattice.
736 /// Use [`cast`](Self::cast) for explicit lossy projection; backend
737 /// execution can additionally return a typed runtime-state error.
738 pub fn convert(&self, to: DType) -> Result<Self> {
739 tenferro_tensor::validate::validate_convert_dtype("EagerTensor::convert", self.dtype(), to)
740 .map_err(Error::TensorRuntime)?;
741 self.cast(to)
742 }
743
744 /// Cast the tensor to a different dtype using explicit dtype projection.
745 ///
746 /// `cast` may truncate, narrow precision, project complex values to their
747 /// real component, or use boolean truthiness where the backend supports the
748 /// requested projection.
749 ///
750 /// # Examples
751 ///
752 /// ```
753 /// use tenferro_cpu::CpuBackend;
754 /// use tenferro_ad::{DType, EagerRuntime, EagerTensor, Tensor};
755 ///
756 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
757 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.2_f64, -2.8]).unwrap(), ctx.clone()).unwrap();
758 /// let y = x.cast(DType::I32).unwrap();
759 ///
760 /// assert_eq!(y.value().unwrap().as_slice::<i32>().unwrap(), &[1, -2]);
761 /// # Ok::<(), tenferro_ad::Error>(())
762 /// ```
763 /// # Errors
764 ///
765 /// Returns a typed [`tenferro_tensor::Error::Unsupported`] when the eager
766 /// backend cannot project the requested dtype, or a backend/runtime-state
767 /// error during execution.
768 pub fn cast(&self, to: DType) -> Result<Self> {
769 self.unary_op(StdTensorOp::Convert {
770 from: self.dtype(),
771 to,
772 })
773 }
774
775 /// Pad with zeros using StableHLO-style edge and interior padding.
776 ///
777 /// # Examples
778 ///
779 /// ```
780 /// use tenferro_cpu::CpuBackend;
781 /// use tenferro_ad::{EagerRuntime, EagerTensor, PadConfig, Tensor};
782 ///
783 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
784 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
785 /// let y = x
786 /// .pad(PadConfig {
787 /// edge_padding_low: vec![1],
788 /// edge_padding_high: vec![1],
789 /// interior_padding: vec![1],
790 /// })
791 /// .unwrap();
792 ///
793 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[0.0, 1.0, 0.0, 2.0, 0.0]);
794 /// # Ok::<(), tenferro_ad::Error>(())
795 /// ```
796 /// # Errors
797 ///
798 /// Returns [`tenferro_runtime::Error::TensorRuntime`] containing
799 /// [`tenferro_tensor::ValidationError::InvalidArgument`] when a
800 /// padding vector has a length different from the input rank, interior
801 /// padding is negative, or edge/interior padding produces a negative
802 /// dimension or checked output-size arithmetic overflows.
803 /// Backend execution and unavailable runtime state are propagated as their
804 /// typed [`tenferro_runtime::Error::TensorRuntime`] or
805 /// [`tenferro_runtime::Error::RuntimeState`] variants.
806 pub fn pad(&self, config: PadConfig) -> Result<Self> {
807 self.unary_op(StdTensorOp::Pad(config))
808 }
809
810 /// Reverse the order of elements along the requested axes.
811 ///
812 /// # Examples
813 ///
814 /// ```
815 /// use tenferro_cpu::CpuBackend;
816 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
817 ///
818 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
819 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![4], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
820 /// let y = x.reverse(&[0]).unwrap();
821 ///
822 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[4.0, 3.0, 2.0, 1.0]);
823 /// # Ok::<(), tenferro_ad::Error>(())
824 /// ```
825 /// # Errors
826 ///
827 /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
828 /// `DuplicateAxis` for an invalid axis list, or a typed backend/
829 /// runtime-state error during execution.
830 pub fn reverse(&self, axes: &[usize]) -> Result<Self> {
831 validate_eager_axes("EagerTensor::reverse", self.shape().len(), axes)?;
832 self.unary_op(StdTensorOp::Reverse {
833 axes: axes.to_vec(),
834 })
835 }
836
837 /// Gather slices from `self` using integer start indices.
838 ///
839 /// # Examples
840 ///
841 /// ```
842 /// use tenferro_cpu::CpuBackend;
843 /// use tenferro_ad::{EagerRuntime, EagerTensor, GatherConfig, Tensor};
844 ///
845 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
846 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
847 /// vec![5],
848 /// vec![10.0_f64, 20.0, 30.0, 40.0, 50.0],
849 /// ).unwrap(), ctx.clone()).unwrap();
850 /// let indices = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3], vec![4_i64, 1, 0]).unwrap(), ctx.clone()).unwrap();
851 /// let y = x
852 /// .gather(
853 /// &indices,
854 /// GatherConfig {
855 /// offset_dims: vec![],
856 /// collapsed_slice_dims: vec![0],
857 /// start_index_map: vec![0],
858 /// index_vector_dim: 1,
859 /// slice_sizes: vec![1],
860 /// },
861 /// )
862 /// .unwrap();
863 ///
864 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[50.0, 20.0, 10.0]);
865 /// # Ok::<(), tenferro_ad::Error>(())
866 /// ```
867 /// # Errors
868 ///
869 /// Returns [`tenferro_tensor::Error::Validation`] when the gather
870 /// configuration has an invalid rank, axis, shape, or index dtype, or a
871 /// typed backend/runtime-state error.
872 pub fn gather(&self, indices: &Self, config: GatherConfig) -> Result<Self> {
873 self.binary_op(indices, StdTensorOp::Gather(config))
874 }
875
876 /// Scatter updates into `self` using StableHLO scatter semantics.
877 ///
878 /// # Examples
879 ///
880 /// ```
881 /// use tenferro_cpu::CpuBackend;
882 /// use tenferro_ad::{EagerRuntime, EagerTensor, ScatterConfig, Tensor};
883 ///
884 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
885 /// let operand = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![4], vec![0.0_f64, 0.0, 0.0, 0.0]).unwrap(), ctx.clone()).unwrap();
886 /// let indices = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 1], vec![1_i64, 3]).unwrap(), ctx.clone()).unwrap();
887 /// let updates = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![5.0_f64, 7.0]).unwrap(), ctx.clone()).unwrap();
888 /// let result = operand
889 /// .scatter(
890 /// &indices,
891 /// &updates,
892 /// ScatterConfig {
893 /// update_window_dims: vec![],
894 /// inserted_window_dims: vec![0],
895 /// scatter_dims_to_operand_dims: vec![0],
896 /// index_vector_dim: 1,
897 /// },
898 /// )
899 /// .unwrap();
900 ///
901 /// assert_eq!(result.value().unwrap().as_slice::<f64>().unwrap(), &[0.0, 5.0, 0.0, 7.0]);
902 /// # Ok::<(), tenferro_ad::Error>(())
903 /// ```
904 /// # Errors
905 ///
906 /// Returns [`tenferro_tensor::Error::Validation`] when the scatter
907 /// configuration, index/update shapes, or index dtype is invalid, or a
908 /// typed backend/runtime-state error.
909 pub fn scatter(&self, indices: &Self, updates: &Self, config: ScatterConfig) -> Result<Self> {
910 self.ternary_op(indices, updates, StdTensorOp::Scatter(config))
911 }
912
913 /// Slice using runtime start indices.
914 ///
915 /// # Examples
916 ///
917 /// ```
918 /// use tenferro_cpu::CpuBackend;
919 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
920 ///
921 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
922 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![5], vec![1.0_f64, 2.0, 3.0, 4.0, 5.0]).unwrap(), ctx.clone()).unwrap();
923 /// let starts = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![1], vec![2_i64]).unwrap(), ctx.clone()).unwrap();
924 /// let y = x.dynamic_slice(&starts, &[2]).unwrap();
925 ///
926 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[3.0, 4.0]);
927 /// # Ok::<(), tenferro_ad::Error>(())
928 /// ```
929 /// # Errors
930 ///
931 /// Returns [`tenferro_tensor::Error::Validation`] when `starts` has the
932 /// wrong dtype/shape or `sizes` exceeds the operand rank, including an
933 /// `AxisOutOfBounds` or `ShapeMismatch`, or a typed backend/runtime-state
934 /// error.
935 pub fn dynamic_slice(&self, starts: &Self, sizes: &[usize]) -> Result<Self> {
936 self.binary_op(
937 starts,
938 StdTensorOp::DynamicSlice {
939 slice_sizes: sizes.to_vec(),
940 },
941 )
942 }
943
944 /// Concatenate tensors along one axis.
945 ///
946 /// # Examples
947 ///
948 /// ```
949 /// use tenferro_cpu::CpuBackend;
950 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
951 ///
952 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
953 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap(), ctx.clone()).unwrap();
954 /// let y = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap(), ctx.clone()).unwrap();
955 /// let z = EagerTensor::concatenate(&[&x, &y], 0).unwrap();
956 ///
957 /// assert_eq!(z.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 2.0, 3.0, 4.0]);
958 /// # Ok::<(), tenferro_ad::Error>(())
959 /// ```
960 /// # Errors
961 ///
962 /// Returns [`tenferro_tensor::ValidationError::InvalidArgument`] when `tensors` is
963 /// empty or `axis` is outside the rank, `ShapeMismatch`/`DTypeMismatch`
964 /// when inputs cannot be concatenated, or a typed backend/runtime-state
965 /// error.
966 pub fn concatenate(tensors: &[&Self], axis: usize) -> Result<Self> {
967 Self::nary_op(
968 tensors,
969 StdTensorOp::Concatenate {
970 axis,
971 input_count: tensors.len(),
972 },
973 )
974 }
975
976 /// Extract the diagonal along two axes.
977 ///
978 /// # Examples
979 ///
980 /// ```
981 /// use tenferro_cpu::CpuBackend;
982 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
983 ///
984 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
985 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(
986 /// vec![3, 3],
987 /// vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
988 /// ).unwrap(), ctx.clone()).unwrap();
989 /// let y = x.extract_diag(0, 1).unwrap();
990 ///
991 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 5.0, 9.0]);
992 /// # Ok::<(), tenferro_ad::Error>(())
993 /// ```
994 /// # Errors
995 ///
996 /// Returns [`tenferro_tensor::Error::Validation`] with `RankMismatch`,
997 /// `AxisOutOfBounds`, or `DuplicateAxis` when the selected axes cannot form
998 /// a diagonal, or a typed backend/runtime-state error.
999 pub fn extract_diag(&self, axis_a: usize, axis_b: usize) -> Result<Self> {
1000 self.unary_op(StdTensorOp::ExtractDiag { axis_a, axis_b })
1001 }
1002
1003 /// Embed a vector or lower-rank tensor along a diagonal.
1004 ///
1005 /// # Examples
1006 ///
1007 /// ```
1008 /// use tenferro_cpu::CpuBackend;
1009 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1010 ///
1011 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1012 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap(), ctx.clone()).unwrap();
1013 /// let y = x.embed_diag(0, 1).unwrap();
1014 ///
1015 /// assert_eq!(y.shape(), &[3, 3]);
1016 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 3.0]);
1017 /// # Ok::<(), tenferro_ad::Error>(())
1018 /// ```
1019 /// # Errors
1020 ///
1021 /// Returns [`tenferro_tensor::Error::Validation`] with `RankMismatch`,
1022 /// `AxisOutOfBounds`, or `DuplicateAxis` when the diagonal axes are not
1023 /// valid for embedding, or a typed backend/runtime-state error.
1024 pub fn embed_diag(&self, axis_a: usize, axis_b: usize) -> Result<Self> {
1025 self.unary_op(StdTensorOp::EmbedDiag { axis_a, axis_b })
1026 }
1027
1028 /// Keep the lower triangle and zero the rest.
1029 ///
1030 /// # Examples
1031 ///
1032 /// ```
1033 /// use tenferro_cpu::CpuBackend;
1034 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1035 ///
1036 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1037 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1038 /// let y = x.tril(0).unwrap();
1039 ///
1040 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 2.0, 0.0, 4.0]);
1041 /// # Ok::<(), tenferro_ad::Error>(())
1042 /// ```
1043 /// # Errors
1044 ///
1045 /// Returns [`tenferro_tensor::ValidationError::RankMismatch`] when the operand is not
1046 /// a matrix, or a typed unsupported/backend/runtime-state error.
1047 pub fn tril(&self, k: i64) -> Result<Self> {
1048 self.unary_op(StdTensorOp::Tril { k })
1049 }
1050
1051 /// Keep the upper triangle and zero the rest.
1052 ///
1053 /// # Examples
1054 ///
1055 /// ```
1056 /// use tenferro_cpu::CpuBackend;
1057 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1058 ///
1059 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1060 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1061 /// let y = x.triu(0).unwrap();
1062 ///
1063 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0, 0.0, 3.0, 4.0]);
1064 /// # Ok::<(), tenferro_ad::Error>(())
1065 /// ```
1066 /// # Errors
1067 ///
1068 /// Returns [`tenferro_tensor::ValidationError::RankMismatch`] when the operand is not
1069 /// a matrix, or a typed unsupported/backend/runtime-state error.
1070 pub fn triu(&self, k: i64) -> Result<Self> {
1071 self.unary_op(StdTensorOp::Triu { k })
1072 }
1073
1074 /// Reduce product over the requested axes.
1075 ///
1076 /// # Examples
1077 ///
1078 /// ```
1079 /// use tenferro_cpu::CpuBackend;
1080 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1081 ///
1082 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1083 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1084 /// let y = x.reduce_prod(None).unwrap();
1085 ///
1086 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[24.0]);
1087 /// # Ok::<(), tenferro_ad::Error>(())
1088 /// ```
1089 /// # Errors
1090 ///
1091 /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
1092 /// `DuplicateAxis` for an invalid reduction axis, or a typed
1093 /// unsupported/backend/runtime-state error for the selected dtype.
1094 pub fn reduce_prod(&self, axes: Option<&[usize]>) -> Result<Self> {
1095 let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
1096 validate_eager_axes("EagerTensor::reduce_prod", self.shape().len(), &axes)?;
1097 self.unary_op(StdTensorOp::ReduceProd { axes })
1098 }
1099
1100 /// Reduce maximum over the requested axes.
1101 ///
1102 /// # Examples
1103 ///
1104 /// ```
1105 /// use tenferro_cpu::CpuBackend;
1106 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1107 ///
1108 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1109 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1110 /// let y = x.reduce_max(None).unwrap();
1111 ///
1112 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[4.0]);
1113 /// # Ok::<(), tenferro_ad::Error>(())
1114 /// ```
1115 /// # Errors
1116 ///
1117 /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
1118 /// `DuplicateAxis` for an invalid reduction axis, or a typed
1119 /// unsupported/backend/runtime-state error for the selected dtype.
1120 pub fn reduce_max(&self, axes: Option<&[usize]>) -> Result<Self> {
1121 let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
1122 validate_eager_axes("EagerTensor::reduce_max", self.shape().len(), &axes)?;
1123 self.unary_op(StdTensorOp::ReduceMax { axes })
1124 }
1125
1126 /// Reduce minimum over the requested axes.
1127 ///
1128 /// # Examples
1129 ///
1130 /// ```
1131 /// use tenferro_cpu::CpuBackend;
1132 /// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1133 ///
1134 /// let ctx = EagerRuntime::with_cpu_backend(CpuBackend::new())?;
1135 /// let x = EagerTensor::from_tensor_in(Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap(), ctx.clone()).unwrap();
1136 /// let y = x.reduce_min(None).unwrap();
1137 ///
1138 /// assert_eq!(y.value().unwrap().as_slice::<f64>().unwrap(), &[1.0]);
1139 /// # Ok::<(), tenferro_ad::Error>(())
1140 /// ```
1141 /// # Errors
1142 ///
1143 /// Returns [`tenferro_tensor::Error::Validation`] with `AxisOutOfBounds` or
1144 /// `DuplicateAxis` for an invalid reduction axis, or a typed
1145 /// unsupported/backend/runtime-state error for the selected dtype.
1146 pub fn reduce_min(&self, axes: Option<&[usize]>) -> Result<Self> {
1147 let axes = axes.map_or_else(|| (0..self.shape().len()).collect(), <[usize]>::to_vec);
1148 validate_eager_axes("EagerTensor::reduce_min", self.shape().len(), &axes)?;
1149 self.unary_op(StdTensorOp::ReduceMin { axes })
1150 }
1151
1152 pub(crate) fn unary_op(&self, op: StdTensorOp) -> Result<Self> {
1153 Self::nary_op(&[self], op)
1154 }
1155
1156 pub(crate) fn binary_op(&self, other: &Self, op: StdTensorOp) -> Result<Self> {
1157 Self::nary_op(&[self, other], op)
1158 }
1159
1160 pub(crate) fn ternary_op(&self, b: &Self, c: &Self, op: StdTensorOp) -> Result<Self> {
1161 Self::nary_op(&[self, b, c], op)
1162 }
1163
1164 pub(crate) fn nary_value_op(
1165 tensors: &[&Self],
1166 op: StdTensorOp,
1167 value: TensorValue,
1168 ) -> Result<Self> {
1169 let Some(first) = tensors.first() else {
1170 return Err(empty_nary_input_error(&op));
1171 };
1172
1173 let ctx = Arc::clone(&first.ctx);
1174 for tensor in tensors.iter().skip(1) {
1175 if !first.same_context(tensor) {
1176 return Err(Error::ContextMismatch {
1177 lhs: first.ctx_id(),
1178 rhs: tensor.ctx_id(),
1179 });
1180 }
1181 }
1182
1183 if !eager_grad_recording_enabled()
1184 || (!eager_capture_active() && !tensors.iter().any(|tensor| tensor.requires_grad))
1185 {
1186 return Self::new_untracked_value_result(ctx, value);
1187 }
1188
1189 let output_ref = &value;
1190 let mut recorded = record_eager_value_outputs(&op, &[output_ref], tensors)?;
1191 let trace = recorded.traces.pop().ok_or_else(|| {
1192 Error::Internal(format!("expected one eager trace for {:?}, got 0", op))
1193 })?;
1194 let semantic_trace = recorded.semantic_traces.pop().flatten();
1195
1196 Self::new_result_value(
1197 ctx,
1198 trace.key,
1199 value,
1200 trace.requires_grad,
1201 trace.trace,
1202 semantic_trace,
1203 )
1204 }
1205
1206 pub(crate) fn nary_op(tensors: &[&Self], op: StdTensorOp) -> Result<Self> {
1207 let total_started = eager_op_profile_start();
1208 let Some(first) = tensors.first() else {
1209 return Err(empty_nary_input_error(&op));
1210 };
1211 let expected = op.input_count();
1212 if tensors.len() != expected {
1213 return Err(wrong_nary_input_count_error(&op, expected, tensors.len()));
1214 }
1215
1216 let ctx = Arc::clone(&first.ctx);
1217 profile_eager_op_section("nary_op.context_check", || -> Result<()> {
1218 for tensor in tensors.iter().skip(1) {
1219 if !first.same_context(tensor) {
1220 return Err(Error::ContextMismatch {
1221 lhs: first.ctx_id(),
1222 rhs: tensor.ctx_id(),
1223 });
1224 }
1225 }
1226 Ok(())
1227 })?;
1228
1229 let any_requires_grad = profile_eager_op_section("nary_op.requires_grad_scan", || {
1230 eager_grad_recording_enabled()
1231 && (eager_capture_active() || tensors.iter().any(|tensor| tensor.requires_grad))
1232 });
1233 if !any_requires_grad {
1234 let input_reads = profile_eager_op_section("nary_op.collect_input_reads", || {
1235 tensors
1236 .iter()
1237 .map(|tensor| tensor.tensor_read())
1238 .collect::<Vec<_>>()
1239 });
1240 let output = profile_eager_op_section("nary_op.exec_single_output_read", || {
1241 exec_single_output_read(&op, &input_reads, &ctx)
1242 })?;
1243 let result = profile_eager_op_section("nary_op.new_untracked_result", || {
1244 Self::new_untracked_result(ctx, output)
1245 });
1246 if let Some(total_started) = total_started {
1247 record_eager_op_profile("nary_op.total", total_started.elapsed());
1248 maybe_print_eager_op_profile();
1249 }
1250 return result;
1251 }
1252
1253 let input_arcs = profile_eager_op_section("nary_op.materialize_inputs", || {
1254 tensors
1255 .iter()
1256 .map(|tensor| tensor.to_tensor().map(Arc::new))
1257 .collect::<Result<Vec<_>>>()
1258 })?;
1259 let inputs: Vec<&Tensor> = profile_eager_op_section("nary_op.collect_inputs", || {
1260 input_arcs.iter().map(|tensor| tensor.as_ref()).collect()
1261 });
1262 let output = profile_eager_op_section("nary_op.exec_single_output", || {
1263 exec_single_output(&op, &inputs, &ctx)
1264 })?;
1265
1266 let outputs = vec![&output];
1267 let mut recorded = profile_eager_op_section("nary_op.record_outputs", || {
1268 record_eager_outputs(&op, &outputs, tensors)
1269 })?;
1270 let trace = recorded.traces.pop().ok_or_else(|| {
1271 Error::Internal(format!("expected one eager trace for {:?}, got 0", op))
1272 })?;
1273 let semantic_trace = recorded.semantic_traces.pop().flatten();
1274
1275 let result = profile_eager_op_section("nary_op.new_tracked_result", || {
1276 Self::new_result_with_semantic_trace(
1277 ctx,
1278 trace.key,
1279 output,
1280 trace.requires_grad,
1281 trace.trace,
1282 semantic_trace,
1283 )
1284 });
1285 if let Some(total_started) = total_started {
1286 record_eager_op_profile("nary_op.total", total_started.elapsed());
1287 maybe_print_eager_op_profile();
1288 }
1289 result
1290 }
1291}
1292
1293fn validate_eager_axes(op: &'static str, rank: usize, axes: &[usize]) -> Result<()> {
1294 tenferro_tensor::validate::validate_unique_axes(op, "axis", rank, axes)
1295 .map_err(Error::TensorRuntime)
1296}
1297
1298fn validate_eager_dot_general_config(
1299 _op: &'static str,
1300 config: &DotGeneralConfig,
1301 lhs_rank: usize,
1302 rhs_rank: usize,
1303) -> Result<()> {
1304 config
1305 .validate_dims_with_ranks(lhs_rank, rhs_rank)
1306 .map_err(Error::TensorRuntime)
1307}
1308
1309fn empty_nary_input_error(op: &StdTensorOp) -> Error {
1310 Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1311 eager_validation_op_name(op),
1312 "inputs",
1313 "operation requires at least one input tensor",
1314 ))
1315}
1316
1317fn wrong_nary_input_count_error(op: &StdTensorOp, expected: usize, actual: usize) -> Error {
1318 Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1319 eager_validation_op_name(op),
1320 "inputs",
1321 format!("operation expects {expected} inputs, got {actual}"),
1322 ))
1323}
1324
1325fn eager_validation_op_name(op: &StdTensorOp) -> &'static str {
1326 match op {
1327 StdTensorOp::Concatenate { .. } => "concatenate",
1328 _ => "eager_nary_op",
1329 }
1330}
1331
1332fn finite_real_factor(value: f64) -> Result<f64> {
1333 if value.is_finite() {
1334 Ok(value)
1335 } else {
1336 Err(Error::TensorRuntime(
1337 tenferro_tensor::Error::invalid_argument(
1338 "scale_real",
1339 "factor",
1340 format!("real scalar must be finite, got {value}"),
1341 ),
1342 ))
1343 }
1344}
1345
1346fn round_real_to_i64(value: f64) -> Result<i64> {
1347 let rounded = finite_real_factor(value)?.round();
1348 if rounded < i64::MIN as f64 || rounded >= -(i64::MIN as f64) {
1349 return Err(Error::TensorRuntime(
1350 tenferro_tensor::Error::invalid_argument(
1351 "scale_real",
1352 "factor",
1353 format!("rounded real scalar {rounded} is out of i64 range"),
1354 ),
1355 ));
1356 }
1357 Ok(rounded as i64)
1358}
1359
1360fn round_real_to_i32(value: f64) -> Result<i32> {
1361 let rounded = round_real_to_i64(value)?;
1362 i32::try_from(rounded).map_err(|_| {
1363 Error::TensorRuntime(tenferro_tensor::Error::invalid_argument(
1364 "scale_real",
1365 "factor",
1366 format!("rounded real scalar {rounded} is out of i32 range"),
1367 ))
1368 })
1369}
1370
1371fn bool_from_real(value: f64) -> Result<bool> {
1372 Ok(finite_real_factor(value)? != 0.0)
1373}