tenferro_linalg/eager_ext.rs
1// Solve residual policy reference: PyTorch 8dd3b763, derivatives.yaml's
2// _linalg_solve_ex and FunctionsManual.cpp::linalg_solve_backward. The tracked
3// implementation runs the fused LuFactorSolve op, whose saved factors feed the
4// LuSolvePrepared adjoint solve.
5use std::sync::Arc;
6
7use tenferro_ad::error::{Error, Result};
8use tenferro_ad::extension::{
9 apply_eager_with_targeted_extension_in_session, apply_eager_with_targeted_extension_session,
10 EagerExtensionBackendKind, EagerExtensionTarget,
11};
12use tenferro_ad::{EagerSession, EagerTensor};
13use tenferro_cpu::CpuEngine;
14#[cfg(feature = "cuda")]
15use tenferro_gpu::cuda::CudaEngine;
16use tenferro_runtime::{ErrorPhase, ExecutionEngine, ExtensionModule};
17
18use crate::eager_composites;
19use crate::extension::{
20 extension_module, validate_derivative_eps, EighOptions, LinalgExtensionOp, LinalgOp, QrOptions,
21 SvdOptions,
22};
23use crate::rank_revealing_qr::validate_rank_revealing_qr_options;
24use crate::{RankRevealingQrOptions, RankRevealingQrResult};
25
26/// Tensor-owned linear solve. Other eager linear-algebra operations use
27/// [`EagerSessionLinalgExt`] inside [`tenferro_ad::EagerRuntime::with_eager_session`].
28///
29/// The tensor-owned entry preserves calling-thread `no_grad` behavior for
30/// tracked solves; it must not be called from inside a borrowed session.
31///
32/// # Examples
33/// ```rust
34/// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
35/// # use tenferro_linalg::EagerTensorLinalgExt;
36/// # let ctx = EagerRuntime::new()?;
37/// # let a = EagerTensor::from_tensor_in(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?, ctx.clone())?;
38/// # let b = EagerTensor::from_tensor_in(Tensor::from_vec_col_major([1, 1], vec![4.0_f64])?, ctx)?;
39/// let x = a.solve(&mut tenferro_cpu::CpuEngine::serial(), &b)?;
40/// assert_eq!(x.shape(), &[1, 1]);
41/// # Ok::<(), tenferro_ad::Error>(())
42/// ```
43#[cfg_attr(docsrs, doc(cfg(feature = "autodiff")))]
44pub trait EagerTensorLinalgExt {
45 /// The CUDA provider does not inspect the cuSOLVER `getrf` status flag on
46 /// the forward solve path, so it performs no per-call non-singularity check
47 /// and issues no host round trip. An exactly singular input is left to the
48 /// caller's verification, as in `torch.linalg.solve`. CPU providers may
49 /// still report their own exactly singular factor as `Error::Singular`.
50 ///
51 /// # Errors
52 ///
53 /// Returns `Error::Validation` for incompatible matrix, batch, or dtype
54 /// metadata, `Error::Extension` for an unsupported dtype or singular
55 /// system, and `Error::RuntimeState` when the backend is unavailable.
56 /// # Examples
57 ///
58 /// ```rust
59 /// # use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
60 /// # use tenferro_cpu::CpuEngine;
61 /// # use tenferro_linalg::EagerTensorLinalgExt;
62 /// # let ctx = EagerRuntime::new()?;
63 /// # let a = EagerTensor::from_tensor_in(
64 /// # Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
65 /// # ctx.clone(),
66 /// # )?;
67 /// # let b = EagerTensor::from_tensor_in(
68 /// # Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0]).unwrap(),
69 /// # ctx,
70 /// # )?;
71 /// let x = a.solve(&mut tenferro_cpu::CpuEngine::serial(), &b)?;
72 /// assert_eq!(x.value()?.as_slice::<f64>()?, &[2.0, 2.0]);
73 /// # Ok::<(), tenferro_ad::Error>(())
74 /// ```
75 fn solve(&self, engine: &mut dyn ExecutionEngine, b: &EagerTensor) -> Result<EagerTensor>;
76}
77
78impl EagerTensorLinalgExt for EagerTensor {
79 fn solve(&self, engine: &mut dyn ExecutionEngine, b: &EagerTensor) -> Result<EagerTensor> {
80 solve(engine, self, b)
81 }
82}
83
84/// Linear algebra operations on a runtime-bound borrowed eager session.
85///
86/// # Examples
87/// ```rust
88/// use tenferro_ad::{EagerRuntime, Tensor};
89/// use tenferro_linalg::EagerSessionLinalgExt;
90/// let ctx = EagerRuntime::new()?;
91/// let factor = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |session| {
92/// let input = session.constant_from(Tensor::from_vec_col_major(vec![1, 1], vec![4.0_f64])?)?;
93/// session.cholesky(&input)
94/// })?;
95/// assert_eq!(factor.value()?.as_slice::<f64>()?, &[2.0]);
96/// # Ok::<(), tenferro_ad::Error>(())
97/// ```
98#[cfg_attr(docsrs, doc(cfg(feature = "autodiff")))]
99pub trait EagerSessionLinalgExt {
100 /// Compute the lower Cholesky factor without reopening the eager backend.
101 ///
102 /// # Examples
103 /// ```rust
104 /// use tenferro_ad::{EagerRuntime, Tensor};
105 /// use tenferro_linalg::EagerSessionLinalgExt;
106 /// let ctx = EagerRuntime::new()?;
107 /// let factor = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |session| {
108 /// let input = session.constant_from(Tensor::from_vec_col_major(vec![1, 1], vec![9.0_f64])?)?;
109 /// session.cholesky(&input)
110 /// })?;
111 /// assert_eq!(factor.value()?.as_slice::<f64>()?, &[3.0]);
112 /// # Ok::<(), tenferro_ad::Error>(())
113 /// ```
114 /// # Errors
115 /// Returns typed validation, extension, backend, module or unsupported-executor errors.
116 fn cholesky(&mut self, input: &EagerTensor) -> Result<EagerTensor>;
117
118 /// Compute a thin singular value decomposition in this borrowed session.
119 ///
120 /// # Examples
121 /// ```rust
122 /// use tenferro_ad::{EagerRuntime, Tensor};
123 /// use tenferro_linalg::EagerSessionLinalgExt;
124 /// let ctx = EagerRuntime::new()?;
125 /// let (_u, values, _vt) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
126 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![3.0_f64])?)?;
127 /// s.svd(&a)
128 /// })?;
129 /// assert_eq!(values.value()?.as_slice::<f64>()?, &[3.0]);
130 /// # Ok::<(), tenferro_ad::Error>(())
131 /// ```
132 /// # Errors
133 /// Returns typed validation, extension, backend, or unsupported-executor errors.
134 fn svd(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor)>;
135
136 /// Compute thin SVD with an explicit gauge, driver, and derivative regularizer.
137 ///
138 /// # Examples
139 /// ```rust
140 /// use tenferro_ad::{EagerRuntime, Tensor};
141 /// use tenferro_linalg::{EagerSessionLinalgExt, SvdOptions};
142 /// let ctx = EagerRuntime::new()?;
143 /// let (_u, values, _vt) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
144 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![3.0_f64])?)?;
145 /// s.svd_with_options(&a, SvdOptions::default())
146 /// })?;
147 /// assert_eq!(values.value()?.as_slice::<f64>()?, &[3.0]);
148 /// # Ok::<(), tenferro_ad::Error>(())
149 /// ```
150 /// # Errors
151 /// Returns typed invalid-tolerance, extension, backend, or unsupported errors.
152 fn svd_with_options(
153 &mut self,
154 input: &EagerTensor,
155 options: SvdOptions,
156 ) -> Result<(EagerTensor, EagerTensor, EagerTensor)>;
157
158 /// Compute full-matrices SVD, including the right nullspace.
159 ///
160 /// # Examples
161 /// ```rust
162 /// use tenferro_ad::{EagerRuntime, Tensor};
163 /// use tenferro_linalg::EagerSessionLinalgExt;
164 /// let ctx = EagerRuntime::new()?;
165 /// let (u, values, vh) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
166 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 2], vec![1.0_f64, 1.0])?)?;
167 /// s.svd_full(&a)
168 /// })?;
169 /// assert_eq!(u.shape(), &[1, 1]);
170 /// assert_eq!(values.shape(), &[1]);
171 /// assert_eq!(vh.shape(), &[2, 2]);
172 /// # Ok::<(), tenferro_ad::Error>(())
173 /// ```
174 /// # Errors
175 /// Returns `Error::Validation` with `ValidationError::RankMismatch` when the
176 /// input is not a (batched) matrix, `Error::UnsupportedAdRule` when a
177 /// traced input needs a derivative this decomposition does not provide,
178 /// `Error::Extension` carrying the linalg failure
179 /// (for example `Error::NonConvergence` or an unsupported dtype), or
180 /// `Error::TensorRuntime` for a backend failure.
181 fn svd_full(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor)>;
182
183 /// Compute a thin QR decomposition in this borrowed session.
184 ///
185 /// # Examples
186 /// ```rust
187 /// use tenferro_ad::{EagerRuntime, Tensor};
188 /// use tenferro_linalg::EagerSessionLinalgExt;
189 /// let ctx = EagerRuntime::new()?;
190 /// let (q, r) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
191 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![3.0_f64])?)?;
192 /// s.qr(&a)
193 /// })?;
194 /// assert_eq!(q.shape(), &[1, 1]);
195 /// assert_eq!(r.shape(), &[1, 1]);
196 /// # Ok::<(), tenferro_ad::Error>(())
197 /// ```
198 /// # Errors
199 /// Returns typed validation, extension, backend, or unsupported-executor errors.
200 fn qr(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor)>;
201
202 /// Compute QR with an explicit post-processing gauge.
203 ///
204 /// The CUDA provider does not read the cuSOLVER `geqrf`/`orgqr` status
205 /// flags, so forward QR issues no host round trip and reports neither a
206 /// vendor parameter error nor a rank deficiency. A rank-deficient input
207 /// still yields valid factors.
208 ///
209 /// # Examples
210 /// ```rust
211 /// use tenferro_ad::{EagerRuntime, Tensor};
212 /// use tenferro_linalg::{EagerSessionLinalgExt, QrOptions};
213 /// let ctx = EagerRuntime::new()?;
214 /// let (q, r) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
215 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![3.0_f64])?)?;
216 /// s.qr_with_options(&a, QrOptions::default())
217 /// })?;
218 /// assert_eq!(q.shape(), &[1, 1]);
219 /// assert_eq!(r.shape(), &[1, 1]);
220 /// # Ok::<(), tenferro_ad::Error>(())
221 /// ```
222 /// # Errors
223 /// Returns typed validation, extension, backend, or unsupported errors.
224 fn qr_with_options(
225 &mut self,
226 input: &EagerTensor,
227 options: QrOptions,
228 ) -> Result<(EagerTensor, EagerTensor)>;
229
230 /// Solve a triangular system in this borrowed session.
231 ///
232 /// # Examples
233 /// ```rust
234 /// use tenferro_ad::{EagerRuntime, Tensor};
235 /// use tenferro_linalg::EagerSessionLinalgExt;
236 /// let ctx = EagerRuntime::new()?;
237 /// let x = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
238 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
239 /// let b = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![4.0_f64])?)?;
240 /// s.triangular_solve(&a, &b, true, true, false, false)
241 /// })?;
242 /// assert_eq!(x.value()?.as_slice::<f64>()?, &[2.0]);
243 /// # Ok::<(), tenferro_ad::Error>(())
244 /// ```
245 /// # Errors
246 /// Returns typed validation, extension, backend, or unsupported-executor errors.
247 fn triangular_solve(
248 &mut self,
249 matrix: &EagerTensor,
250 rhs: &EagerTensor,
251 left_side: bool,
252 lower: bool,
253 transpose_a: bool,
254 unit_diagonal: bool,
255 ) -> Result<EagerTensor>;
256
257 /// Solve a square linear system in this borrowed session.
258 ///
259 /// # Examples
260 /// ```rust
261 /// use tenferro_ad::{EagerRuntime, Tensor};
262 /// use tenferro_linalg::EagerSessionLinalgExt;
263 /// let ctx = EagerRuntime::new()?;
264 /// let x = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
265 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
266 /// let b = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![4.0_f64])?)?;
267 /// s.solve(&a, &b)
268 /// })?;
269 /// assert_eq!(x.value()?.as_slice::<f64>()?, &[2.0]);
270 /// # Ok::<(), tenferro_ad::Error>(())
271 /// ```
272 /// The CUDA provider does not inspect the cuSOLVER `getrf` status flag on
273 /// the forward solve path, so it performs no per-call non-singularity check
274 /// and issues no host round trip. An exactly singular input is left to the
275 /// caller's verification, as in `torch.linalg.solve`. CPU providers may
276 /// still report their own exactly singular factor as `Error::Singular`.
277 ///
278 /// # Errors
279 /// Returns typed validation, extension, backend, or unsupported-executor errors.
280 fn solve(&mut self, matrix: &EagerTensor, rhs: &EagerTensor) -> Result<EagerTensor>;
281
282 /// Solve a full-column-rank least-squares problem inside this session.
283 ///
284 /// # Examples
285 /// ```rust
286 /// use tenferro_ad::{EagerRuntime, Tensor};
287 /// use tenferro_linalg::EagerSessionLinalgExt;
288 /// let ctx = EagerRuntime::new()?;
289 /// let x = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
290 /// let a = s.constant_from(Tensor::from_vec_col_major([2, 1], vec![1.0_f64, 2.0])?)?;
291 /// let b = s.constant_from(Tensor::from_vec_col_major([2, 1], vec![2.0_f64, 4.0])?)?;
292 /// s.lstsq(&a, &b)
293 /// })?;
294 /// assert!((x.value()?.as_slice::<f64>()?[0] - 2.0).abs() < 1e-12);
295 /// # Ok::<(), tenferro_ad::Error>(())
296 /// ```
297 /// # Errors
298 /// Returns typed rank/shape/dtype validation, extension, backend, or unsupported errors.
299 fn lstsq(&mut self, matrix: &EagerTensor, rhs: &EagerTensor) -> Result<EagerTensor>;
300
301 /// Factor a matrix without leaving this borrowed session.
302 ///
303 /// # Examples
304 /// ```rust
305 /// use tenferro_ad::{EagerRuntime, Tensor};
306 /// use tenferro_linalg::EagerSessionLinalgExt;
307 /// let ctx = EagerRuntime::new()?;
308 /// let (_p, _l, u, _parity) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
309 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
310 /// s.lu(&a)
311 /// })?;
312 /// assert_eq!(u.value()?.as_slice::<f64>()?, &[2.0]);
313 /// # Ok::<(), tenferro_ad::Error>(())
314 /// ```
315 /// # Errors
316 /// Returns typed validation, extension, backend, or unsupported-executor errors.
317 fn lu(
318 &mut self,
319 input: &EagerTensor,
320 ) -> Result<(EagerTensor, EagerTensor, EagerTensor, EagerTensor)>;
321
322 /// Compute determinant sign and logarithm of its absolute value in this session.
323 ///
324 /// # Examples
325 /// ```rust
326 /// use tenferro_ad::{EagerRuntime, Tensor};
327 /// use tenferro_linalg::EagerSessionLinalgExt;
328 /// let ctx = EagerRuntime::new()?;
329 /// let (sign, logabs) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
330 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
331 /// s.slogdet(&a)
332 /// })?;
333 /// assert_eq!(sign.value()?.as_slice::<f64>()?, &[1.0]);
334 /// assert!((logabs.value()?.as_slice::<f64>()?[0] - 2.0_f64.ln()).abs() < 1e-12);
335 /// # Ok::<(), tenferro_ad::Error>(())
336 /// ```
337 /// # Errors
338 /// Returns typed validation, extension, backend, or unsupported-executor errors.
339 fn slogdet(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor)>;
340
341 /// Compute a determinant inside the borrowed session.
342 ///
343 /// # Examples
344 /// ```rust
345 /// use tenferro_ad::{EagerRuntime, Tensor};
346 /// use tenferro_linalg::EagerSessionLinalgExt;
347 /// let ctx = EagerRuntime::new()?;
348 /// let det = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
349 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
350 /// s.det(&a)
351 /// })?;
352 /// assert_eq!(det.value()?.as_slice::<f64>()?, &[2.0]);
353 /// # Ok::<(), tenferro_ad::Error>(())
354 /// ```
355 /// # Errors
356 /// Returns typed validation, extension, backend, or unsupported-executor errors.
357 fn det(&mut self, input: &EagerTensor) -> Result<EagerTensor>;
358
359 /// Invert a square matrix inside this borrowed session.
360 ///
361 /// # Examples
362 /// ```rust
363 /// use tenferro_ad::{EagerRuntime, Tensor};
364 /// use tenferro_linalg::EagerSessionLinalgExt;
365 /// let ctx = EagerRuntime::new()?;
366 /// let inverse = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
367 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
368 /// s.inv(&a)
369 /// })?;
370 /// assert_eq!(inverse.value()?.as_slice::<f64>()?, &[0.5]);
371 /// # Ok::<(), tenferro_ad::Error>(())
372 /// ```
373 /// # Errors
374 /// Returns typed rank/shape/dtype validation, extension, backend, or unsupported errors.
375 fn inv(&mut self, input: &EagerTensor) -> Result<EagerTensor>;
376
377 /// Compute eigenvalues of a Hermitian matrix in this borrowed session.
378 ///
379 /// # Examples
380 /// ```rust
381 /// use tenferro_ad::{EagerRuntime, Tensor};
382 /// use tenferro_linalg::EagerSessionLinalgExt;
383 /// let ctx = EagerRuntime::new()?;
384 /// let values = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
385 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![4.0_f64])?)?;
386 /// s.eigvalsh(&a)
387 /// })?;
388 /// assert_eq!(values.value()?.as_slice::<f64>()?, &[4.0]);
389 /// # Ok::<(), tenferro_ad::Error>(())
390 /// ```
391 /// # Errors
392 /// Returns typed validation, extension, backend, or unsupported-executor errors.
393 fn eigvalsh(&mut self, input: &EagerTensor) -> Result<EagerTensor>;
394
395 /// Compute general eigenvalues in this borrowed session.
396 ///
397 /// # Examples
398 /// ```rust
399 /// use tenferro_ad::{EagerRuntime, Tensor};
400 /// use tenferro_linalg::EagerSessionLinalgExt;
401 /// let ctx = EagerRuntime::new()?;
402 /// let values = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
403 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![4.0_f64])?)?;
404 /// s.eigvals(&a)
405 /// })?;
406 /// assert_eq!(values.value()?.as_slice::<num_complex::Complex64>()?, &[num_complex::Complex64::new(4.0, 0.0)]);
407 /// # Ok::<(), tenferro_ad::Error>(())
408 /// ```
409 /// # Errors
410 /// Returns typed validation, extension, backend, or unsupported-executor errors.
411 fn eigvals(&mut self, input: &EagerTensor) -> Result<EagerTensor>;
412
413 /// Compute eigenvalues and vectors of a Hermitian matrix in this session.
414 ///
415 /// # Examples
416 /// ```rust
417 /// use tenferro_ad::{EagerRuntime, Tensor};
418 /// use tenferro_linalg::EagerSessionLinalgExt;
419 /// let ctx = EagerRuntime::new()?;
420 /// let (values, vectors) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
421 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![4.0_f64])?)?;
422 /// s.eigh(&a)
423 /// })?;
424 /// assert_eq!(values.value()?.as_slice::<f64>()?, &[4.0]);
425 /// assert_eq!(vectors.shape(), &[1, 1]);
426 /// # Ok::<(), tenferro_ad::Error>(())
427 /// ```
428 /// # Errors
429 /// Returns typed validation, unsupported-dtype, extension, or backend errors.
430 fn eigh(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor)>;
431
432 /// Compute Hermitian eigendecomposition with explicit gauge and tolerance.
433 ///
434 /// # Examples
435 /// ```rust
436 /// use tenferro_ad::{EagerRuntime, Tensor};
437 /// use tenferro_linalg::{EagerSessionLinalgExt, EighOptions};
438 /// let ctx = EagerRuntime::new()?;
439 /// let (values, _vectors) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
440 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![4.0_f64])?)?;
441 /// s.eigh_with_options(&a, EighOptions::default())
442 /// })?;
443 /// assert_eq!(values.value()?.as_slice::<f64>()?, &[4.0]);
444 /// # Ok::<(), tenferro_ad::Error>(())
445 /// ```
446 /// # Errors
447 /// Returns `Error::Validation` with `ValidationError::InvalidArgument` for an
448 /// invalid tolerance or with `ValidationError::ShapeMismatch` for a
449 /// non-square input, `Error::Extension` carrying the linalg failure
450 /// (for example `Error::NonConvergence` or an unsupported dtype), or
451 /// `Error::TensorRuntime` for a backend failure.
452 fn eigh_with_options(
453 &mut self,
454 input: &EagerTensor,
455 options: EighOptions,
456 ) -> Result<(EagerTensor, EagerTensor)>;
457
458 /// Compute general eigenvalues and eigenvectors inside this session.
459 /// Eigenvectors come back with unit 2-norm columns, whichever provider computed them;
460 /// a column the provider left non-finite is reported as a numerical failure instead
461 /// of being returned as `NaN` (#2148).
462 ///
463 /// # Examples
464 /// ```rust
465 /// use tenferro_ad::{EagerRuntime, Tensor};
466 /// use tenferro_linalg::EagerSessionLinalgExt;
467 /// let ctx = EagerRuntime::new()?;
468 /// let (values, vectors) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
469 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![4.0_f64])?)?;
470 /// s.eig(&a)
471 /// })?;
472 /// assert_eq!(values.value()?.as_slice::<num_complex::Complex64>()?, &[num_complex::Complex64::new(4.0, 0.0)]);
473 /// assert_eq!(vectors.shape(), &[1, 1]);
474 /// # Ok::<(), tenferro_ad::Error>(())
475 /// ```
476 /// # Errors
477 /// Returns `Error::Validation` with `ValidationError::RankMismatch` or
478 /// `ValidationError::ShapeMismatch` for a non-square (batched) matrix,
479 /// `Error::Extension` carrying the linalg failure
480 /// (for example `Error::NonConvergence` or an unsupported dtype), or
481 /// `Error::TensorRuntime` for a backend failure.
482 fn eig(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor)>;
483
484 /// Compute the Moore-Penrose pseudoinverse using the default tolerance.
485 ///
486 /// The cutoff is applied to an accuracy-preserving default SVD
487 /// (`SvdDriver::Auto`), so the retained rank matches the CPU reference.
488 ///
489 /// # Examples
490 /// ```rust
491 /// use tenferro_ad::{EagerRuntime, Tensor};
492 /// use tenferro_linalg::EagerSessionLinalgExt;
493 /// let ctx = EagerRuntime::new()?;
494 /// let inverse = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
495 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
496 /// s.pinv(&a)
497 /// })?;
498 /// assert_eq!(inverse.value()?.as_slice::<f64>()?, &[0.5]);
499 /// # Ok::<(), tenferro_ad::Error>(())
500 /// ```
501 /// # Errors
502 /// Returns `Error::Validation` with `ValidationError::RankMismatch` when the
503 /// input is not a (batched) matrix, `Error::Extension` carrying the linalg failure
504 /// (for example `Error::NonConvergence` or an unsupported dtype) from the
505 /// underlying SVD, or `Error::TensorRuntime` for a backend failure.
506 fn pinv(&mut self, input: &EagerTensor) -> Result<EagerTensor>;
507
508 /// Compute the pseudoinverse with an explicit relative tolerance.
509 ///
510 /// Like [`Self::pinv`], the threshold is applied to an accuracy-preserving
511 /// default SVD (`SvdDriver::Auto`).
512 ///
513 /// # Examples
514 /// ```rust
515 /// use tenferro_ad::{EagerRuntime, Tensor};
516 /// use tenferro_linalg::EagerSessionLinalgExt;
517 /// let ctx = EagerRuntime::new()?;
518 /// let inverse = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
519 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
520 /// s.pinv_with_rtol(&a, 1.0e-12)
521 /// })?;
522 /// assert_eq!(inverse.value()?.as_slice::<f64>()?, &[0.5]);
523 /// # Ok::<(), tenferro_ad::Error>(())
524 /// ```
525 /// # Errors
526 /// Returns `Error::Validation` with `ValidationError::RankMismatch` when the
527 /// input is not a (batched) matrix, `ValidationError::InvalidArgument` for a
528 /// negative or non-finite tolerance, `Error::Extension` carrying the linalg failure
529 /// (for example `Error::NonConvergence` or an unsupported dtype) from
530 /// the underlying SVD, or `Error::TensorRuntime` for a backend failure.
531 fn pinv_with_rtol(&mut self, input: &EagerTensor, rtol: f64) -> Result<EagerTensor>;
532
533 /// Compute a vector, matrix, or tensor norm in this session.
534 /// An empty axis list is a no-op and clones the input without dispatch.
535 ///
536 /// # Examples
537 /// ```rust
538 /// use tenferro_ad::{EagerRuntime, Tensor};
539 /// use tenferro_linalg::EagerSessionLinalgExt;
540 /// let ctx = EagerRuntime::new()?;
541 /// let result = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
542 /// let a = s.constant_from(Tensor::from_vec_col_major([2], vec![3.0_f64, 4.0])?)?;
543 /// s.norm(&a, Some(2.0), Some(&[0]), false)
544 /// })?;
545 /// assert_eq!(result.value()?.as_slice::<f64>()?, &[5.0]);
546 /// # Ok::<(), tenferro_ad::Error>(())
547 /// ```
548 /// # Errors
549 /// Returns typed unsupported-dtype, invalid-axis/order, SVD, or backend errors.
550 fn norm(
551 &mut self,
552 input: &EagerTensor,
553 ord: Option<f64>,
554 dim: Option<&[usize]>,
555 keepdim: bool,
556 ) -> Result<EagerTensor>;
557
558 /// Compute complete-pivot LU factors `(P, L, U, Q, parity)` in this session.
559 /// Reconstruction uses `A = P^T * L * U * Q`; scalar `parity` is real
560 /// (`F32` for `F32`/`C32` inputs and `F64` for `F64`/`C64`).
561 ///
562 /// # Examples
563 /// ```rust
564 /// use tenferro_ad::{EagerRuntime, Tensor};
565 /// use tenferro_linalg::EagerSessionLinalgExt;
566 /// let ctx = EagerRuntime::new()?;
567 /// let (p, _l, _u, q, parity) = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
568 /// let a = s.constant_from(Tensor::from_vec_col_major([2, 2], vec![1.0_f64, 3.0, 2.0, 4.0])?)?;
569 /// s.full_piv_lu(&a)
570 /// })?;
571 /// assert_eq!(p.shape(), &[2, 2]);
572 /// assert_eq!(q.shape(), &[2, 2]);
573 /// assert_eq!(parity.shape(), &[] as &[usize]);
574 /// # Ok::<(), tenferro_ad::Error>(())
575 /// ```
576 /// # Errors
577 /// Returns typed rank/shape, unsupported-provider, numerical, or output-count errors.
578 fn full_piv_lu(
579 &mut self,
580 input: &EagerTensor,
581 ) -> Result<(
582 EagerTensor,
583 EagerTensor,
584 EagerTensor,
585 EagerTensor,
586 EagerTensor,
587 )>;
588
589 /// Solve a linear system using complete-pivot LU in this session.
590 ///
591 /// # Examples
592 /// ```rust
593 /// use tenferro_ad::{EagerRuntime, Tensor};
594 /// use tenferro_linalg::EagerSessionLinalgExt;
595 /// let ctx = EagerRuntime::new()?;
596 /// let x = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
597 /// let a = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![2.0_f64])?)?;
598 /// let b = s.constant_from(Tensor::from_vec_col_major([1, 1], vec![4.0_f64])?)?;
599 /// s.full_piv_lu_solve(&a, &b)
600 /// })?;
601 /// assert_eq!(x.value()?.as_slice::<f64>()?, &[2.0]);
602 /// # Ok::<(), tenferro_ad::Error>(())
603 /// ```
604 /// # Errors
605 /// Returns typed rank/shape, unsupported-provider, singularity, or backend errors.
606 fn full_piv_lu_solve(&mut self, matrix: &EagerTensor, rhs: &EagerTensor)
607 -> Result<EagerTensor>;
608
609 /// Compute column-pivoted rank-revealing QR in this session.
610 ///
611 /// # Examples
612 /// ```rust
613 /// use tenferro_ad::{EagerRuntime, Tensor};
614 /// use tenferro_linalg::{EagerSessionLinalgExt, RankRevealingQrOptions};
615 /// let ctx = EagerRuntime::new()?;
616 /// let result = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
617 /// let a = s.constant_from(Tensor::from_vec_col_major([2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?)?;
618 /// s.rank_revealing_qr(&a, RankRevealingQrOptions::default())
619 /// })?;
620 /// assert_eq!(result.column_permutation.shape(), &[2]);
621 /// assert_eq!(result.rank.value()?.as_slice::<i64>()?, &[2]);
622 /// # Ok::<(), tenferro_ad::Error>(())
623 /// ```
624 /// # Errors
625 /// Returns `Error::Validation` with `ValidationError::RankMismatch`,
626 /// `ValidationError::DTypeMismatch` or `ValidationError::InvalidArgument` for
627 /// an invalid rank, dtype or tolerance, `Error::Extension` carrying the linalg failure
628 /// (for example `Error::NonConvergence` or an unsupported dtype), or
629 /// `Error::TensorRuntime` for a backend failure.
630 fn rank_revealing_qr(
631 &mut self,
632 input: &EagerTensor,
633 options: RankRevealingQrOptions,
634 ) -> Result<RankRevealingQrResult<EagerTensor>>;
635
636 /// Initialize compact Householder QR state in this borrowed session.
637 ///
638 /// # Examples
639 /// ```rust
640 /// use tenferro_ad::{EagerRuntime, Tensor};
641 /// use tenferro_linalg::EagerSessionLinalgExt;
642 /// let ctx = EagerRuntime::new()?;
643 /// let state = ctx.with_eager_session(&mut tenferro_cpu::CpuEngine::serial(), |s| {
644 /// let a = s.constant_from(Tensor::from_vec_col_major([2, 1], vec![1.0_f64, 2.0])?)?;
645 /// s.householder_qr(&a)
646 /// })?;
647 /// assert!(format!("{state:?}").starts_with("HouseholderQr"));
648 /// # Ok::<(), tenferro_ad::Error>(())
649 /// ```
650 /// # Errors
651 /// Returns typed invalid metadata, unsupported executor, or backend errors.
652 fn householder_qr(&mut self, input: &EagerTensor) -> Result<crate::HouseholderQr<EagerTensor>>;
653}
654
655impl EagerSessionLinalgExt for EagerSession<'_> {
656 fn svd(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
657 self.svd_with_options(input, SvdOptions::default())
658 }
659
660 fn svd_with_options(
661 &mut self,
662 input: &EagerTensor,
663 options: SvdOptions,
664 ) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
665 validate_derivative_eps("svd_with_options", options.derivative_eps)?;
666 three_outputs(
667 apply_linalg_eager_in_session(
668 self,
669 LinalgOp::Svd {
670 derivative_eps: options.derivative_eps,
671 gauge: options.gauge,
672 driver: options.driver,
673 },
674 &[input],
675 )?,
676 "svd",
677 )
678 }
679
680 fn svd_full(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
681 three_outputs(
682 apply_linalg_eager_in_session(self, LinalgOp::SvdFull, &[input])?,
683 "svd_full",
684 )
685 }
686
687 fn qr(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
688 self.qr_with_options(input, QrOptions::default())
689 }
690
691 fn qr_with_options(
692 &mut self,
693 input: &EagerTensor,
694 options: QrOptions,
695 ) -> Result<(EagerTensor, EagerTensor)> {
696 two_outputs(
697 apply_linalg_eager_in_session(
698 self,
699 LinalgOp::Qr {
700 gauge: options.gauge,
701 },
702 &[input],
703 )?,
704 "qr",
705 )
706 }
707
708 fn cholesky(&mut self, input: &EagerTensor) -> Result<EagerTensor> {
709 one_output(
710 apply_linalg_eager_in_session(self, LinalgOp::Cholesky, &[input])?,
711 "cholesky",
712 )
713 }
714
715 fn triangular_solve(
716 &mut self,
717 matrix: &EagerTensor,
718 rhs: &EagerTensor,
719 left_side: bool,
720 lower: bool,
721 transpose_a: bool,
722 unit_diagonal: bool,
723 ) -> Result<EagerTensor> {
724 one_output(
725 apply_linalg_eager_in_session(
726 self,
727 LinalgOp::TriangularSolve {
728 left_side,
729 lower,
730 transpose_a,
731 unit_diagonal,
732 },
733 &[matrix, rhs],
734 )?,
735 "triangular_solve",
736 )
737 }
738
739 fn solve(&mut self, matrix: &EagerTensor, rhs: &EagerTensor) -> Result<EagerTensor> {
740 if !matrix.tracks_grad() && !rhs.tracks_grad() {
741 return one_output(
742 apply_linalg_eager_in_session(self, LinalgOp::Solve, &[matrix, rhs])?,
743 "solve",
744 );
745 }
746 validate_tracked_solve_inputs(matrix, rhs)?;
747 factor_solve_output(apply_linalg_eager_in_session(
748 self,
749 LinalgOp::LuFactorSolve,
750 &[matrix, rhs],
751 )?)
752 }
753
754 fn lstsq(&mut self, matrix: &EagerTensor, rhs: &EagerTensor) -> Result<EagerTensor> {
755 eager_composites::lstsq(self, matrix, rhs)
756 }
757
758 fn lu(
759 &mut self,
760 input: &EagerTensor,
761 ) -> Result<(EagerTensor, EagerTensor, EagerTensor, EagerTensor)> {
762 let mut outputs = apply_linalg_eager_in_session(self, LinalgOp::Lu, &[input])?.into_iter();
763 match (
764 outputs.next(),
765 outputs.next(),
766 outputs.next(),
767 outputs.next(),
768 outputs.next(),
769 ) {
770 (Some(p), Some(l), Some(u), Some(parity), None) => Ok((p, l, u, parity)),
771 _ => Err(Error::Internal(
772 "lu eager op returned an unexpected number of outputs".into(),
773 )),
774 }
775 }
776
777 fn slogdet(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
778 eager_composites::slogdet(self, input)
779 }
780
781 fn det(&mut self, input: &EagerTensor) -> Result<EagerTensor> {
782 eager_composites::det(self, input)
783 }
784
785 fn inv(&mut self, input: &EagerTensor) -> Result<EagerTensor> {
786 eager_composites::inv(self, input)
787 }
788
789 fn eigvalsh(&mut self, input: &EagerTensor) -> Result<EagerTensor> {
790 one_output(
791 apply_linalg_eager_in_session(
792 self,
793 LinalgOp::EighVals {
794 derivative_eps: crate::extension::DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
795 driver: EighOptions::default().driver,
796 },
797 &[input],
798 )?,
799 "eigvalsh",
800 )
801 }
802
803 fn eigvals(&mut self, input: &EagerTensor) -> Result<EagerTensor> {
804 one_output(
805 apply_linalg_eager_in_session(
806 self,
807 LinalgOp::EigVals {
808 input_dtype: input.dtype(),
809 },
810 &[input],
811 )?,
812 "eigvals",
813 )
814 }
815
816 fn eigh(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
817 self.eigh_with_options(input, EighOptions::default())
818 }
819
820 fn eigh_with_options(
821 &mut self,
822 input: &EagerTensor,
823 options: EighOptions,
824 ) -> Result<(EagerTensor, EagerTensor)> {
825 validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
826 two_outputs(
827 apply_linalg_eager_in_session(
828 self,
829 LinalgOp::Eigh {
830 derivative_eps: options.derivative_eps,
831 gauge: options.gauge,
832 driver: options.driver,
833 },
834 &[input],
835 )?,
836 "eigh",
837 )
838 }
839
840 fn eig(&mut self, input: &EagerTensor) -> Result<(EagerTensor, EagerTensor)> {
841 two_outputs(
842 apply_linalg_eager_in_session(
843 self,
844 LinalgOp::Eig {
845 input_dtype: input.dtype(),
846 },
847 &[input],
848 )?,
849 "eig",
850 )
851 }
852
853 fn pinv(&mut self, input: &EagerTensor) -> Result<EagerTensor> {
854 eager_composites::pinv(self, input)
855 }
856
857 fn pinv_with_rtol(&mut self, input: &EagerTensor, rtol: f64) -> Result<EagerTensor> {
858 eager_composites::pinv_with_rtol(self, input, rtol)
859 }
860
861 fn norm(
862 &mut self,
863 input: &EagerTensor,
864 ord: Option<f64>,
865 dim: Option<&[usize]>,
866 keepdim: bool,
867 ) -> Result<EagerTensor> {
868 eager_composites::norm(self, input, ord, dim, keepdim)
869 }
870
871 fn full_piv_lu(
872 &mut self,
873 input: &EagerTensor,
874 ) -> Result<(
875 EagerTensor,
876 EagerTensor,
877 EagerTensor,
878 EagerTensor,
879 EagerTensor,
880 )> {
881 let mut outputs =
882 apply_linalg_eager_in_session(self, LinalgOp::FullPivLu, &[input])?.into_iter();
883 match (
884 outputs.next(),
885 outputs.next(),
886 outputs.next(),
887 outputs.next(),
888 outputs.next(),
889 outputs.next(),
890 ) {
891 (Some(p), Some(l), Some(u), Some(q), Some(parity), None) => Ok((p, l, u, q, parity)),
892 _ => Err(Error::Internal(
893 "full_piv_lu eager op returned an unexpected number of outputs".into(),
894 )),
895 }
896 }
897
898 fn full_piv_lu_solve(
899 &mut self,
900 matrix: &EagerTensor,
901 rhs: &EagerTensor,
902 ) -> Result<EagerTensor> {
903 one_output(
904 apply_linalg_eager_in_session(
905 self,
906 LinalgOp::FullPivLuSolve { transpose_a: false },
907 &[matrix, rhs],
908 )?,
909 "full_piv_lu_solve",
910 )
911 }
912
913 fn rank_revealing_qr(
914 &mut self,
915 input: &EagerTensor,
916 options: RankRevealingQrOptions,
917 ) -> Result<RankRevealingQrResult<EagerTensor>> {
918 validate_rank_revealing_qr_options("rank_revealing_qr", options)?;
919 let mut outputs = apply_linalg_eager_in_session(
920 self,
921 LinalgOp::RankRevealingQr {
922 gauge: options.gauge,
923 rtol: options.rtol,
924 atol: options.atol,
925 },
926 &[input],
927 )?
928 .into_iter();
929 match (
930 outputs.next(),
931 outputs.next(),
932 outputs.next(),
933 outputs.next(),
934 outputs.next(),
935 ) {
936 (Some(q), Some(r), Some(column_permutation), Some(rank), None) => {
937 Ok(RankRevealingQrResult {
938 q,
939 r,
940 column_permutation,
941 rank,
942 })
943 }
944 _ => Err(Error::Internal(
945 "rank_revealing_qr eager op returned an unexpected number of outputs".into(),
946 )),
947 }
948 }
949
950 fn householder_qr(&mut self, input: &EagerTensor) -> Result<crate::HouseholderQr<EagerTensor>> {
951 crate::householder::eager_state(apply_linalg_eager_in_session(
952 self,
953 LinalgOp::HouseholderQrFactor,
954 &[input],
955 )?)
956 }
957}
958
959pub(crate) fn apply_linalg_eager_in_session(
960 session: &mut EagerSession<'_>,
961 op: LinalgOp,
962 inputs: &[&EagerTensor],
963) -> Result<Vec<EagerTensor>> {
964 let op = Arc::new(LinalgExtensionOp::new(op));
965 apply_eager_with_targeted_extension_in_session(session, op, inputs, eager_extension_module)
966}
967
968pub(crate) fn apply_linalg_eager(
969 engine: &mut dyn ExecutionEngine,
970 op: LinalgOp,
971 inputs: &[&EagerTensor],
972) -> Result<Vec<EagerTensor>> {
973 let op = Arc::new(LinalgExtensionOp::new(op));
974 apply_eager_with_targeted_extension_session(engine, op, inputs, eager_extension_module)
975}
976
977fn eager_extension_module(target: EagerExtensionTarget) -> Result<Arc<dyn ExtensionModule>> {
978 let EagerExtensionTarget {
979 engine_id,
980 backend_kind,
981 } = target;
982 match backend_kind {
983 EagerExtensionBackendKind::Cpu => {
984 extension_module::<CpuEngine>(engine_id).map_err(eager_runtime_config_error)
985 }
986 #[cfg(feature = "cuda")]
987 EagerExtensionBackendKind::Cuda => {
988 extension_module::<CudaEngine>(engine_id).map_err(eager_runtime_config_error)
989 }
990 }
991}
992
993fn eager_runtime_config_error(source: tenferro_runtime::RuntimeConfigError) -> Error {
994 Error::runtime_state_source(
995 "tenferro_linalg::eager_extension_module",
996 ErrorPhase::Execution,
997 source,
998 )
999}
1000
1001/// Solve a linear system for eager tensors.
1002///
1003/// # Examples
1004///
1005/// ```rust
1006/// use tenferro_ad::{EagerRuntime, EagerTensor, Tensor};
1007/// use tenferro_linalg::EagerTensorLinalgExt;
1008///
1009/// let ctx = EagerRuntime::new()?;
1010/// let a = EagerTensor::from_tensor_in(
1011/// Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0]).unwrap(),
1012/// ctx.clone(),
1013/// ).unwrap();
1014/// let b = EagerTensor::from_tensor_in(
1015/// Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0]).unwrap(),
1016/// ctx,
1017/// ).unwrap();
1018/// let x = a.solve(&mut tenferro_cpu::CpuEngine::serial(), &b)?;
1019/// assert_eq!(x.shape(), &[2, 1]);
1020/// # Ok::<(), tenferro_ad::Error>(())
1021/// ```
1022///
1023/// The CUDA provider does not inspect the cuSOLVER `getrf` status flag on
1024/// the forward solve path, so it performs no per-call non-singularity check
1025/// and issues no host round trip. An exactly singular input is left to the
1026/// caller's verification, as in `torch.linalg.solve`. CPU providers may
1027/// still report their own exactly singular factor as `Error::Singular`.
1028///
1029/// # Errors
1030///
1031/// Returns `Error::Validation` for incompatible matrix, batch, or dtype
1032/// metadata, `Error::Extension` for an unsupported dtype or singular system,
1033/// and `Error::RuntimeState` when the backend is unavailable.
1034pub fn solve(
1035 engine: &mut dyn ExecutionEngine,
1036 a: &EagerTensor,
1037 b: &EagerTensor,
1038) -> Result<EagerTensor> {
1039 if !a.tracks_grad() && !b.tracks_grad() {
1040 return one_output(
1041 apply_linalg_eager(engine, LinalgOp::Solve, &[a, b])?,
1042 "solve",
1043 );
1044 }
1045 validate_tracked_solve_inputs(a, b)?;
1046 factor_solve_output(apply_linalg_eager(
1047 engine,
1048 LinalgOp::LuFactorSolve,
1049 &[a, b],
1050 )?)
1051}
1052
1053fn validate_tracked_solve_inputs(a: &EagerTensor, b: &EagerTensor) -> Result<()> {
1054 if !a.same_context(b) {
1055 return Err(Error::ContextMismatch {
1056 lhs: a.ctx_id(),
1057 rhs: b.ctx_id(),
1058 });
1059 }
1060 crate::validation::validate_solve_inputs(a.dtype(), a.shape(), b.dtype(), b.shape())
1061}
1062
1063// One fused factor+solve retains LU/pivots and X for backward, while the
1064// explicit A operand preserves higher-order semantics (PyTorch's
1065// _linalg_solve_ex / FunctionsManual.cpp::linalg_solve_backward).
1066fn factor_solve_output(outputs: Vec<EagerTensor>) -> Result<EagerTensor> {
1067 let mut outputs = outputs.into_iter();
1068 match (
1069 outputs.next(),
1070 outputs.next(),
1071 outputs.next(),
1072 outputs.next(),
1073 ) {
1074 (Some(x), Some(_packed_lu), Some(_pivots), None) => Ok(x),
1075 _ => Err(Error::Internal(
1076 "lu_factor_solve eager op returned an unexpected number of outputs".into(),
1077 )),
1078 }
1079}
1080
1081fn three_outputs(
1082 outputs: Vec<EagerTensor>,
1083 name: &str,
1084) -> Result<(EagerTensor, EagerTensor, EagerTensor)> {
1085 let mut outputs = outputs.into_iter();
1086 match (
1087 outputs.next(),
1088 outputs.next(),
1089 outputs.next(),
1090 outputs.next(),
1091 ) {
1092 (Some(first), Some(second), Some(third), None) => Ok((first, second, third)),
1093 _ => Err(Error::Internal(format!(
1094 "{name} eager op returned an unexpected number of outputs"
1095 ))),
1096 }
1097}
1098
1099pub(crate) fn one_output(outputs: Vec<EagerTensor>, name: &str) -> Result<EagerTensor> {
1100 let mut outputs = outputs.into_iter();
1101 match (outputs.next(), outputs.next()) {
1102 (Some(output), None) => Ok(output),
1103 _ => Err(Error::Internal(format!(
1104 "{name} eager op returned an unexpected number of outputs"
1105 ))),
1106 }
1107}
1108
1109fn two_outputs(outputs: Vec<EagerTensor>, name: &str) -> Result<(EagerTensor, EagerTensor)> {
1110 let mut outputs = outputs.into_iter();
1111 match (outputs.next(), outputs.next(), outputs.next()) {
1112 (Some(lhs), Some(rhs), None) => Ok((lhs, rhs)),
1113 _ => Err(Error::Internal(format!(
1114 "{name} eager op returned an unexpected number of outputs"
1115 ))),
1116 }
1117}