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}