Skip to main content

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}