Skip to main content

tenferro_linalg/
backend.rs

1use std::ops::Range;
2
3use tenferro_tensor::{Engine, Tensor, TensorRead, TensorWrite};
4
5/// Compact provider-neutral Householder QR payload used by backend hooks.
6#[doc(hidden)]
7#[derive(Debug)]
8pub struct CompactQrResult {
9    pub(crate) packed: Tensor,
10    pub(crate) coeff: Tensor,
11}
12
13pub(crate) use crate::error::unsupported_dtype;
14use crate::extension::{
15    apply_eigh_gauge, apply_qr_gauge, apply_svd_gauge, validate_derivative_eps, EighDriver,
16    EighOptions, QrOptions, SvdDriver, SvdOptions,
17};
18use crate::RankRevealingQrOptions;
19
20/// Backend surface required by the linalg extension runtime.
21///
22/// # Examples
23///
24/// ```rust
25/// use tenferro_cpu::{with_cpu_engine, CpuEngine};
26/// use tenferro_linalg::backend::LinalgBackend;
27///
28/// fn assert_linalg_backend<B: LinalgBackend>() {}
29///
30/// assert_linalg_backend::<CpuEngine>();
31/// let mut host = tenferro_cpu::CpuEngine::serial();
32/// {
33///     with_cpu_engine(&mut host, |_backend| ())
34///         .expect("CpuEngine must expose a CpuEngine");
35/// };
36/// # Ok::<(), Box<dyn std::error::Error>>(())
37/// ```
38pub trait LinalgBackend: Engine {
39    /// Compute a Cholesky factorization.
40    ///
41    /// # Errors
42    ///
43    /// Returns `Error::Validation` for non-matrix, non-square, or unsupported
44    /// input dtypes; `Error::Extension` with `ErrorKind::NumericalFailure`
45    /// when the matrix is not positive definite; or a typed backend source
46    /// when the provider cannot execute the factorization.
47    fn cholesky(&mut self, input: &Tensor) -> tenferro_tensor::Result<Tensor>;
48
49    /// Solve a triangular linear system with explicit side, triangle,
50    /// transpose, and unit-diagonal flags.
51    ///
52    /// # Errors
53    ///
54    /// Returns `Error::Validation` for incompatible matrix/rhs shapes, rank,
55    /// or dtype; `Error::Extension` with `ErrorKind::NumericalFailure` for a
56    /// singular or zero-diagonal system; or a typed backend source for a
57    /// provider failure.
58    fn triangular_solve(
59        &mut self,
60        a: &Tensor,
61        b: &Tensor,
62        left_side: bool,
63        lower: bool,
64        transpose_a: bool,
65        unit_diagonal: bool,
66    ) -> tenferro_tensor::Result<Tensor>;
67
68    // INVARIANT: the six flags are the public triangular-solve contract and mirror the owned hook.
69    #[allow(clippy::too_many_arguments)]
70    /// Solve a triangular linear system from tensor read targets.
71    ///
72    /// Backends may canonicalize the inputs inside the same placement family,
73    /// but must not silently transfer between CPU and GPU memory.
74    ///
75    /// # Examples
76    ///
77    /// ```rust
78    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
79    /// use tenferro_linalg::LinalgBackend;
80    /// use tenferro_tensor::{Tensor, TensorRead};
81    ///
82    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 1.0, 3.0])?;
83    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0])?;
84    /// let mut host = tenferro_cpu::CpuEngine::serial();
85    /// let x = {
86    ///     with_cpu_engine(&mut host, |backend| {
87    ///         backend.triangular_solve_read(
88    ///             TensorRead::from_tensor(&a),
89    ///             TensorRead::from_tensor(&b),
90    ///             true,
91    ///             false,
92    ///             false,
93    ///             false,
94    ///         )
95    ///     })
96    ///     .expect("CpuEngine must expose a CpuEngine")
97    /// }?;
98    /// let x = x.into_typed::<f64>().expect("F64 inputs return F64 output");
99    /// assert_eq!(x.host_data()?, &[0.5, 3.0]);
100    /// # Ok::<(), tenferro_tensor::Error>(())
101    /// ```
102    ///
103    /// # Errors
104    ///
105    /// The default implementation returns `Error::Unsupported` because the
106    /// backend does not accept tensor read targets. Implementations may return
107    /// `Error::Validation` for incompatible shapes or dtypes,
108    /// `Error::RuntimeState` for invalid placement, `Error::Extension` for a
109    /// singular system, or a typed backend-source error.
110    fn triangular_solve_read(
111        &mut self,
112        _a: TensorRead<'_>,
113        _b: TensorRead<'_>,
114        _left_side: bool,
115        _lower: bool,
116        _transpose_a: bool,
117        _unit_diagonal: bool,
118    ) -> tenferro_tensor::Result<Tensor> {
119        Err(tenferro_tensor::Error::unsupported(
120            "triangular_solve",
121            "backend does not accept tensor reads at this execution boundary",
122        ))
123    }
124
125    /// Compute public LU outputs `(P, L, U, parity)`.
126    ///
127    /// # Errors
128    ///
129    /// Returns `Error::Validation` when the input is not a supported matrix or
130    /// dtype, and `Error::Extension` or a typed backend source when LU
131    /// execution or pivot storage fails.
132    fn lu(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
133
134    #[doc(hidden)]
135    fn lu_factor(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
136        Err(tenferro_tensor::Error::unsupported(
137            "lu_factor",
138            format!(
139                "backend {} does not implement internal packed LU factorization",
140                std::any::type_name::<Self>()
141            ),
142        ))
143    }
144
145    /// Compute complete-pivot LU outputs `(P, L, U, Q, parity)`.
146    ///
147    /// The reconstruction convention is `A = P^T * L * U * Q`, equivalently
148    /// `P * A * Q^T = L * U`. `parity` is a scalar real tensor containing
149    /// `+1` or `-1`: `F32` for `F32`/`C32` inputs and `F64` for `F64`/`C64`
150    /// inputs.
151    ///
152    /// # Errors
153    ///
154    /// Returns `Error::Validation` for an invalid rank, square-shape
155    /// requirement, or dtype, and `Error::Extension` or a typed backend source
156    /// when complete-pivot factorization cannot be executed.
157    fn full_piv_lu(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
158
159    /// Solve a linear system through the complete-pivot LU path.
160    ///
161    /// With `transpose_a = false`, this solves `A * x = b`. With
162    /// `transpose_a = true`, this solves `A^T * x = b`.
163    ///
164    /// # Errors
165    ///
166    /// Returns `Error::Validation` for incompatible coefficient/rhs shapes or
167    /// dtypes, `Error::Extension` with `ErrorKind::NumericalFailure` for a
168    /// singular system, or a typed backend source for provider failure.
169    fn full_piv_lu_solve(
170        &mut self,
171        a: &Tensor,
172        b: &Tensor,
173        transpose_a: bool,
174    ) -> tenferro_tensor::Result<Tensor>;
175
176    /// Compute public SVD outputs `(U, S, Vt)`.
177    ///
178    /// # Errors
179    ///
180    /// Returns `Error::Validation` for an unsupported rank or dtype and a
181    /// typed `Error::Extension` or backend source when the solver fails.
182    fn svd(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
183
184    /// Compute public SVD outputs `(U, S, Vt)` with explicit options.
185    ///
186    /// `derivative_eps` is validated for API consistency, but concrete backend
187    /// execution does not perform AD. `gauge` controls optional singular-vector
188    /// post-processing. `driver` selects the cuSOLVER routine on the CUDA
189    /// backend; the default implementation ignores it because CPU providers
190    /// have a single SVD kernel.
191    ///
192    /// # Examples
193    ///
194    /// ```rust
195    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
196    /// use tenferro_linalg::{LinalgBackend, SvdGauge, SvdOptions};
197    /// use tenferro_tensor::Tensor;
198    ///
199    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
200    /// let mut host = tenferro_cpu::CpuEngine::serial();
201    /// let outputs = {
202    ///     with_cpu_engine(&mut host, |backend| {
203    ///         backend.svd_with_options(
204    ///             &input,
205    ///             SvdOptions::default().gauge(SvdGauge::CanonicalPivot),
206    ///         )
207    ///     })
208    ///     .expect("CpuEngine must expose a CpuEngine")
209    /// }?;
210    /// assert_eq!(outputs[1].shape(), &[2]);
211    /// # Ok::<(), tenferro_tensor::Error>(())
212    /// ```
213    ///
214    /// # Errors
215    ///
216    /// Returns [`tenferro_tensor::Error::Validation`] containing
217    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
218    /// `derivative_eps` is non-finite or non-positive, or when canonical gauge
219    /// output metadata is malformed. It can return
220    /// [`tenferro_tensor::Error::Validation`] with
221    /// [`tenferro_tensor::ValidationError::RankMismatch`],
222    /// [`tenferro_tensor::ValidationError::ShapeMismatch`], or
223    /// [`tenferro_tensor::ValidationError::DTypeMismatch`] for the input
224    /// or generated outputs, [`tenferro_tensor::Error::Extension`] with the
225    /// typed `tenferro_linalg::Error::UnsupportedDType` or
226    /// `NonConvergence` source, [`tenferro_tensor::Error::BackendSource`] for
227    /// provider calls, and [`tenferro_tensor::Error::RuntimeState`] for
228    /// placement failures. A CPU provider that was not compiled is reported
229    /// as [`tenferro_tensor::ValidationError::InvalidArgument`] on the
230    /// provider configuration.
231    fn svd_with_options(
232        &mut self,
233        input: &Tensor,
234        options: SvdOptions,
235    ) -> tenferro_tensor::Result<Vec<Tensor>> {
236        validate_derivative_eps("svd_with_options", options.derivative_eps)?;
237        let mut outputs = self.svd(input)?;
238        apply_svd_gauge(options.gauge, &mut outputs)?;
239        Ok(outputs)
240    }
241
242    /// Compute public full-matrices SVD outputs `(U, S, Vt)` with `U` shaped
243    /// `m x m` and `Vt` shaped `n x n`, so the trailing `Vt` rows span the
244    /// input's right nullspace.
245    ///
246    /// # Errors
247    ///
248    /// The default implementation returns `Error::Unsupported`: a backend that
249    /// does not implement the full variant reports it explicitly rather than
250    /// silently falling back to the thin decomposition. Implementing backends
251    /// may additionally return `Error::Validation` for an unsupported rank or
252    /// dtype and a typed backend source when the solver fails.
253    fn svd_full(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
254        Err(tenferro_tensor::Error::unsupported(
255            "svd_full",
256            format!(
257                "backend {} does not implement full-matrices SVD",
258                std::any::type_name::<Self>()
259            ),
260        ))
261    }
262
263    /// Compute full-matrices SVD outputs `(U, S, Vt)` from a tensor read target.
264    ///
265    /// This is the borrowed-input counterpart of [`LinalgBackend::svd_full`] and
266    /// keeps the same output contract: `U` is `m x m`, `S` has `min(m, n)`
267    /// entries, and `Vt` is `n x n`. Backends may canonicalize the input inside
268    /// the same placement family, but must not silently transfer between CPU
269    /// and GPU memory, and must not substitute the thin decomposition.
270    ///
271    /// # Examples
272    ///
273    /// ```rust
274    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
275    /// use tenferro_linalg::LinalgBackend;
276    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
277    ///
278    /// let input = TypedTensor::<f64>::from_vec_col_major(vec![1, 2], vec![1.0, 1.0])?;
279    /// let mut host = tenferro_cpu::CpuEngine::serial();
280    /// let outputs = {
281    ///     with_cpu_engine(&mut host, |backend| {
282    ///         backend.svd_full_read(TensorRead::from_view(TensorView::F64(input.as_view())))
283    ///     })
284    ///     .expect("CpuEngine must expose a CpuEngine")
285    /// }?;
286    /// assert_eq!(outputs[0].shape(), &[1, 1]);
287    /// assert_eq!(outputs[1].shape(), &[1]);
288    /// assert_eq!(outputs[2].shape(), &[2, 2]);
289    /// # Ok::<(), tenferro_tensor::Error>(())
290    /// ```
291    ///
292    /// # Errors
293    ///
294    /// The default implementation returns `Error::Unsupported` because the
295    /// backend does not accept tensor read targets for the full variant; an
296    /// implementation may instead return `Error::Validation` for an unsupported
297    /// rank or dtype, `Error::Unsupported` when the selected provider has no
298    /// full-matrices kernel, and a typed backend source when the solver fails.
299    fn svd_full_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
300        Err(tenferro_tensor::Error::unsupported(
301            "svd_full",
302            "backend does not accept tensor reads for full-matrices SVD at this execution boundary",
303        ))
304    }
305
306    #[doc(hidden)]
307    fn svd_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
308        Err(tenferro_tensor::Error::unsupported(
309            "svd_values",
310            format!(
311                "backend {} does not implement internal singular-values-only decomposition",
312                std::any::type_name::<Self>()
313            ),
314        ))
315    }
316
317    /// Singular values only with an explicit CUDA driver. The default
318    /// implementation ignores the driver, matching [`LinalgBackend::svd_with_options`].
319    #[doc(hidden)]
320    fn svd_values_with_driver(
321        &mut self,
322        input: &Tensor,
323        _driver: SvdDriver,
324    ) -> tenferro_tensor::Result<Tensor> {
325        self.svd_values(input)
326    }
327
328    /// Compute a singular value decomposition from a tensor read target.
329    ///
330    /// Backends may canonicalize the input inside the same placement family, but
331    /// must not silently transfer between CPU and GPU memory.
332    ///
333    /// # Examples
334    ///
335    /// ```rust
336    /// use tenferro_linalg::LinalgBackend;
337    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
338    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
339    ///
340    /// let input = TypedTensor::<f64>::from_vec_col_major(
341    ///     vec![2, 2],
342    ///     vec![1.0, 0.0, 0.0, 2.0],
343    /// )?;
344    /// let mut host = tenferro_cpu::CpuEngine::serial();
345    /// let outputs = {
346    ///     with_cpu_engine(&mut host, |backend| {
347    ///         backend.svd_read(TensorRead::from_view(TensorView::F64(input.as_view())))
348    ///     })
349    ///     .expect("CpuEngine must expose a CpuEngine")
350    /// }?;
351    /// assert_eq!(outputs[1].shape(), &[2]);
352    /// # Ok::<(), tenferro_tensor::Error>(())
353    /// ```
354    ///
355    /// # Errors
356    ///
357    /// The default implementation returns `Error::Unsupported` because the
358    /// backend does not accept tensor read targets; an implementation may instead
359    /// return validation or typed backend-source errors after canonicalizing
360    /// the view.
361    fn svd_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
362        Err(tenferro_tensor::Error::unsupported(
363            "svd",
364            "backend does not accept tensor reads at this execution boundary",
365        ))
366    }
367
368    /// Compute public SVD outputs `(U, S, Vt)` from a tensor read target with
369    /// explicit options.
370    ///
371    /// This is the borrowed-input counterpart of
372    /// [`LinalgBackend::svd_with_options`]. The default implementation runs
373    /// [`LinalgBackend::svd_read`] and applies the gauge on the host; it
374    /// ignores `driver` because CPU providers have a single SVD kernel. The
375    /// CUDA backend overrides it so the driver reaches cuSOLVER.
376    ///
377    /// # Examples
378    ///
379    /// ```rust
380    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
381    /// use tenferro_linalg::{LinalgBackend, SvdDriver, SvdOptions};
382    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
383    ///
384    /// let input = TypedTensor::<f64>::from_vec_col_major(
385    ///     vec![2, 2],
386    ///     vec![1.0, 0.0, 0.0, 2.0],
387    /// )?;
388    /// let mut host = tenferro_cpu::CpuEngine::serial();
389    /// let outputs = {
390    ///     with_cpu_engine(&mut host, |backend| {
391    ///         backend.svd_with_options_read(
392    ///             TensorRead::from_view(TensorView::F64(input.as_view())),
393    ///             SvdOptions::default().driver(SvdDriver::Gesvd),
394    ///         )
395    ///     })
396    ///     .expect("CpuEngine must expose a CpuEngine")
397    /// }?;
398    /// assert_eq!(outputs[1].shape(), &[2]);
399    /// # Ok::<(), tenferro_tensor::Error>(())
400    /// ```
401    ///
402    /// # Errors
403    ///
404    /// Returns [`tenferro_tensor::Error::Validation`] containing
405    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
406    /// `derivative_eps` is non-finite or non-positive, plus the validation,
407    /// unsupported-dtype, numerical, placement, or typed backend/provider
408    /// errors from [`LinalgBackend::svd_read`] and the gauge metadata and
409    /// host-access errors from the default host gauge path.
410    fn svd_with_options_read(
411        &mut self,
412        input: TensorRead<'_>,
413        options: SvdOptions,
414    ) -> tenferro_tensor::Result<Vec<Tensor>> {
415        validate_derivative_eps("svd_with_options_read", options.derivative_eps)?;
416        let mut outputs = self.svd_read(input)?;
417        apply_svd_gauge(options.gauge, &mut outputs)?;
418        Ok(outputs)
419    }
420
421    #[doc(hidden)]
422    fn svd_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
423        Err(tenferro_tensor::Error::unsupported(
424            "svd_values",
425            "backend does not implement borrowed singular-values-only decomposition",
426        ))
427    }
428
429    /// Borrowed-input counterpart of [`LinalgBackend::svd_values_with_driver`].
430    #[doc(hidden)]
431    fn svd_values_with_driver_read(
432        &mut self,
433        input: TensorRead<'_>,
434        _driver: SvdDriver,
435    ) -> tenferro_tensor::Result<Tensor> {
436        self.svd_values_read(input)
437    }
438
439    /// Compute public QR outputs `(Q, R)`.
440    ///
441    /// QR is thin: for an `m x n` input, `Q` has shape `m x min(m, n)` and
442    /// `R` has shape `min(m, n) x n`.
443    ///
444    /// This is [`LinalgBackend::qr_with_options`] with default options. The
445    /// CUDA provider does not inspect the cuSOLVER device status flags for
446    /// ordinary QR; synchronous provider errors still propagate. It does not
447    /// report rank deficiency: shapes and dtypes are validated up front, and a
448    /// rank-deficient input still yields its valid `(Q, R)` factors.
449    ///
450    /// # Errors
451    ///
452    /// Returns `Error::Validation` for an unsupported rank, shape, or dtype,
453    /// and a typed `Error::Extension` or backend source when QR execution
454    /// fails.
455    fn qr(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
456
457    /// Compute public QR outputs `(Q, R)` with explicit options.
458    ///
459    /// `gauge` controls optional sign or phase post-processing. The CUDA
460    /// provider does not inspect the cuSOLVER device status flags for ordinary
461    /// QR; synchronous provider errors still propagate. It does not report
462    /// rank deficiency: shapes and dtypes are validated up front, and a
463    /// rank-deficient input still yields its valid factors. The caller's own
464    /// verification decides whether those factors are acceptable.
465    ///
466    /// # Examples
467    ///
468    /// ```rust
469    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
470    /// use tenferro_linalg::{LinalgBackend, QrGauge, QrOptions};
471    /// use tenferro_tensor::Tensor;
472    ///
473    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
474    /// let mut host = tenferro_cpu::CpuEngine::serial();
475    /// let outputs = {
476    ///     with_cpu_engine(&mut host, |backend| {
477    ///         backend.qr_with_options(
478    ///             &input,
479    ///             QrOptions::default().gauge(QrGauge::PositiveDiagonal),
480    ///         )
481    ///     })
482    ///     .expect("CpuEngine must expose a CpuEngine")
483    /// }?;
484    /// assert_eq!(outputs[0].shape(), &[2, 2]);
485    /// # Ok::<(), tenferro_tensor::Error>(())
486    /// ```
487    ///
488    /// # Errors
489    ///
490    /// Returns [`tenferro_tensor::Error::Validation`] containing
491    /// [`tenferro_tensor::ValidationError::RankMismatch`] or
492    /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for an invalid
493    /// matrix input, or [`tenferro_tensor::ValidationError::InvalidArgument`]
494    /// for malformed gauge output metadata, checked size arithmetic, or an
495    /// unavailable compiled provider. A mismatched generated `Q`/`R` dtype is reported as
496    /// [`tenferro_tensor::ValidationError::DTypeMismatch`]. Provider
497    /// unsupported dtype or numerical rejection is
498    /// [`tenferro_tensor::Error::Extension`] with a typed linalg source, while
499    /// provider failures use [`tenferro_tensor::Error::BackendSource`] and a
500    /// backend-resident input uses [`tenferro_tensor::Error::RuntimeState`].
501    fn qr_with_options(
502        &mut self,
503        input: &Tensor,
504        options: QrOptions,
505    ) -> tenferro_tensor::Result<Vec<Tensor>> {
506        let mut outputs = self.qr(input)?;
507        apply_qr_gauge(options.gauge, &mut outputs)?;
508        Ok(outputs)
509    }
510
511    /// Compute public QR outputs `(Q, R)` from a tensor read target.
512    ///
513    /// Backends may canonicalize the input inside the same placement family, but
514    /// must not silently transfer between CPU and GPU memory.
515    ///
516    /// # Examples
517    ///
518    /// ```rust
519    /// use tenferro_linalg::LinalgBackend;
520    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
521    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
522    ///
523    /// let input = TypedTensor::<f64>::from_vec_col_major(
524    ///     vec![2, 2],
525    ///     vec![1.0, 0.0, 0.0, 2.0],
526    /// )?;
527    /// let mut host = tenferro_cpu::CpuEngine::serial();
528    /// let outputs = {
529    ///     with_cpu_engine(&mut host, |backend| {
530    ///         backend.qr_read(TensorRead::from_view(TensorView::F64(input.as_view())))
531    ///     })
532    ///     .expect("CpuEngine must expose a CpuEngine")
533    /// }?;
534    /// assert_eq!(outputs[0].shape(), &[2, 2]);
535    /// assert_eq!(outputs[1].shape(), &[2, 2]);
536    /// # Ok::<(), tenferro_tensor::Error>(())
537    /// ```
538    ///
539    /// # Errors
540    ///
541    /// The default implementation returns `Error::Unsupported` because the
542    /// backend does not accept tensor read targets; implementations may return
543    /// validation or typed backend-source errors.
544    fn qr_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
545        Err(tenferro_tensor::Error::unsupported(
546            "qr",
547            "backend does not accept tensor reads at this execution boundary",
548        ))
549    }
550
551    /// Compute public QR outputs `(Q, R)` from a tensor read target with options.
552    ///
553    /// Device backends override this hook to keep gauge processing in the input
554    /// placement. The default is appropriate for host backends.
555    ///
556    /// # Examples
557    ///
558    /// ```rust
559    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
560    /// use tenferro_linalg::{LinalgBackend, QrGauge, QrOptions};
561    /// use tenferro_tensor::{Tensor, TensorRead};
562    ///
563    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
564    /// let mut host = tenferro_cpu::CpuEngine::serial();
565    /// let outputs = {
566    ///     with_cpu_engine(&mut host, |backend| {
567    ///         backend.qr_with_options_read(
568    ///             TensorRead::from_tensor(&input),
569    ///             QrOptions::default().gauge(QrGauge::PositiveDiagonal),
570    ///         )
571    ///     })
572    ///     .expect("CpuEngine must expose a CpuEngine")
573    /// }?;
574    /// assert_eq!(outputs[0].shape(), &[2, 2]);
575    /// # Ok::<(), tenferro_tensor::Error>(())
576    /// ```
577    ///
578    /// # Errors
579    ///
580    /// Returns the validation, unsupported-dtype, numerical, placement, or
581    /// typed backend/provider errors from [`LinalgBackend::qr_read`], plus
582    /// gauge metadata and host-access errors from the default host gauge path.
583    fn qr_with_options_read(
584        &mut self,
585        input: TensorRead<'_>,
586        options: QrOptions,
587    ) -> tenferro_tensor::Result<Vec<Tensor>> {
588        let mut outputs = self.qr_read(input)?;
589        apply_qr_gauge(options.gauge, &mut outputs)?;
590        Ok(outputs)
591    }
592
593    #[doc(hidden)]
594    fn rank_revealing_qr(
595        &mut self,
596        _input: &Tensor,
597        _options: RankRevealingQrOptions,
598    ) -> tenferro_tensor::Result<Vec<Tensor>> {
599        Err(tenferro_tensor::Error::unsupported(
600            "rank_revealing_qr",
601            "backend does not implement rank-revealing QR",
602        ))
603    }
604
605    #[doc(hidden)]
606    fn rank_revealing_qr_read(
607        &mut self,
608        _input: TensorRead<'_>,
609        _options: RankRevealingQrOptions,
610    ) -> tenferro_tensor::Result<Vec<Tensor>> {
611        Err(tenferro_tensor::Error::unsupported(
612            "rank_revealing_qr",
613            "backend does not implement borrowed rank-revealing QR",
614        ))
615    }
616
617    #[doc(hidden)]
618    fn householder_qr(&mut self, _input: &Tensor) -> tenferro_tensor::Result<CompactQrResult> {
619        Err(tenferro_tensor::Error::unsupported(
620            "householder_qr",
621            "backend does not implement compact Householder QR",
622        ))
623    }
624
625    #[doc(hidden)]
626    fn householder_qr_from_factors(
627        &mut self,
628        _q: &Tensor,
629        _r: &Tensor,
630    ) -> tenferro_tensor::Result<CompactQrResult> {
631        Err(tenferro_tensor::Error::unsupported(
632            "householder_qr_from_factors",
633            "backend does not implement compact Householder QR factor import",
634        ))
635    }
636
637    #[doc(hidden)]
638    fn householder_qr_append(
639        &mut self,
640        _packed: &Tensor,
641        _coeff: &Tensor,
642        _block: &Tensor,
643    ) -> tenferro_tensor::Result<CompactQrResult> {
644        Err(tenferro_tensor::Error::unsupported(
645            "householder_qr_append",
646            "backend does not implement compact Householder QR append",
647        ))
648    }
649
650    #[doc(hidden)]
651    fn householder_qr_r(
652        &mut self,
653        _packed: &Tensor,
654        _coeff: &Tensor,
655        _options: QrOptions,
656    ) -> tenferro_tensor::Result<Tensor> {
657        Err(tenferro_tensor::Error::unsupported(
658            "householder_qr_r",
659            "backend does not implement compact Householder QR extraction",
660        ))
661    }
662
663    /// Materialize Q columns `columns` from compact Householder state.
664    ///
665    /// The reachable width is full Q: for an `m x n` input, `columns` may run
666    /// to `m`, and the columns past `k = min(m, n)` span the orthogonal
667    /// complement of the input's column space. `QrOptions::gauge` comes from
668    /// R's diagonal and therefore applies to the first `k` columns only.
669    #[doc(hidden)]
670    fn householder_qr_q_columns(
671        &mut self,
672        _packed: &Tensor,
673        _coeff: &Tensor,
674        _columns: Range<usize>,
675        _options: QrOptions,
676    ) -> tenferro_tensor::Result<Tensor> {
677        Err(tenferro_tensor::Error::unsupported(
678            "householder_qr_q_columns",
679            "backend does not implement compact Householder Q-column materialization",
680        ))
681    }
682
683    /// Compute public Hermitian eigendecomposition outputs `(values, vectors)`.
684    ///
685    /// The returned vector order is `[values, vectors]`, where `values` has
686    /// shape `[n]` and `vectors` has shape `[n, n]`.
687    ///
688    /// # Errors
689    ///
690    /// Returns `Error::Validation` for a non-square or unsupported-dtype input
691    /// and a typed `Error::Extension` or backend source when eigendecomposition
692    /// fails.
693    fn eigh(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
694
695    /// Compute public Hermitian eigendecomposition outputs with explicit options.
696    ///
697    /// `derivative_eps` is validated for API consistency, but concrete backend
698    /// execution does not perform AD. `gauge` controls optional eigenvector
699    /// post-processing.
700    ///
701    /// # Examples
702    ///
703    /// ```rust
704    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
705    /// use tenferro_linalg::{EighGauge, EighOptions, LinalgBackend};
706    /// use tenferro_tensor::Tensor;
707    ///
708    /// let input = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 0.0, 0.0, 2.0])?;
709    /// let mut host = tenferro_cpu::CpuEngine::serial();
710    /// let outputs = {
711    ///     with_cpu_engine(&mut host, |backend| {
712    ///         backend.eigh_with_options(
713    ///             &input,
714    ///             EighOptions::default()
715    ///                 .gauge(EighGauge::CanonicalPivot)
716    ///                 .derivative_eps(1.0e-10),
717    ///         )
718    ///     })
719    ///     .expect("CpuEngine must expose a CpuEngine")
720    /// }?;
721    /// assert_eq!(outputs[0].shape(), &[2]);
722    /// # Ok::<(), tenferro_tensor::Error>(())
723    /// ```
724    ///
725    /// # Errors
726    ///
727    /// Returns [`tenferro_tensor::Error::Validation`] containing
728    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
729    /// `derivative_eps` is non-finite or non-positive, when canonical gauge
730    /// output metadata is malformed, or when checked output-size arithmetic
731    /// overflows. It can return [`tenferro_tensor::Error::Validation`] with
732    /// [`tenferro_tensor::ValidationError::RankMismatch`] or
733    /// [`tenferro_tensor::ValidationError::ShapeMismatch`] for the
734    /// matrix input, or [`tenferro_tensor::ValidationError::DTypeMismatch`]
735    /// for generated outputs. It can also return
736    /// [`tenferro_tensor::Error::Extension`] with typed
737    /// `tenferro_linalg::Error::UnsupportedDType` or `NonConvergence`, and
738    /// [`tenferro_tensor::Error::BackendSource`] or
739    /// [`tenferro_tensor::Error::RuntimeState`] for provider and placement
740    /// failures.
741    fn eigh_with_options(
742        &mut self,
743        input: &Tensor,
744        options: EighOptions,
745    ) -> tenferro_tensor::Result<Vec<Tensor>> {
746        validate_derivative_eps("eigh_with_options", options.derivative_eps)?;
747        let mut outputs = self.eigh(input)?;
748        apply_eigh_gauge(options.gauge, &mut outputs)?;
749        Ok(outputs)
750    }
751
752    /// Compute public eigh outputs `(values, vectors)` from a tensor read
753    /// target with explicit options.
754    ///
755    /// This is the borrowed-input counterpart of
756    /// [`LinalgBackend::eigh_with_options`]. The default implementation runs
757    /// [`LinalgBackend::eigh_read`] and applies the gauge on the host; it
758    /// ignores `driver` because CPU providers have a single eigh kernel. The
759    /// CUDA backend overrides it so the driver reaches cuSOLVER.
760    ///
761    /// # Examples
762    ///
763    /// ```rust
764    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
765    /// use tenferro_linalg::{EighDriver, EighOptions, LinalgBackend};
766    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
767    ///
768    /// let input = TypedTensor::<f64>::from_vec_col_major(
769    ///     vec![2, 2],
770    ///     vec![2.0, 0.0, 0.0, 3.0],
771    /// )?;
772    /// let mut host = tenferro_cpu::CpuEngine::serial();
773    /// let outputs = {
774    ///     with_cpu_engine(&mut host, |backend| {
775    ///         backend.eigh_with_options_read(
776    ///             TensorRead::from_view(TensorView::F64(input.as_view())),
777    ///             EighOptions::default().driver(EighDriver::Syevj),
778    ///         )
779    ///     })
780    ///     .expect("CpuEngine must expose a CpuEngine")
781    /// }?;
782    /// assert_eq!(outputs[0].shape(), &[2]);
783    /// # Ok::<(), tenferro_tensor::Error>(())
784    /// ```
785    ///
786    /// # Errors
787    ///
788    /// Returns [`tenferro_tensor::Error::Validation`] containing
789    /// [`tenferro_tensor::ValidationError::InvalidArgument`] when
790    /// `derivative_eps` is non-finite or non-positive, plus the validation,
791    /// unsupported-dtype, numerical, placement, or typed backend/provider
792    /// errors from [`LinalgBackend::eigh_read`] and the gauge metadata and
793    /// host-access errors from the default host gauge path.
794    fn eigh_with_options_read(
795        &mut self,
796        input: TensorRead<'_>,
797        options: EighOptions,
798    ) -> tenferro_tensor::Result<Vec<Tensor>> {
799        validate_derivative_eps("eigh_with_options_read", options.derivative_eps)?;
800        let mut outputs = self.eigh_read(input)?;
801        apply_eigh_gauge(options.gauge, &mut outputs)?;
802        Ok(outputs)
803    }
804
805    /// Compute public Hermitian eigendecomposition outputs from a tensor read target.
806    ///
807    /// Backends may canonicalize the input inside the same placement family, but
808    /// must not silently transfer between CPU and GPU memory.
809    ///
810    /// # Examples
811    ///
812    /// ```rust
813    /// use tenferro_linalg::LinalgBackend;
814    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
815    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
816    ///
817    /// let input = TypedTensor::<f64>::from_vec_col_major(
818    ///     vec![2, 2],
819    ///     vec![1.0, 0.0, 0.0, 2.0],
820    /// )?;
821    /// let mut host = tenferro_cpu::CpuEngine::serial();
822    /// let outputs = {
823    ///     with_cpu_engine(&mut host, |backend| {
824    ///         backend.eigh_read(TensorRead::from_view(TensorView::F64(input.as_view())))
825    ///     })
826    ///     .expect("CpuEngine must expose a CpuEngine")
827    /// }?;
828    /// assert_eq!(outputs[0].shape(), &[2]);
829    /// assert_eq!(outputs[1].shape(), &[2, 2]);
830    /// # Ok::<(), tenferro_tensor::Error>(())
831    /// ```
832    ///
833    /// # Errors
834    ///
835    /// The default implementation returns `Error::Unsupported` because the
836    /// backend does not accept tensor read targets; implementations may return
837    /// validation or typed backend-source errors.
838    fn eigh_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
839        Err(tenferro_tensor::Error::unsupported(
840            "eigh",
841            "backend does not accept tensor reads at this execution boundary",
842        ))
843    }
844
845    /// Compute Cholesky factorization from a tensor read target.
846    ///
847    /// Backends may canonicalize the input inside the same placement family, but
848    /// must not silently transfer between CPU and GPU memory.
849    ///
850    /// # Examples
851    ///
852    /// ```rust
853    /// use tenferro_linalg::LinalgBackend;
854    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
855    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
856    ///
857    /// let input = TypedTensor::<f64>::from_vec_col_major(
858    ///     vec![2, 2],
859    ///     vec![4.0, 2.0, 2.0, 3.0],
860    /// )?;
861    /// let mut host = tenferro_cpu::CpuEngine::serial();
862    /// let output = {
863    ///     with_cpu_engine(&mut host, |backend| {
864    ///         backend.cholesky_read(TensorRead::from_view(TensorView::F64(input.as_view())))
865    ///     })
866    ///     .expect("CpuEngine must expose a CpuEngine")
867    /// }?;
868    /// assert_eq!(output.shape(), &[2, 2]);
869    /// # Ok::<(), tenferro_tensor::Error>(())
870    /// ```
871    ///
872    /// # Errors
873    ///
874    /// The default implementation returns `Error::Unsupported` because the
875    /// backend does not accept tensor read targets; implementations may return
876    /// validation or typed backend-source errors.
877    fn cholesky_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
878        Err(tenferro_tensor::Error::unsupported(
879            "cholesky",
880            "backend does not accept tensor reads at this execution boundary",
881        ))
882    }
883
884    /// Compute public LU outputs from a tensor read target.
885    ///
886    /// Backends may canonicalize the input inside the same placement family, but
887    /// must not silently transfer between CPU and GPU memory.
888    ///
889    /// # Examples
890    ///
891    /// ```rust
892    /// use tenferro_linalg::LinalgBackend;
893    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
894    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
895    ///
896    /// let input = TypedTensor::<f64>::from_vec_col_major(
897    ///     vec![2, 2],
898    ///     vec![1.0, 3.0, 2.0, 4.0],
899    /// )?;
900    /// let mut host = tenferro_cpu::CpuEngine::serial();
901    /// let outputs = {
902    ///     with_cpu_engine(&mut host, |backend| {
903    ///         backend.lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
904    ///     })
905    ///     .expect("CpuEngine must expose a CpuEngine")
906    /// }?;
907    /// assert_eq!(outputs.len(), 4);
908    /// # Ok::<(), tenferro_tensor::Error>(())
909    /// ```
910    ///
911    /// # Errors
912    ///
913    /// The default implementation returns `Error::Unsupported` because the
914    /// backend does not accept tensor read targets; implementations may return
915    /// validation or typed backend-source errors.
916    fn lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
917        Err(tenferro_tensor::Error::unsupported(
918            "lu",
919            "backend does not accept tensor reads at this execution boundary",
920        ))
921    }
922
923    /// Compute public full-pivoting LU outputs from a tensor read target.
924    ///
925    /// Backends may canonicalize the input inside the same placement family, but
926    /// must not silently transfer between CPU and GPU memory.
927    ///
928    /// # Examples
929    ///
930    /// ```rust
931    /// use tenferro_linalg::LinalgBackend;
932    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
933    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
934    ///
935    /// let input = TypedTensor::<f64>::from_vec_col_major(
936    ///     vec![2, 2],
937    ///     vec![1.0, 3.0, 2.0, 4.0],
938    /// )?;
939    /// let mut host = tenferro_cpu::CpuEngine::serial();
940    /// let outputs = {
941    ///     with_cpu_engine(&mut host, |backend| {
942    ///         backend.full_piv_lu_read(TensorRead::from_view(TensorView::F64(input.as_view())))
943    ///     })
944    ///     .expect("CpuEngine must expose a CpuEngine")
945    /// }?;
946    /// assert_eq!(outputs.len(), 5);
947    /// # Ok::<(), tenferro_tensor::Error>(())
948    /// ```
949    ///
950    /// # Errors
951    ///
952    /// The default implementation returns `Error::Unsupported` because the
953    /// backend does not accept tensor read targets; implementations may return
954    /// validation or typed backend-source errors.
955    fn full_piv_lu_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
956        Err(tenferro_tensor::Error::unsupported(
957            "full_piv_lu",
958            "backend does not accept tensor reads at this execution boundary",
959        ))
960    }
961
962    /// Compute general eigendecomposition outputs from a tensor read target.
963    ///
964    /// Backends may canonicalize the input inside the same placement family, but
965    /// must not silently transfer between CPU and GPU memory.
966    ///
967    /// # Examples
968    ///
969    /// ```rust
970    /// use tenferro_linalg::LinalgBackend;
971    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
972    /// use tenferro_tensor::{TensorRead, TensorView, TypedTensor};
973    ///
974    /// let input = TypedTensor::<f64>::from_vec_col_major(
975    ///     vec![2, 2],
976    ///     vec![2.0, 0.0, 0.0, 3.0],
977    /// )?;
978    /// let mut host = tenferro_cpu::CpuEngine::serial();
979    /// let outputs = {
980    ///     with_cpu_engine(&mut host, |backend| {
981    ///         backend.eig_read(TensorRead::from_view(TensorView::F64(input.as_view())))
982    ///     })
983    ///     .expect("CpuEngine must expose a CpuEngine")
984    /// }?;
985    /// assert_eq!(outputs.len(), 2);
986    /// # Ok::<(), tenferro_tensor::Error>(())
987    /// ```
988    ///
989    /// # Errors
990    ///
991    /// The default implementation returns `Error::Unsupported` because the
992    /// backend does not accept tensor read targets; implementations may return
993    /// validation or typed backend-source errors.
994    fn eig_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Vec<Tensor>> {
995        Err(tenferro_tensor::Error::unsupported(
996            "eig",
997            "backend does not accept tensor reads at this execution boundary",
998        ))
999    }
1000
1001    #[doc(hidden)]
1002    fn eigh_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
1003        Err(tenferro_tensor::Error::unsupported(
1004            "eigh_values",
1005            format!(
1006                "backend {} does not implement internal Hermitian eigenvalues-only decomposition",
1007                std::any::type_name::<Self>()
1008            ),
1009        ))
1010    }
1011
1012    /// Eigenvalues only with an explicit CUDA driver. The default
1013    /// implementation ignores the driver, matching
1014    /// [`LinalgBackend::eigh_with_options`].
1015    #[doc(hidden)]
1016    fn eigh_values_with_driver(
1017        &mut self,
1018        input: &Tensor,
1019        _driver: EighDriver,
1020    ) -> tenferro_tensor::Result<Tensor> {
1021        self.eigh_values(input)
1022    }
1023
1024    #[doc(hidden)]
1025    fn eigh_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
1026        Err(tenferro_tensor::Error::unsupported(
1027            "eigh_values",
1028            "backend does not implement borrowed Hermitian eigenvalues-only decomposition",
1029        ))
1030    }
1031
1032    /// Borrowed-input counterpart of
1033    /// [`LinalgBackend::eigh_values_with_driver`].
1034    #[doc(hidden)]
1035    fn eigh_values_with_driver_read(
1036        &mut self,
1037        input: TensorRead<'_>,
1038        _driver: EighDriver,
1039    ) -> tenferro_tensor::Result<Tensor> {
1040        self.eigh_values_read(input)
1041    }
1042
1043    /// Compute public general eigendecomposition outputs `(values, vectors)`.
1044    /// Eigenvectors come back with unit 2-norm columns, whichever provider computed them;
1045    /// a column the provider left non-finite is reported as a numerical failure instead
1046    /// of being returned as `NaN` (#2148).
1047    ///
1048    /// # Errors
1049    ///
1050    /// Returns `Error::Validation` for a non-square, rank, or dtype mismatch,
1051    /// and a typed `Error::Extension` or backend source when the eigensolver
1052    /// fails.
1053    fn eig(&mut self, input: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>>;
1054
1055    #[doc(hidden)]
1056    fn eig_values(&mut self, _input: &Tensor) -> tenferro_tensor::Result<Tensor> {
1057        Err(tenferro_tensor::Error::unsupported(
1058            "eig_values",
1059            format!(
1060                "backend {} does not implement internal general eigenvalues-only decomposition",
1061                std::any::type_name::<Self>()
1062            ),
1063        ))
1064    }
1065
1066    #[doc(hidden)]
1067    fn eig_values_read(&mut self, _input: TensorRead<'_>) -> tenferro_tensor::Result<Tensor> {
1068        Err(tenferro_tensor::Error::unsupported(
1069            "eig_values",
1070            "backend does not implement borrowed general eigenvalues-only decomposition",
1071        ))
1072    }
1073
1074    /// Solve a dense linear system.
1075    ///
1076    /// The CUDA provider does not inspect the cuSOLVER `getrf` status flag on
1077    /// the forward solve path, so it performs no per-call non-singularity check
1078    /// and issues no host round trip. An exactly singular input is left to the
1079    /// caller's verification, as in `torch.linalg.solve`. CPU providers may
1080    /// still report their own exactly singular factor as `Error::Singular`.
1081    ///
1082    /// # Errors
1083    ///
1084    /// Returns `Error::Validation` for incompatible matrix/rhs shapes, rank,
1085    /// or dtype; `Error::Extension` with `ErrorKind::NumericalFailure` for a
1086    /// singular system; or a typed backend source for provider failure.
1087    fn solve(&mut self, a: &Tensor, b: &Tensor) -> tenferro_tensor::Result<Tensor>;
1088
1089    /// Solve a linear system from tensor read targets.
1090    ///
1091    /// Backends may canonicalize the inputs inside the same placement family,
1092    /// but must not silently transfer between CPU and GPU memory.
1093    ///
1094    /// # Examples
1095    ///
1096    /// ```rust
1097    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
1098    /// use tenferro_linalg::LinalgBackend;
1099    /// use tenferro_tensor::{Tensor, TensorRead};
1100    ///
1101    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 3.0])?;
1102    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 9.0])?;
1103    /// let mut host = tenferro_cpu::CpuEngine::serial();
1104    /// let x = {
1105    ///     with_cpu_engine(&mut host, |backend| {
1106    ///         backend.solve_read(
1107    ///             TensorRead::from_tensor(&a),
1108    ///             TensorRead::from_tensor(&b),
1109    ///         )
1110    ///     })
1111    ///     .expect("CpuEngine must expose a CpuEngine")
1112    /// }?;
1113    /// let x = x.into_typed::<f64>().expect("F64 inputs return F64 output");
1114    /// assert_eq!(x.host_data()?, &[2.0, 3.0]);
1115    /// # Ok::<(), tenferro_tensor::Error>(())
1116    /// ```
1117    ///
1118    /// The CUDA provider does not inspect the cuSOLVER `getrf` status flag on
1119    /// the forward solve path, so it performs no per-call non-singularity check
1120    /// and issues no host round trip. An exactly singular input is left to the
1121    /// caller's verification, as in `torch.linalg.solve`. CPU providers may
1122    /// still report their own exactly singular factor as `Error::Singular`.
1123    ///
1124    /// # Errors
1125    ///
1126    /// The default implementation returns `Error::Unsupported` because the
1127    /// backend does not accept tensor read targets. Implementations may return
1128    /// `Error::Validation` for incompatible shapes or dtypes,
1129    /// `Error::RuntimeState` for invalid placement, `Error::Extension` for a
1130    /// singular system, or a typed backend-source error.
1131    fn solve_read(
1132        &mut self,
1133        _a: TensorRead<'_>,
1134        _b: TensorRead<'_>,
1135    ) -> tenferro_tensor::Result<Tensor> {
1136        Err(tenferro_tensor::Error::unsupported(
1137            "solve",
1138            "backend does not accept tensor reads at this execution boundary",
1139        ))
1140    }
1141
1142    /// Solve into a caller-owned destination.
1143    ///
1144    /// The default preserves the ordinary read path and copies its result into
1145    /// `out`. Backends with a native destination path may override this method,
1146    /// but must validate the destination before the first write and preserve
1147    /// the same shape, dtype, placement, aliasing, and error contracts.
1148    ///
1149    /// The CUDA provider does not inspect the cuSOLVER `getrf` status flag on
1150    /// the forward solve path, so it performs no per-call non-singularity check
1151    /// and issues no host round trip. An exactly singular input is left to the
1152    /// caller's verification, as in `torch.linalg.solve`. CPU providers may
1153    /// still report their own exactly singular factor as `Error::Singular`.
1154    ///
1155    /// # Errors
1156    ///
1157    /// Returns `tenferro_tensor_core::ShapeMismatch` or
1158    /// `tenferro_tensor_core::ValidationError::DTypeMismatch` for incompatible
1159    /// destination metadata, `tenferro_tensor_core::ValidationError::InvalidArgument`
1160    /// for aliasing or placement violations, `Error::Unsupported` when the
1161    /// provider is unavailable, and `Error::Singular` for a singular system.
1162    ///
1163    /// # Examples
1164    ///
1165    /// ```rust
1166    /// use tenferro_cpu::{with_cpu_engine, CpuEngine};
1167    /// use tenferro_linalg::LinalgBackend;
1168    /// use tenferro_tensor::{Tensor, TensorRead, TensorWrite};
1169    ///
1170    /// let a = Tensor::from_vec_col_major(vec![2, 2], vec![2.0_f64, 0.0, 0.0, 4.0])?;
1171    /// let b = Tensor::from_vec_col_major(vec![2, 1], vec![4.0_f64, 8.0])?;
1172    /// let mut out = Tensor::from_vec_col_major(vec![2, 1], vec![0.0_f64; 2])?;
1173    /// let mut host = tenferro_cpu::CpuEngine::serial();
1174    /// {
1175    ///     with_cpu_engine(&mut host, |backend| {
1176    ///         backend.solve_read_into(
1177    ///             TensorRead::from_tensor(&a),
1178    ///             TensorRead::from_tensor(&b),
1179    ///             TensorWrite::from_tensor(&mut out),
1180    ///         )
1181    ///     })
1182    ///     .expect("CpuEngine must expose a CpuEngine")
1183    /// }?;
1184    /// assert_eq!(out.as_slice::<f64>()?, &[2.0, 2.0]);
1185    /// # Ok::<(), tenferro_tensor::Error>(())
1186    /// ```
1187    fn solve_read_into(
1188        &mut self,
1189        a: TensorRead<'_>,
1190        b: TensorRead<'_>,
1191        out: TensorWrite<'_>,
1192    ) -> tenferro_tensor::Result<()> {
1193        solve_read_into_default(self, a, b, out)
1194    }
1195
1196    #[doc(hidden)]
1197    fn lu_solve_prepared(
1198        &mut self,
1199        _a: &Tensor,
1200        _packed_lu: &Tensor,
1201        _pivots: &Tensor,
1202        _b: &Tensor,
1203        _transpose_a: bool,
1204        _conjugate_a: bool,
1205    ) -> tenferro_tensor::Result<Tensor> {
1206        Err(tenferro_tensor::Error::unsupported(
1207            "lu_solve_prepared",
1208            format!(
1209                "backend {} does not implement internal prepared LU solve",
1210                std::any::type_name::<Self>()
1211            ),
1212        ))
1213    }
1214
1215    /// Factor `a` and solve `a x = b`, returning `(x, packed_lu, pivots)`.
1216    ///
1217    /// This is the fused primal of `lu_factor` followed by
1218    /// `lu_solve_prepared`. The default composes those two hooks; a backend
1219    /// overrides it to run both steps in one batched kernel without the
1220    /// intermediate parity output.
1221    ///
1222    /// # Errors
1223    ///
1224    /// Returns the errors of `lu_factor` and `lu_solve_prepared`, including
1225    /// `Error::Extension` for an exactly singular `a` with a nonempty `b`.
1226    #[doc(hidden)]
1227    fn lu_factor_solve(&mut self, a: &Tensor, b: &Tensor) -> tenferro_tensor::Result<Vec<Tensor>> {
1228        let mut factors = self.lu_factor(a)?.into_iter();
1229        let (Some(packed_lu), Some(pivots)) = (factors.next(), factors.next()) else {
1230            return Err(tenferro_tensor::Error::Internal(
1231                "lu_factor_solve: lu_factor returned fewer than two outputs".into(),
1232            ));
1233        };
1234        let x = self.lu_solve_prepared(a, &packed_lu, &pivots, b, false, false)?;
1235        Ok(vec![x, packed_lu, pivots])
1236    }
1237}
1238
1239pub(crate) fn solve_read_into_default<B: LinalgBackend + ?Sized>(
1240    backend: &mut B,
1241    a: TensorRead<'_>,
1242    b: TensorRead<'_>,
1243    out: TensorWrite<'_>,
1244) -> tenferro_tensor::Result<()> {
1245    validate_solve_read_into(&a, &b, &out)?;
1246    let result = backend.solve_read(a, b)?;
1247    backend.copy_read_into(TensorRead::from_tensor(&result), out)
1248}
1249
1250pub(crate) fn validate_solve_read_into(
1251    _a: &TensorRead<'_>,
1252    b: &TensorRead<'_>,
1253    out: &TensorWrite<'_>,
1254) -> tenferro_tensor::Result<()> {
1255    if b.shape() != out.shape() {
1256        return Err(tenferro_tensor::Error::shape_mismatch(
1257            "solve_read_into",
1258            b.shape().to_vec(),
1259            out.shape().to_vec(),
1260        ));
1261    }
1262    if b.dtype() != out.dtype() {
1263        return Err(tenferro_tensor::Error::dtype_mismatch(
1264            "solve_read_into",
1265            b.dtype(),
1266            out.dtype(),
1267        ));
1268    }
1269    if b.placement() != out.as_read().placement() {
1270        return Err(tenferro_tensor::Error::invalid_argument(
1271            "solve_read_into",
1272            "out",
1273            format!(
1274                "destination placement {:?} does not match rhs placement {:?}",
1275                out.as_read().placement(),
1276                b.placement()
1277            ),
1278        ));
1279    }
1280    let inputs = [_a.clone(), b.clone()];
1281    tenferro_tensor::backend::validate_read_into_destination("solve_read_into", &inputs, out)
1282}