Skip to main content

tenferro_linalg/
extension.rs

1use std::any::Any;
2use std::hash::{Hash, Hasher};
3use std::sync::Arc;
4
5use num_complex::{Complex32, Complex64};
6use tenferro_cpu::with_cpu_engine;
7use tenferro_extension_macros::define_extension_runtime;
8use tenferro_ops::SymDim;
9use tenferro_runtime::extension::ExtensionOp;
10use tenferro_tensor::{DType, Engine, Error, ErrorKind, Tensor, TensorRead};
11
12#[cfg(feature = "cuda")]
13use tenferro_gpu::cuda::raw::with_cuda_engine;
14
15use crate::backend::LinalgBackend;
16use crate::RankRevealingQrOptions;
17
18#[cfg(all(test, feature = "cuda"))]
19#[path = "extension/cuda_tests.rs"]
20mod cuda_tests;
21mod gauge;
22#[cfg(all(test, not(feature = "cuda")))]
23mod tests;
24
25pub(crate) use gauge::{apply_eigh_gauge, apply_qr_gauge};
26
27pub const LINALG_EXTENSION_FAMILY_ID: &str = "tenferro-linalg.linalg.v1";
28
29/// Default derivative regularization used by decomposition AD rules.
30///
31/// This epsilon is used only when differentiating decomposition formulas with
32/// repeated or nearly repeated spectral values. It is not a solver tolerance.
33///
34/// # Examples
35///
36/// ```rust
37/// use tenferro_linalg::{SvdOptions, DEFAULT_DECOMPOSITION_DERIVATIVE_EPS};
38///
39/// let options = SvdOptions::default();
40/// assert_eq!(options.derivative_eps, DEFAULT_DECOMPOSITION_DERIVATIVE_EPS);
41/// ```
42pub const DEFAULT_DECOMPOSITION_DERIVATIVE_EPS: f64 = 1e-12;
43
44/// Singular-vector gauge convention used by [`SvdOptions`].
45///
46/// # Examples
47///
48/// ```rust
49/// use tenferro_linalg::{SvdGauge, SvdOptions};
50///
51/// let options = SvdOptions::default().gauge(SvdGauge::CanonicalPivot);
52/// assert_eq!(options.gauge, SvdGauge::CanonicalPivot);
53/// ```
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub enum SvdGauge {
56    /// Leave the backend's raw singular vector signs or phases unchanged.
57    Raw,
58    /// Make each left singular vector's max-absolute pivot entry positive-real
59    /// and adjust the matching `VT` row so reconstruction is preserved.
60    CanonicalPivot,
61}
62
63/// Eigenvector gauge convention used by [`EighOptions`].
64///
65/// # Examples
66///
67/// ```rust
68/// use tenferro_linalg::{EighGauge, EighOptions};
69///
70/// let options = EighOptions::default().gauge(EighGauge::CanonicalPivot);
71/// assert_eq!(options.gauge, EighGauge::CanonicalPivot);
72/// ```
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum EighGauge {
75    /// Leave the backend's raw eigenvector signs or phases unchanged.
76    Raw,
77    /// Make each eigenvector's max-absolute pivot entry positive-real.
78    CanonicalPivot,
79}
80
81/// QR factor gauge convention used by [`QrOptions`].
82///
83/// # Examples
84///
85/// ```rust
86/// use tenferro_linalg::{QrGauge, QrOptions};
87///
88/// let options = QrOptions::default().gauge(QrGauge::PositiveDiagonal);
89/// assert_eq!(options.gauge, QrGauge::PositiveDiagonal);
90/// ```
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
92pub enum QrGauge {
93    /// Leave the backend's raw QR signs or phases unchanged.
94    Raw,
95    /// Make each `R` diagonal entry positive-real, compensating `Q`.
96    PositiveDiagonal,
97}
98
99/// cuSOLVER SVD routine selection used by [`SvdOptions`] on the CUDA backend.
100///
101/// The driver changes speed and numerical behavior; [`Self::Xgesvdp`] may
102/// perturb near-singular inputs. Gauge handling and AD rule selection are
103/// unchanged. CPU providers have a single SVD kernel and ignore the driver.
104/// Equivalent to `jax.lax.linalg.svd(..., algorithm=...)` and
105/// `torch.linalg.svd(..., driver=...)`.
106///
107/// # Examples
108///
109/// ```rust
110/// use tenferro_linalg::{SvdDriver, SvdOptions};
111///
112/// let options = SvdOptions::default().driver(SvdDriver::Gesvd);
113/// assert_eq!(options.driver, SvdDriver::Gesvd);
114/// assert_eq!(SvdOptions::default().driver, SvdDriver::Auto);
115/// ```
116#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
117pub enum SvdDriver {
118    /// The backend's default policy. On CUDA this is the accuracy-preserving,
119    /// JAX-compatible rule: `gesvdj` when both matrix dimensions are at most
120    /// 1024, otherwise `gesvd`. It never selects the polar
121    /// [`Self::Xgesvdp`], whose small singular values are only accurate to
122    /// about `1e-6` of the largest (#2182). CPU providers have a single SVD
123    /// kernel and ignore the driver.
124    #[default]
125    Auto,
126    /// cuSOLVER's Jacobi driver (`cusolverDn<t>gesvdj`) regardless of size.
127    Gesvdj,
128    /// cuSOLVER's QR-based driver (`cusolverDn<t>gesvd`) regardless of size.
129    Gesvd,
130    /// cuSOLVER's polar-decomposition driver (`cusolverDnXgesvdp`).
131    ///
132    /// Explicit only: [`Self::Auto`] never selects this routine. It is the
133    /// fastest of the three drivers for `min(m, n) >= 256` on an A100 (see the
134    /// `cuda_svd_driver` benchmark), but its small singular values are only
135    /// accurate to about `1e-6` of the largest singular value. Do not use it
136    /// for an `eps`-scale truncation (a pseudoinverse or tensor-network
137    /// truncation at `1e-12`..`1e-14`): the perturbation can change the
138    /// retained rank.
139    ///
140    /// Tenferro reads cuSOLVER's `h_err_sigma` perturbation bound and returns a
141    /// typed backend failure instead of silently wrong factors when it is not
142    /// small relative to the largest singular value. On rank-deficient
143    /// `F32`/`C32` input cuSOLVER reports a perturbation comparable to the
144    /// spectrum, so an explicit single-precision call there fails rather than
145    /// returning the perturbed values. CPU providers accept and ignore this
146    /// selection.
147    ///
148    /// Accuracy is input-dependent, not uniformly better or worse than
149    /// [`Self::Gesvd`]. On ten-decade spectra built as `U diag(s) Vá´´` it was
150    /// the most accurate of the three drivers on an A100, but on a
151    /// circulant-like matrix with the same spectrum it lost about two digits
152    /// against `gesvd`. Measure on your own inputs before relying on it for
153    /// small singular values.
154    Xgesvdp,
155}
156
157/// Options for singular value decomposition.
158///
159/// # Examples
160///
161/// ```rust
162/// use tenferro_linalg::{SvdDriver, SvdGauge, SvdOptions};
163///
164/// let options = SvdOptions::default()
165///     .gauge(SvdGauge::CanonicalPivot)
166///     .derivative_eps(1.0e-10)
167///     .driver(SvdDriver::Gesvd);
168/// assert_eq!(options.gauge, SvdGauge::CanonicalPivot);
169/// assert_eq!(options.derivative_eps, 1.0e-10);
170/// assert_eq!(options.driver, SvdDriver::Gesvd);
171/// ```
172#[derive(Clone, Copy, Debug, PartialEq)]
173pub struct SvdOptions {
174    /// Singular-vector gauge convention.
175    pub gauge: SvdGauge,
176    /// AD derivative regularization for repeated or nearly repeated singular values.
177    pub derivative_eps: f64,
178    /// CUDA SVD driver; ignored by CPU providers.
179    pub driver: SvdDriver,
180}
181
182impl Default for SvdOptions {
183    fn default() -> Self {
184        Self {
185            gauge: SvdGauge::Raw,
186            derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
187            driver: SvdDriver::Auto,
188        }
189    }
190}
191
192impl SvdOptions {
193    /// Return options with the requested singular-vector gauge.
194    ///
195    /// # Examples
196    ///
197    /// ```rust
198    /// use tenferro_linalg::{SvdGauge, SvdOptions};
199    ///
200    /// let options = SvdOptions::default().gauge(SvdGauge::CanonicalPivot);
201    /// assert_eq!(options.gauge, SvdGauge::CanonicalPivot);
202    /// ```
203    pub fn gauge(mut self, gauge: SvdGauge) -> Self {
204        self.gauge = gauge;
205        self
206    }
207
208    /// Return options with an explicit derivative epsilon.
209    ///
210    /// # Examples
211    ///
212    /// ```rust
213    /// use tenferro_linalg::SvdOptions;
214    ///
215    /// let options = SvdOptions::default().derivative_eps(1.0e-9);
216    /// assert_eq!(options.derivative_eps, 1.0e-9);
217    /// ```
218    pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
219        self.derivative_eps = derivative_eps;
220        self
221    }
222
223    /// Return options with an explicit CUDA SVD driver.
224    ///
225    /// # Examples
226    ///
227    /// ```rust
228    /// use tenferro_linalg::{SvdDriver, SvdOptions};
229    ///
230    /// let options = SvdOptions::default().driver(SvdDriver::Gesvdj);
231    /// assert_eq!(options.driver, SvdDriver::Gesvdj);
232    /// ```
233    pub fn driver(mut self, driver: SvdDriver) -> Self {
234        self.driver = driver;
235        self
236    }
237}
238
239/// cuSOLVER Hermitian eigensolver selection used by [`EighOptions`] on the
240/// CUDA backend.
241///
242/// The driver changes speed and rounding, not the decomposition contract, so
243/// gauges and AD rules are unaffected. CPU providers have a single eigh kernel
244/// and ignore it. Analogous to [`SvdDriver`] and to
245/// `scipy.linalg.eigh(..., driver=...)`.
246///
247/// # Examples
248///
249/// ```rust
250/// use tenferro_linalg::{EighDriver, EighOptions};
251///
252/// let options = EighOptions::default().driver(EighDriver::Syevj);
253/// assert_eq!(options.driver, EighDriver::Syevj);
254/// assert_eq!(EighOptions::default().driver, EighDriver::Auto);
255/// ```
256#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
257pub enum EighDriver {
258    /// The backend's default policy: the fastest divide-and-conquer routine
259    /// available. On CUDA a single matrix uses `syevd`/`heevd` and a batch
260    /// uses `cusolverDnXsyevBatched`, which solves the whole batch in one
261    /// launch at the same accuracy. On an A100 that is about 100x to 280x
262    /// faster than the per-matrix loop this default used before.
263    ///
264    /// The batched routine is a different cuSOLVER entry point, so batched
265    /// results can differ from the per-matrix loop in the last ULPs.
266    #[default]
267    Auto,
268    /// cuSOLVER's divide-and-conquer driver, which is what [`Self::Auto`]
269    /// already selects. Kept as an explicit spelling of the same choice.
270    Syevd,
271    /// cuSOLVER's Jacobi driver (`cusolverDn<t>syevj`, `heevj` for complex
272    /// input, and their batched forms).
273    ///
274    /// Worth asking for only on batches of small matrices: measured on an
275    /// A100 it is slightly ahead of the default up to about order 32 and
276    /// loses from there, by about 4x at order 128 and far more on wide
277    /// spectra, where it is also the less accurate of the two.
278    Syevj,
279}
280
281/// Options for Hermitian eigenvalue decomposition.
282///
283/// # Examples
284///
285/// ```rust
286/// use tenferro_linalg::{EighDriver, EighOptions};
287///
288/// let options = EighOptions::default()
289///     .derivative_eps(1.0e-10)
290///     .driver(EighDriver::Syevj);
291/// assert_eq!(options.derivative_eps, 1.0e-10);
292/// assert_eq!(options.driver, EighDriver::Syevj);
293/// ```
294#[derive(Clone, Copy, Debug, PartialEq)]
295pub struct EighOptions {
296    /// Eigenvector gauge convention.
297    pub gauge: EighGauge,
298    /// AD derivative regularization for repeated or nearly repeated eigenvalues.
299    pub derivative_eps: f64,
300    /// CUDA eigensolver driver; ignored by CPU providers.
301    pub driver: EighDriver,
302}
303
304impl Default for EighOptions {
305    fn default() -> Self {
306        Self {
307            gauge: EighGauge::Raw,
308            derivative_eps: DEFAULT_DECOMPOSITION_DERIVATIVE_EPS,
309            driver: EighDriver::Auto,
310        }
311    }
312}
313
314impl EighOptions {
315    /// Return options with the requested eigenvector gauge.
316    ///
317    /// # Examples
318    ///
319    /// ```rust
320    /// use tenferro_linalg::{EighGauge, EighOptions};
321    ///
322    /// let options = EighOptions::default().gauge(EighGauge::CanonicalPivot);
323    /// assert_eq!(options.gauge, EighGauge::CanonicalPivot);
324    /// ```
325    pub fn gauge(mut self, gauge: EighGauge) -> Self {
326        self.gauge = gauge;
327        self
328    }
329
330    /// Return options with an explicit derivative epsilon.
331    ///
332    /// # Examples
333    ///
334    /// ```rust
335    /// use tenferro_linalg::EighOptions;
336    ///
337    /// let options = EighOptions::default().derivative_eps(1.0e-9);
338    /// assert_eq!(options.derivative_eps, 1.0e-9);
339    /// ```
340    pub fn derivative_eps(mut self, derivative_eps: f64) -> Self {
341        self.derivative_eps = derivative_eps;
342        self
343    }
344
345    /// Return options with an explicit CUDA eigensolver driver.
346    ///
347    /// # Examples
348    ///
349    /// ```rust
350    /// use tenferro_linalg::{EighDriver, EighOptions};
351    ///
352    /// let options = EighOptions::default().driver(EighDriver::Syevd);
353    /// assert_eq!(options.driver, EighDriver::Syevd);
354    /// ```
355    pub fn driver(mut self, driver: EighDriver) -> Self {
356        self.driver = driver;
357        self
358    }
359}
360
361/// Options for QR decomposition.
362///
363/// # Examples
364///
365/// ```rust
366/// use tenferro_linalg::{QrGauge, QrOptions};
367///
368/// let options = QrOptions::default().gauge(QrGauge::PositiveDiagonal);
369/// assert_eq!(options.gauge, QrGauge::PositiveDiagonal);
370/// ```
371#[derive(Clone, Copy, Debug, PartialEq, Eq)]
372pub struct QrOptions {
373    /// QR sign or phase convention.
374    pub gauge: QrGauge,
375}
376
377impl Default for QrOptions {
378    fn default() -> Self {
379        Self {
380            gauge: QrGauge::Raw,
381        }
382    }
383}
384
385impl QrOptions {
386    /// Return options with the requested QR gauge.
387    ///
388    /// # Examples
389    ///
390    /// ```rust
391    /// use tenferro_linalg::{QrGauge, QrOptions};
392    ///
393    /// let options = QrOptions::default().gauge(QrGauge::PositiveDiagonal);
394    /// assert_eq!(options.gauge, QrGauge::PositiveDiagonal);
395    /// ```
396    pub fn gauge(mut self, gauge: QrGauge) -> Self {
397        self.gauge = gauge;
398        self
399    }
400}
401
402pub(crate) fn validate_derivative_eps(
403    op: &'static str,
404    derivative_eps: f64,
405) -> tenferro_tensor::Result<()> {
406    if derivative_eps.is_finite() && derivative_eps > 0.0 {
407        Ok(())
408    } else {
409        Err(Error::invalid_argument(
410            op,
411            "derivative_eps",
412            format!("must be positive and finite, got {derivative_eps}"),
413        ))
414    }
415}
416
417#[derive(Clone, Copy, Debug, PartialEq)]
418#[doc(hidden)]
419#[allow(dead_code)]
420pub(crate) enum LinalgOp {
421    Cholesky,
422    Lu,
423    LuFactor,
424    LuSolvePrepared {
425        transpose_a: bool,
426        conjugate_a: bool,
427    },
428    SignDetFromLuFactor,
429    LogAbsDetFromLuFactor,
430    FullPivLu,
431    FullPivLuSolve {
432        transpose_a: bool,
433    },
434    /// Solve `a @ x = b` with partial-pivot LU (same kernel as
435    /// `LinalgBackend::solve`). Two inputs (matrix, rhs) to one output.
436    /// The untracked eager surface constructs this variant.
437    Solve,
438    /// Fused partial-pivot solve that also returns its factors: inputs
439    /// `(a, b)`, outputs `(x, packed_lu, pivots)`, with the `LuFactor` packed
440    /// layout and 1-based LAPACK pivots.
441    ///
442    /// Tracked eager and traced `solve` emit this op so one backend call both
443    /// factors and solves while the factors stay available as AD residuals:
444    /// the tangent and the transpose solve reuse them through
445    /// [`LinalgOp::LuSolvePrepared`] instead of refactoring. It is never
446    /// pruned to [`LinalgOp::Solve`]; see `prune_outputs`.
447    LuFactorSolve,
448    Svd {
449        derivative_eps: f64,
450        gauge: SvdGauge,
451        driver: SvdDriver,
452    },
453    /// Full-matrices SVD: `U` is `m x m` and `Vh` is `n x n`, so the trailing
454    /// `Vh` rows span the input's right nullspace. Value-only: AD is
455    /// intentionally unsupported (see the linalg AD support manifest).
456    SvdFull,
457    /// Singular values only.
458    SvdVals {
459        derivative_eps: f64,
460        driver: SvdDriver,
461    },
462    Qr {
463        gauge: QrGauge,
464    },
465    RankRevealingQr {
466        gauge: QrGauge,
467        rtol: f64,
468        atol: f64,
469    },
470    HouseholderQrFactor,
471    HouseholderQrFromFactors,
472    HouseholderQrAppend,
473    HouseholderQrR {
474        gauge: QrGauge,
475    },
476    HouseholderQrQColumns {
477        start: usize,
478        end: usize,
479        gauge: QrGauge,
480    },
481    /// Internal AD residual operation for symbolic full thin-Q recovery.
482    HouseholderQrThinQ {
483        gauge: QrGauge,
484    },
485    /// Internal linear operation for abstract-state column append.
486    HouseholderQrAppendTangent,
487    /// Internal transpose operation that splits an appended state cotangent.
488    HouseholderQrSplitTangent {
489        right: bool,
490    },
491    Eigh {
492        derivative_eps: f64,
493        gauge: EighGauge,
494        driver: EighDriver,
495    },
496    /// Eigenvalues only.
497    EighVals {
498        derivative_eps: f64,
499        driver: EighDriver,
500    },
501    Eig {
502        input_dtype: DType,
503    },
504    EigVals {
505        input_dtype: DType,
506    },
507    TriangularSolve {
508        left_side: bool,
509        lower: bool,
510        transpose_a: bool,
511        unit_diagonal: bool,
512    },
513}
514
515impl LinalgOp {
516    fn output_count(self) -> usize {
517        match self {
518            Self::Cholesky
519            | Self::EighVals { .. }
520            | Self::EigVals { .. }
521            | Self::FullPivLuSolve { .. }
522            | Self::LogAbsDetFromLuFactor
523            | Self::LuSolvePrepared { .. }
524            | Self::SignDetFromLuFactor
525            | Self::Solve
526            | Self::SvdVals { .. }
527            | Self::TriangularSolve { .. } => 1,
528            Self::Svd { .. } | Self::SvdFull | Self::LuFactorSolve => 3,
529            Self::RankRevealingQr { .. } | Self::Lu => 4,
530            Self::Qr { .. }
531            | Self::HouseholderQrFactor
532            | Self::HouseholderQrFromFactors
533            | Self::HouseholderQrAppend
534            | Self::Eigh { .. }
535            | Self::Eig { .. } => 2,
536            Self::HouseholderQrR { .. }
537            | Self::HouseholderQrQColumns { .. }
538            | Self::HouseholderQrThinQ { .. }
539            | Self::HouseholderQrAppendTangent
540            | Self::HouseholderQrSplitTangent { .. } => 1,
541            Self::LuFactor => 3,
542            Self::FullPivLu => 5,
543        }
544    }
545
546    fn input_count(self) -> usize {
547        match self {
548            Self::FullPivLuSolve { .. }
549            | Self::Solve
550            | Self::LuFactorSolve
551            | Self::TriangularSolve { .. }
552            | Self::HouseholderQrFromFactors
553            | Self::HouseholderQrR { .. }
554            | Self::HouseholderQrQColumns { .. }
555            | Self::HouseholderQrThinQ { .. } => 2,
556            Self::LogAbsDetFromLuFactor => 2,
557            Self::SignDetFromLuFactor | Self::HouseholderQrAppend => 3,
558            Self::HouseholderQrAppendTangent => 4,
559            Self::HouseholderQrSplitTangent { .. } => 3,
560            Self::LuSolvePrepared { .. } => 4,
561            _ => 1,
562        }
563    }
564
565    fn tag(self) -> u8 {
566        match self {
567            Self::Cholesky => 0,
568            Self::Lu => 1,
569            Self::FullPivLu => 2,
570            Self::FullPivLuSolve { .. } => 3,
571            Self::Svd { .. } => 4,
572            Self::Qr { .. } => 5,
573            Self::Eigh { .. } => 6,
574            Self::Eig { .. } => 7,
575            Self::TriangularSolve { .. } => 9,
576            Self::LuFactor => 10,
577            Self::LuSolvePrepared { .. } => 11,
578            Self::SvdVals { .. } => 12,
579            Self::EighVals { .. } => 13,
580            Self::EigVals { .. } => 14,
581            Self::SvdFull => 15,
582            Self::LogAbsDetFromLuFactor => 16,
583            Self::SignDetFromLuFactor => 17,
584            Self::Solve => 18,
585            Self::HouseholderQrFactor => 19,
586            Self::HouseholderQrFromFactors => 20,
587            Self::HouseholderQrAppend => 21,
588            Self::HouseholderQrR { .. } => 22,
589            Self::HouseholderQrQColumns { .. } => 23,
590            Self::HouseholderQrThinQ { .. } => 24,
591            Self::HouseholderQrAppendTangent => 25,
592            Self::HouseholderQrSplitTangent { .. } => 26,
593            Self::RankRevealingQr { .. } => 27,
594            Self::LuFactorSolve => 28,
595        }
596    }
597}
598
599#[derive(Clone, Debug, PartialEq)]
600#[doc(hidden)]
601pub(crate) struct LinalgExtensionOp {
602    op: LinalgOp,
603}
604
605impl LinalgExtensionOp {
606    pub(crate) fn new(op: LinalgOp) -> Self {
607        Self { op }
608    }
609
610    pub(crate) fn op(&self) -> LinalgOp {
611        self.op
612    }
613}
614
615impl ExtensionOp for LinalgExtensionOp {
616    fn family_id(&self) -> &'static str {
617        LINALG_EXTENSION_FAMILY_ID
618    }
619
620    fn payload_hash(&self, hasher: &mut dyn Hasher) {
621        hasher.write_u8(self.op.tag());
622        match self.op {
623            LinalgOp::Svd {
624                derivative_eps,
625                gauge,
626                driver,
627            } => {
628                hasher.write_u64(derivative_eps.to_bits());
629                hash_svd_gauge(hasher, gauge);
630                hash_svd_driver(hasher, driver);
631            }
632            LinalgOp::SvdVals {
633                derivative_eps,
634                driver,
635            } => {
636                hasher.write_u64(derivative_eps.to_bits());
637                hash_svd_driver(hasher, driver);
638            }
639            LinalgOp::EighVals {
640                derivative_eps,
641                driver,
642            } => {
643                hasher.write_u64(derivative_eps.to_bits());
644                hash_eigh_driver(hasher, driver);
645            }
646            LinalgOp::Qr { gauge }
647            | LinalgOp::HouseholderQrR { gauge }
648            | LinalgOp::HouseholderQrThinQ { gauge } => {
649                hash_qr_gauge(hasher, gauge);
650            }
651            LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
652                hash_qr_gauge(hasher, gauge);
653                hasher.write_u64(rtol.to_bits());
654                hasher.write_u64(atol.to_bits());
655            }
656            LinalgOp::HouseholderQrQColumns { start, end, gauge } => {
657                hasher.write_usize(start);
658                hasher.write_usize(end);
659                hash_qr_gauge(hasher, gauge);
660            }
661            LinalgOp::Eigh {
662                derivative_eps,
663                gauge,
664                driver,
665            } => {
666                hasher.write_u64(derivative_eps.to_bits());
667                hash_eigh_gauge(hasher, gauge);
668                hash_eigh_driver(hasher, driver);
669            }
670            LinalgOp::Eig { input_dtype } | LinalgOp::EigVals { input_dtype } => {
671                hash_dtype(hasher, input_dtype);
672            }
673            LinalgOp::FullPivLuSolve { transpose_a }
674            | LinalgOp::HouseholderQrSplitTangent { right: transpose_a } => {
675                hasher.write_u8(u8::from(transpose_a));
676            }
677            LinalgOp::LuSolvePrepared {
678                transpose_a,
679                conjugate_a,
680            } => {
681                hasher.write_u8(u8::from(transpose_a));
682                hasher.write_u8(u8::from(conjugate_a));
683            }
684            LinalgOp::TriangularSolve {
685                left_side,
686                lower,
687                transpose_a,
688                unit_diagonal,
689            } => {
690                hasher.write_u8(u8::from(left_side));
691                hasher.write_u8(u8::from(lower));
692                hasher.write_u8(u8::from(transpose_a));
693                hasher.write_u8(u8::from(unit_diagonal));
694            }
695            LinalgOp::Cholesky
696            | LinalgOp::Lu
697            | LinalgOp::LuFactor
698            | LinalgOp::LogAbsDetFromLuFactor
699            | LinalgOp::SignDetFromLuFactor
700            | LinalgOp::FullPivLu
701            | LinalgOp::SvdFull
702            | LinalgOp::Solve
703            | LinalgOp::LuFactorSolve
704            | LinalgOp::HouseholderQrFactor
705            | LinalgOp::HouseholderQrFromFactors
706            | LinalgOp::HouseholderQrAppend
707            | LinalgOp::HouseholderQrAppendTangent => {}
708        }
709    }
710
711    fn payload_eq(&self, other: &dyn ExtensionOp) -> bool {
712        other
713            .as_any()
714            .downcast_ref::<Self>()
715            .is_some_and(|that| self == that)
716    }
717
718    fn clone_arc(&self) -> Arc<dyn ExtensionOp> {
719        Arc::new(self.clone())
720    }
721
722    fn as_any(&self) -> &dyn Any {
723        self
724    }
725
726    fn input_count(&self) -> usize {
727        self.op.input_count()
728    }
729
730    fn output_count(&self) -> usize {
731        self.op.output_count()
732    }
733
734    fn semantic_effects(&self) -> tenferro_ops::ext_op::ExtensionEffectDeclaration<'_> {
735        tenferro_ops::ext_op::ExtensionEffectDeclaration::Declared(&[])
736    }
737
738    fn semantic_aliases(&self) -> tenferro_ops::ext_op::ExtensionAliasDeclaration<'_> {
739        tenferro_ops::ext_op::ExtensionAliasDeclaration::AllFresh
740    }
741
742    fn prune_outputs(&self, live_outputs: &[bool]) -> Option<Arc<dyn ExtensionOp>> {
743        match self.op {
744            LinalgOp::Svd {
745                derivative_eps,
746                driver,
747                ..
748            } if live_outputs == [false, true, false] => {
749                Some(Arc::new(Self::new(LinalgOp::SvdVals {
750                    derivative_eps,
751                    driver,
752                })))
753            }
754            LinalgOp::Eigh {
755                derivative_eps,
756                driver,
757                ..
758            } if live_outputs == [true, false] => Some(Arc::new(Self::new(LinalgOp::EighVals {
759                derivative_eps,
760                driver,
761            }))),
762            LinalgOp::Eig { input_dtype } if live_outputs == [true, false] => {
763                Some(Arc::new(Self::new(LinalgOp::EigVals { input_dtype })))
764            }
765            // `LuFactorSolve` is deliberately never pruned to `Solve`. Traced
766            // AD compiles its source program with this pruning before
767            // differentiating, so a prune would drop the saved factors and
768            // force the adjoint solve to refactor `A`. A primal only program
769            // loses nothing by keeping the op: the fused CPU kernel does the
770            // `Solve` kernel's work, and the factor buffer the plain solve
771            // uses as scratch becomes the (pooled) LU output instead.
772            _ => None,
773        }
774    }
775
776    fn infer_output_meta(
777        &self,
778        ctx: &mut tenferro_ops::ExtensionShapeContext<'_>,
779    ) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
780        let input_dtypes = (0..self.input_count())
781            .map(|input| ctx.input_dtype(input))
782            .collect::<Result<Vec<_>, _>>()?;
783        let input_shapes = (0..self.input_count())
784            .map(|input| ctx.input_shape(input))
785            .collect::<Result<Vec<_>, _>>()?;
786        let metas = match self.op {
787            LinalgOp::Cholesky => {
788                require_matrix_meta("tenferro-linalg.cholesky", input_shapes[0])?;
789                vec![(promote_dtypes(&input_dtypes), input_shapes[0].to_vec())]
790            }
791            LinalgOp::FullPivLuSolve { .. } => {
792                require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[0])?;
793                require_matrix_meta("tenferro-linalg.full_piv_lu_solve", input_shapes[1])?;
794                vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
795            }
796            LinalgOp::Solve => {
797                require_matrix_meta("tenferro-linalg.solve", input_shapes[0])?;
798                require_matrix_meta("tenferro-linalg.solve", input_shapes[1])?;
799                vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
800            }
801            LinalgOp::LuFactorSolve => {
802                require_matrix_meta("tenferro-linalg.lu_factor_solve", input_shapes[1])?;
803                let mut factors = lu_factor_meta(input_dtypes[0], input_shapes[0])?.into_iter();
804                let (Some(packed_lu), Some(pivots)) = (factors.next(), factors.next()) else {
805                    return Err(Error::Internal(
806                        "lu_factor_solve: lu_factor metadata returned fewer than two outputs"
807                            .into(),
808                    ));
809                };
810                vec![
811                    (promote_dtypes(&input_dtypes), input_shapes[1].to_vec()),
812                    packed_lu,
813                    pivots,
814                ]
815            }
816            LinalgOp::TriangularSolve { .. } => {
817                require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[0])?;
818                require_matrix_meta("tenferro-linalg.triangular_solve", input_shapes[1])?;
819                vec![(promote_dtypes(&input_dtypes), input_shapes[1].to_vec())]
820            }
821            LinalgOp::LuSolvePrepared { .. } => {
822                require_matrix_meta("tenferro-linalg.lu_solve_prepared_lu", input_shapes[0])?;
823                require_matrix_meta("tenferro-linalg.lu_solve_prepared_rhs", input_shapes[3])?;
824                vec![(
825                    promote_dtypes(&[input_dtypes[0], input_dtypes[3]]),
826                    input_shapes[3].to_vec(),
827                )]
828            }
829            LinalgOp::Lu => lu_meta(input_dtypes[0], input_shapes[0])?,
830            LinalgOp::LuFactor => lu_factor_meta(input_dtypes[0], input_shapes[0])?,
831            LinalgOp::SignDetFromLuFactor => {
832                vec![signdet_from_lu_factor_meta(
833                    input_dtypes[0],
834                    input_shapes[0],
835                    input_shapes[1],
836                    input_shapes[2],
837                )?]
838            }
839            LinalgOp::LogAbsDetFromLuFactor => {
840                vec![logabsdet_from_lu_factor_meta(
841                    input_dtypes[0],
842                    input_shapes[0],
843                    input_shapes[1],
844                )?]
845            }
846            LinalgOp::FullPivLu => full_piv_lu_meta(input_dtypes[0], input_shapes[0])?,
847            LinalgOp::Svd { .. } => svd_meta(input_dtypes[0], input_shapes[0])?,
848            LinalgOp::SvdFull => svd_full_meta(input_dtypes[0], input_shapes[0])?,
849            LinalgOp::SvdVals { .. } => {
850                vec![svd_values_meta(input_dtypes[0], input_shapes[0])?]
851            }
852            LinalgOp::Qr { .. } => qr_meta(input_dtypes[0], input_shapes[0])?,
853            LinalgOp::RankRevealingQr { .. } => {
854                rank_revealing_qr_meta(input_dtypes[0], input_shapes[0])?
855            }
856            LinalgOp::HouseholderQrFactor => {
857                householder_qr_factor_meta(input_dtypes[0], input_shapes[0])?
858            }
859            LinalgOp::HouseholderQrFromFactors => {
860                householder_qr_from_factors_meta(&input_dtypes, &input_shapes)?
861            }
862            LinalgOp::HouseholderQrAppend => {
863                householder_qr_append_meta(&input_dtypes, &input_shapes)?
864            }
865            LinalgOp::HouseholderQrR { .. } => vec![householder_qr_r_meta(
866                &input_dtypes,
867                input_shapes[0],
868                input_shapes[1],
869            )?],
870            LinalgOp::HouseholderQrQColumns { start, end, .. } => {
871                vec![householder_qr_q_columns_meta(
872                    &input_dtypes,
873                    input_shapes[0],
874                    input_shapes[1],
875                    start,
876                    end,
877                )?]
878            }
879            LinalgOp::HouseholderQrThinQ { .. } => {
880                vec![householder_qr_thin_q_meta(
881                    &input_dtypes,
882                    input_shapes[0],
883                    input_shapes[1],
884                )?]
885            }
886            LinalgOp::HouseholderQrAppendTangent => {
887                vec![householder_qr_append_tangent_meta(
888                    &input_dtypes,
889                    &input_shapes,
890                )?]
891            }
892            LinalgOp::HouseholderQrSplitTangent { right } => {
893                vec![householder_qr_split_tangent_meta(
894                    &input_dtypes,
895                    &input_shapes,
896                    right,
897                )?]
898            }
899            LinalgOp::Eigh { .. } => eigh_meta(input_dtypes[0], input_shapes[0])?,
900            LinalgOp::EighVals { .. } => vec![eigh_values_meta(input_dtypes[0], input_shapes[0])?],
901            LinalgOp::Eig { input_dtype } => eig_meta(input_dtype, input_shapes[0])?,
902            LinalgOp::EigVals { input_dtype } => {
903                vec![eig_values_meta(input_dtype, input_shapes[0])?]
904            }
905        };
906        Ok(metas)
907    }
908}
909
910fn execute_linalg_extension_reads_on_session<B: Engine + ?Sized>(
911    op: &LinalgExtensionOp,
912    inputs: &[TensorRead<'_>],
913    session: &mut B,
914) -> tenferro_tensor::Result<Vec<Tensor>> {
915    if let Some(result) = with_cpu_engine(session, |session| {
916        execute_linalg_extension_reads_in_session(op, inputs, session)
917    }) {
918        return result;
919    }
920    #[cfg(feature = "cuda")]
921    if let Some(result) = with_cuda_engine(session, |session| {
922        execute_linalg_extension_reads_in_session(op, inputs, session)
923    }) {
924        return result;
925    }
926    Err(Error::unsupported(
927        "linalg_extension",
928        "selected backend session does not expose a linalg execution capability",
929    ))
930}
931
932fn execute_linalg_extension_reads_in_session<S: LinalgBackend>(
933    op: &LinalgExtensionOp,
934    inputs: &[TensorRead<'_>],
935    session: &mut S,
936) -> tenferro_tensor::Result<Vec<Tensor>> {
937    if op.op() == LinalgOp::HouseholderQrAppendTangent {
938        let left = session.to_contiguous_read(inputs[0].clone())?;
939        let right = session.to_contiguous_read(inputs[1].clone())?;
940        return Ok(vec![session.concatenate(&[&left, &right], 1)?]);
941    }
942    if let LinalgOp::HouseholderQrSplitTangent { right } = op.op() {
943        let cotangent_shape = inputs[0].clone().tensor_view().shape().to_vec();
944        let left_shape = inputs[1].clone().tensor_view().shape().to_vec();
945        let right_shape = inputs[2].clone().tensor_view().shape().to_vec();
946        let config =
947            householder_qr_split_config(&cotangent_shape, &left_shape, &right_shape, right)?;
948        let cotangent = session.to_contiguous_read(inputs[0].clone())?;
949        return Ok(vec![session.slice(&cotangent, &config)?]);
950    }
951    // Preserve borrowed layouts through the same provider hooks used by the
952    // concrete read API. In particular, CPU Faer can consume strided views;
953    // packing here would defeat that path before provider dispatch.
954    match op.op() {
955        LinalgOp::Cholesky => return Ok(vec![session.cholesky_read(inputs[0].clone())?]),
956        LinalgOp::Lu => return session.lu_read(inputs[0].clone()),
957        LinalgOp::FullPivLu => return session.full_piv_lu_read(inputs[0].clone()),
958        LinalgOp::Svd {
959            derivative_eps,
960            gauge,
961            driver,
962        } => {
963            return session.svd_with_options_read(
964                inputs[0].clone(),
965                SvdOptions {
966                    derivative_eps,
967                    gauge,
968                    driver,
969                },
970            );
971        }
972        LinalgOp::SvdFull => return session.svd_full_read(inputs[0].clone()),
973        LinalgOp::SvdVals { driver, .. } => {
974            return Ok(vec![
975                session.svd_values_with_driver_read(inputs[0].clone(), driver)?
976            ]);
977        }
978        LinalgOp::Qr { gauge } => {
979            return session.qr_with_options_read(inputs[0].clone(), QrOptions { gauge });
980        }
981        LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
982            return session.rank_revealing_qr_read(
983                inputs[0].clone(),
984                RankRevealingQrOptions { gauge, rtol, atol },
985            );
986        }
987        LinalgOp::Eigh {
988            derivative_eps,
989            gauge,
990            driver,
991        } => {
992            return session.eigh_with_options_read(
993                inputs[0].clone(),
994                EighOptions {
995                    derivative_eps,
996                    gauge,
997                    driver,
998                },
999            );
1000        }
1001        LinalgOp::EighVals { driver, .. } => {
1002            return Ok(vec![
1003                session.eigh_values_with_driver_read(inputs[0].clone(), driver)?
1004            ]);
1005        }
1006        LinalgOp::Eig { .. } => return session.eig_read(inputs[0].clone()),
1007        LinalgOp::EigVals { .. } => return Ok(vec![session.eig_values_read(inputs[0].clone())?]),
1008        LinalgOp::Solve => match session.solve_read(inputs[0].clone(), inputs[1].clone()) {
1009            Ok(output) => return Ok(vec![output]),
1010            Err(error) if error.kind() == ErrorKind::Unsupported => {}
1011            Err(error) => return Err(error),
1012        },
1013        _ => {}
1014    }
1015    if let LinalgOp::TriangularSolve {
1016        left_side,
1017        lower,
1018        transpose_a,
1019        unit_diagonal,
1020    } = op.op()
1021    {
1022        match session.triangular_solve_read(
1023            inputs[0].clone(),
1024            inputs[1].clone(),
1025            left_side,
1026            lower,
1027            transpose_a,
1028            unit_diagonal,
1029        ) {
1030            Ok(output) => return Ok(vec![output]),
1031            Err(error) if error.kind() == ErrorKind::Unsupported => {}
1032            Err(error) => return Err(error),
1033        }
1034    }
1035
1036    // The owned-only hooks need materialized views, not copies of inputs that
1037    // are already owned tensors (notably the saved matrix and LU in backward).
1038    let materialized_inputs = inputs
1039        .iter()
1040        .filter(|input| input.as_tensor().is_none())
1041        .cloned()
1042        .map(|input| session.to_contiguous_read(input))
1043        .collect::<tenferro_tensor::Result<Vec<_>>>()?;
1044    let mut views = materialized_inputs.iter();
1045    // INVARIANT: exactly one materialized tensor was produced per View, in
1046    // input order; every input therefore contributes exactly one reference.
1047    let input_refs: Vec<&Tensor> = inputs
1048        .iter()
1049        .filter_map(|input| input.as_tensor().or_else(|| views.next()))
1050        .collect();
1051    execute_linalg(op.op(), &input_refs, session)
1052}
1053
1054fn linalg_session_supported<B: Engine + 'static>(
1055    #[cfg_attr(not(feature = "cuda"), allow(unused_variables))] op: &LinalgExtensionOp,
1056) -> bool {
1057    // The `supports_session` contract (capability.rs) requires that an op is
1058    // admitted to a scheduler session only when the session executor genuinely
1059    // executes it without returning `Unsupported`. Admission is exactly
1060    // per-op/per-backend so `apply_eager` keeps the native prepared path for
1061    // every op the session can actually run (issue #1665).
1062    let type_id = std::any::TypeId::of::<B>();
1063    if type_id == std::any::TypeId::of::<tenferro_cpu::CpuEngine>() {
1064        // Every CPU linalg kernel, including full-matrices SVD, runs in-session
1065        // on both the faer and the BLAS provider, so the type-only seam can
1066        // admit the whole family without inspecting the provider kind.
1067        return true;
1068    }
1069    #[cfg(feature = "cuda")]
1070    {
1071        if type_id == std::any::TypeId::of::<tenferro_gpu::cuda::CudaEngine>() {
1072            return match op.op() {
1073                // Complete-pivoting LU and general eig have no CUDA kernels.
1074                LinalgOp::FullPivLu | LinalgOp::FullPivLuSolve { .. } => false,
1075                LinalgOp::Eig { .. } | LinalgOp::EigVals { .. } => false,
1076                // Plain partial-pivot solve runs in-session via cuSOLVER
1077                // getrf plus prepared pivot/triangular solves
1078                // (`gpu/linalg.rs::solve` = lu_factor + lu_solve_prepared, no
1079                // Unsupported path for F32/F64/C32/C64), so it is admitted.
1080                LinalgOp::Solve => true,
1081                // The fused solve runs the trait default on CUDA: getrf then
1082                // the plain prepared solve, both admitted above.
1083                LinalgOp::LuFactorSolve => true,
1084                // Conjugate-only prepared LU solve is unsupported on CUDA.
1085                LinalgOp::LuSolvePrepared {
1086                    transpose_a: false,
1087                    conjugate_a: true,
1088                } => false,
1089                _ => true,
1090            };
1091        }
1092    }
1093    false
1094}
1095
1096fn execute_linalg_extension_in_session(
1097    op: &LinalgExtensionOp,
1098    session: &mut dyn Engine,
1099    _extension_caches: &mut tenferro_runtime::ExtensionCacheStore,
1100    inputs: &[TensorRead<'_>],
1101) -> tenferro_tensor::Result<Vec<Tensor>> {
1102    // Reuse the existing session executor that the eager and scheduler paths
1103    // already share; it downcasts the borrowed session to the CPU/CUDA exec
1104    // session and runs the same forward kernel for every LinalgOp.
1105    execute_linalg_extension_reads_on_session(op, inputs, session)
1106}
1107
1108define_extension_runtime! {
1109    runtime = LinalgRuntime,
1110    family_id = LINALG_EXTENSION_FAMILY_ID,
1111    op_type = LinalgExtensionOp,
1112    execute_in_session = execute_linalg_extension_in_session,
1113    session_supported = linalg_session_supported,
1114    backend_bound = Engine,
1115}
1116
1117fn execute_linalg<B: LinalgBackend>(
1118    op: LinalgOp,
1119    inputs: &[&Tensor],
1120    backend: &mut B,
1121) -> tenferro_tensor::Result<Vec<Tensor>> {
1122    match op {
1123        LinalgOp::Cholesky => Ok(vec![backend.cholesky(inputs[0])?]),
1124        LinalgOp::Lu => backend.lu(inputs[0]),
1125        LinalgOp::LuFactor => backend.lu_factor(inputs[0]),
1126        LinalgOp::SignDetFromLuFactor => {
1127            Ok(vec![signdet_from_lu_factor(inputs[1], inputs[2], backend)?])
1128        }
1129        LinalgOp::LogAbsDetFromLuFactor => Ok(vec![logabsdet_from_lu_factor(inputs[1], backend)?]),
1130        LinalgOp::LuSolvePrepared {
1131            transpose_a,
1132            conjugate_a,
1133        } => Ok(vec![backend.lu_solve_prepared(
1134            inputs[0],
1135            inputs[1],
1136            inputs[2],
1137            inputs[3],
1138            transpose_a,
1139            conjugate_a,
1140        )?]),
1141        LinalgOp::FullPivLu => backend.full_piv_lu(inputs[0]),
1142        LinalgOp::FullPivLuSolve { transpose_a } => Ok(vec![backend.full_piv_lu_solve(
1143            inputs[0],
1144            inputs[1],
1145            transpose_a,
1146        )?]),
1147        LinalgOp::Solve => Ok(vec![backend.solve(inputs[0], inputs[1])?]),
1148        LinalgOp::LuFactorSolve => backend.lu_factor_solve(inputs[0], inputs[1]),
1149        LinalgOp::Svd {
1150            derivative_eps,
1151            gauge,
1152            driver,
1153        } => backend.svd_with_options(
1154            inputs[0],
1155            SvdOptions {
1156                derivative_eps,
1157                gauge,
1158                driver,
1159            },
1160        ),
1161        LinalgOp::SvdFull => backend.svd_full(inputs[0]),
1162        LinalgOp::SvdVals { driver, .. } => {
1163            Ok(vec![backend.svd_values_with_driver(inputs[0], driver)?])
1164        }
1165        LinalgOp::Qr { gauge } => backend.qr_with_options(inputs[0], QrOptions { gauge }),
1166        LinalgOp::RankRevealingQr { gauge, rtol, atol } => {
1167            backend.rank_revealing_qr(inputs[0], RankRevealingQrOptions { gauge, rtol, atol })
1168        }
1169        LinalgOp::HouseholderQrFactor => {
1170            let state = backend.householder_qr(inputs[0])?;
1171            Ok(vec![state.packed, state.coeff])
1172        }
1173        LinalgOp::HouseholderQrFromFactors => {
1174            let state = backend.householder_qr_from_factors(inputs[0], inputs[1])?;
1175            Ok(vec![state.packed, state.coeff])
1176        }
1177        LinalgOp::HouseholderQrAppend => {
1178            let state = backend.householder_qr_append(inputs[0], inputs[1], inputs[2])?;
1179            Ok(vec![state.packed, state.coeff])
1180        }
1181        LinalgOp::HouseholderQrR { gauge } => Ok(vec![backend.householder_qr_r(
1182            inputs[0],
1183            inputs[1],
1184            QrOptions { gauge },
1185        )?]),
1186        LinalgOp::HouseholderQrQColumns { start, end, gauge } => Ok(vec![backend
1187            .householder_qr_q_columns(inputs[0], inputs[1], start..end, QrOptions { gauge })?]),
1188        LinalgOp::HouseholderQrThinQ { gauge } => {
1189            let end = inputs[1].shape().first().copied().ok_or_else(|| {
1190                Error::rank_mismatch("tenferro-linalg.householder_qr_thin_q", 1, 0)
1191            })?;
1192            Ok(vec![backend.householder_qr_q_columns(
1193                inputs[0],
1194                inputs[1],
1195                0..end,
1196                QrOptions { gauge },
1197            )?])
1198        }
1199        LinalgOp::HouseholderQrAppendTangent => {
1200            Ok(vec![backend.concatenate(&[inputs[0], inputs[1]], 1)?])
1201        }
1202        LinalgOp::HouseholderQrSplitTangent { right } => {
1203            let config = householder_qr_split_config(
1204                inputs[0].shape(),
1205                inputs[1].shape(),
1206                inputs[2].shape(),
1207                right,
1208            )?;
1209            Ok(vec![backend.slice(inputs[0], &config)?])
1210        }
1211        LinalgOp::Eigh {
1212            derivative_eps,
1213            gauge,
1214            driver,
1215        } => backend.eigh_with_options(
1216            inputs[0],
1217            EighOptions {
1218                derivative_eps,
1219                gauge,
1220                driver,
1221            },
1222        ),
1223        LinalgOp::EighVals { driver, .. } => {
1224            Ok(vec![backend.eigh_values_with_driver(inputs[0], driver)?])
1225        }
1226        LinalgOp::Eig { .. } => backend.eig(inputs[0]),
1227        LinalgOp::EigVals { .. } => Ok(vec![backend.eig_values(inputs[0])?]),
1228        LinalgOp::TriangularSolve {
1229            left_side,
1230            lower,
1231            transpose_a,
1232            unit_diagonal,
1233        } => Ok(vec![backend.triangular_solve(
1234            inputs[0],
1235            inputs[1],
1236            left_side,
1237            lower,
1238            transpose_a,
1239            unit_diagonal,
1240        )?]),
1241    }
1242}
1243
1244/// Sign (or complex phase) of the determinant from an LU factorization.
1245///
1246/// The sign is built from the per-pivot signs rather than from the determinant
1247/// product, so it is magnitude-independent: a determinant whose product would
1248/// underflow or overflow still reports its mathematical sign. `Sign` maps an
1249/// exactly zero pivot to zero, so finite singular LU factors report zero sign
1250/// for both real and complex inputs, matching the existing eager composite.
1251fn signdet_from_lu_factor<B: LinalgBackend + ?Sized>(
1252    packed_lu: &Tensor,
1253    parity: &Tensor,
1254    backend: &mut B,
1255) -> tenferro_tensor::Result<Tensor> {
1256    let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
1257    let sign_diag = backend.sign_read(TensorRead::from_tensor(&diag))?;
1258    let sign_u = backend.reduce_prod_read(TensorRead::from_tensor(&sign_diag), &[0])?;
1259    backend.mul_read(
1260        TensorRead::from_tensor(parity),
1261        TensorRead::from_tensor(&sign_u),
1262    )
1263}
1264
1265fn logabsdet_from_lu_factor<B: LinalgBackend + ?Sized>(
1266    packed_lu: &Tensor,
1267    backend: &mut B,
1268) -> tenferro_tensor::Result<Tensor> {
1269    let diag = backend.extract_diagonal(packed_lu, 0, 1)?;
1270    let abs = backend.abs_read(TensorRead::from_tensor(&diag))?;
1271    let log = backend.log_read(TensorRead::from_tensor(&abs))?;
1272    backend.reduce_sum_read(TensorRead::from_tensor(&log), &[0])
1273}
1274
1275pub(crate) fn apply_svd_gauge(
1276    gauge: SvdGauge,
1277    outputs: &mut [Tensor],
1278) -> tenferro_tensor::Result<()> {
1279    match gauge {
1280        SvdGauge::Raw => Ok(()),
1281        SvdGauge::CanonicalPivot => apply_canonical_pivot_svd_gauge(outputs),
1282    }
1283}
1284
1285fn apply_canonical_pivot_svd_gauge(outputs: &mut [Tensor]) -> tenferro_tensor::Result<()> {
1286    if outputs.len() != 3 {
1287        return Err(Error::invalid_argument(
1288            "tenferro-linalg.svd",
1289            "outputs",
1290            format!(
1291                "canonical SVD gauge expected three outputs, got {}",
1292                outputs.len()
1293            ),
1294        ));
1295    }
1296
1297    let (u_slice, rest) = outputs.split_at_mut(1);
1298    let (singular_slice, vt_slice) = rest.split_at_mut(1);
1299    let u = &mut u_slice[0];
1300    let singular_values = &singular_slice[0];
1301    let vt = &mut vt_slice[0];
1302    let u_shape = u.shape().to_vec();
1303    let s_shape = singular_values.shape().to_vec();
1304    let vt_shape = vt.shape().to_vec();
1305    if u_shape.len() < 2 || vt_shape.len() < 2 || s_shape.is_empty() {
1306        return Err(Error::invalid_argument(
1307            "tenferro-linalg.svd",
1308            "outputs",
1309            format!(
1310                "canonical SVD gauge expected U rank >= 2, S rank >= 1, VT rank >= 2; got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
1311            ),
1312        ));
1313    }
1314
1315    let m = u_shape[0];
1316    let k = u_shape[1];
1317    let n = vt_shape[1];
1318    if s_shape[0] != k
1319        || vt_shape[0] != k
1320        || u_shape[2..] != vt_shape[2..]
1321        || s_shape[1..] != u_shape[2..]
1322    {
1323        return Err(Error::invalid_argument(
1324            "tenferro-linalg.svd",
1325            "outputs",
1326            format!(
1327                "canonical SVD gauge expected compatible compact SVD shapes, got U={u_shape:?}, S={s_shape:?}, VT={vt_shape:?}"
1328            ),
1329        ));
1330    }
1331    let layout = canonical_svd_gauge_layout(m, k, n, &u_shape[2..])?;
1332
1333    match (u.dtype(), vt.dtype()) {
1334        (tenferro_tensor::DType::F64, tenferro_tensor::DType::F64) => {
1335            let (u, vt) = svd_gauge_pair_mut::<f64>(u, vt)?;
1336            canonicalize_svd_gauge_f64(u.host_data_mut()?, vt.host_data_mut()?, layout)
1337        }
1338        (tenferro_tensor::DType::F32, tenferro_tensor::DType::F32) => {
1339            let (u, vt) = svd_gauge_pair_mut::<f32>(u, vt)?;
1340            canonicalize_svd_gauge_f32(u.host_data_mut()?, vt.host_data_mut()?, layout)
1341        }
1342        (tenferro_tensor::DType::C64, tenferro_tensor::DType::C64) => {
1343            let (u, vt) = svd_gauge_pair_mut::<Complex64>(u, vt)?;
1344            canonicalize_svd_gauge_c64(u.host_data_mut()?, vt.host_data_mut()?, layout)
1345        }
1346        (tenferro_tensor::DType::C32, tenferro_tensor::DType::C32) => {
1347            let (u, vt) = svd_gauge_pair_mut::<Complex32>(u, vt)?;
1348            canonicalize_svd_gauge_c32(u.host_data_mut()?, vt.host_data_mut()?, layout)
1349        }
1350        (u_dtype, vt_dtype) => Err(Error::dtype_mismatch(
1351            "tenferro-linalg.svd",
1352            u_dtype,
1353            vt_dtype,
1354        )),
1355    }
1356}
1357
1358/// The typed pair behind a same-dtype pair of mutable tensors, or this module's refusal.
1359fn svd_gauge_pair_mut<'a, T: tenferro_tensor::TensorScalar>(
1360    u: &'a mut Tensor,
1361    vt: &'a mut Tensor,
1362) -> tenferro_tensor::Result<(
1363    &'a mut tenferro_tensor::TypedTensor<T>,
1364    &'a mut tenferro_tensor::TypedTensor<T>,
1365)> {
1366    let (u_dtype, vt_dtype) = (u.dtype(), vt.dtype());
1367    let u_t = u
1368        .as_typed_mut::<T>()
1369        .ok_or_else(|| Error::dtype_mismatch("tenferro-linalg.svd", u_dtype, vt_dtype))?;
1370    let vt_t = vt
1371        .as_typed_mut::<T>()
1372        .ok_or_else(|| Error::dtype_mismatch("tenferro-linalg.svd", u_dtype, vt_dtype))?;
1373    Ok((u_t, vt_t))
1374}
1375
1376#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1377struct CanonicalSvdGaugeLayout {
1378    m: usize,
1379    k: usize,
1380    batch_count: usize,
1381    u_batch_len: usize,
1382    vt_batch_len: usize,
1383    u_len: usize,
1384    vt_len: usize,
1385}
1386
1387impl CanonicalSvdGaugeLayout {
1388    fn validate_storage(self, u_len: usize, vt_len: usize) -> tenferro_tensor::Result<()> {
1389        if u_len != self.u_len {
1390            return Err(Error::invalid_argument(
1391                "tenferro-linalg.svd",
1392                "U storage",
1393                format!(
1394                    "canonical SVD gauge expected U storage length {}, got {u_len}",
1395                    self.u_len
1396                ),
1397            ));
1398        }
1399        if vt_len != self.vt_len {
1400            return Err(Error::invalid_argument(
1401                "tenferro-linalg.svd",
1402                "VT storage",
1403                format!(
1404                    "canonical SVD gauge expected VT storage length {}, got {vt_len}",
1405                    self.vt_len
1406                ),
1407            ));
1408        }
1409        Ok(())
1410    }
1411}
1412
1413fn canonical_svd_gauge_layout(
1414    m: usize,
1415    k: usize,
1416    n: usize,
1417    batch_shape: &[usize],
1418) -> tenferro_tensor::Result<CanonicalSvdGaugeLayout> {
1419    let batch_count = tenferro_tensor::validate::checked_shape_product(
1420        "tenferro-linalg.svd",
1421        "canonical SVD batch",
1422        batch_shape,
1423    )?;
1424    let u_batch_len = tenferro_tensor::validate::checked_shape_product(
1425        "tenferro-linalg.svd",
1426        "canonical SVD U batch",
1427        &[m, k],
1428    )?;
1429    let vt_batch_len = tenferro_tensor::validate::checked_shape_product(
1430        "tenferro-linalg.svd",
1431        "canonical SVD VT batch",
1432        &[k, n],
1433    )?;
1434    let u_len = tenferro_tensor::validate::checked_shape_product(
1435        "tenferro-linalg.svd",
1436        "canonical SVD U storage",
1437        &[u_batch_len, batch_count],
1438    )?;
1439    let vt_len = tenferro_tensor::validate::checked_shape_product(
1440        "tenferro-linalg.svd",
1441        "canonical SVD VT storage",
1442        &[vt_batch_len, batch_count],
1443    )?;
1444    Ok(CanonicalSvdGaugeLayout {
1445        m,
1446        k,
1447        batch_count,
1448        u_batch_len,
1449        vt_batch_len,
1450        u_len,
1451        vt_len,
1452    })
1453}
1454
1455fn canonicalize_svd_gauge_f64(
1456    u: &mut [f64],
1457    vt: &mut [f64],
1458    layout: CanonicalSvdGaugeLayout,
1459) -> tenferro_tensor::Result<()> {
1460    layout.validate_storage(u.len(), vt.len())?;
1461    if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1462        return Ok(());
1463    }
1464    for (u_batch, vt_batch) in u
1465        .chunks_exact_mut(layout.u_batch_len)
1466        .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1467    {
1468        for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1469            let pivot = max_abs_pivot_f64(u_column);
1470            let pivot_value = u_column[pivot];
1471            if pivot_value < 0.0 {
1472                for value in u_column {
1473                    *value = -*value;
1474                }
1475                for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1476                    vt_column[col] = -vt_column[col];
1477                }
1478            }
1479        }
1480    }
1481    Ok(())
1482}
1483
1484fn canonicalize_svd_gauge_f32(
1485    u: &mut [f32],
1486    vt: &mut [f32],
1487    layout: CanonicalSvdGaugeLayout,
1488) -> tenferro_tensor::Result<()> {
1489    layout.validate_storage(u.len(), vt.len())?;
1490    if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1491        return Ok(());
1492    }
1493    for (u_batch, vt_batch) in u
1494        .chunks_exact_mut(layout.u_batch_len)
1495        .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1496    {
1497        for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1498            let pivot = max_abs_pivot_f32(u_column);
1499            let pivot_value = u_column[pivot];
1500            if pivot_value < 0.0 {
1501                for value in u_column {
1502                    *value = -*value;
1503                }
1504                for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1505                    vt_column[col] = -vt_column[col];
1506                }
1507            }
1508        }
1509    }
1510    Ok(())
1511}
1512
1513fn canonicalize_svd_gauge_c64(
1514    u: &mut [Complex64],
1515    vt: &mut [Complex64],
1516    layout: CanonicalSvdGaugeLayout,
1517) -> tenferro_tensor::Result<()> {
1518    layout.validate_storage(u.len(), vt.len())?;
1519    if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1520        return Ok(());
1521    }
1522    for (u_batch, vt_batch) in u
1523        .chunks_exact_mut(layout.u_batch_len)
1524        .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1525    {
1526        for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1527            let pivot = max_abs_pivot_c64(u_column);
1528            let pivot_value = u_column[pivot];
1529            let pivot_norm = pivot_value.norm();
1530            if pivot_norm == 0.0 {
1531                continue;
1532            }
1533            let phase = pivot_value.conj() / pivot_norm;
1534            let vt_phase = phase.conj();
1535            for value in u_column {
1536                *value *= phase;
1537            }
1538            for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1539                vt_column[col] *= vt_phase;
1540            }
1541        }
1542    }
1543    Ok(())
1544}
1545
1546fn canonicalize_svd_gauge_c32(
1547    u: &mut [Complex32],
1548    vt: &mut [Complex32],
1549    layout: CanonicalSvdGaugeLayout,
1550) -> tenferro_tensor::Result<()> {
1551    layout.validate_storage(u.len(), vt.len())?;
1552    if layout.batch_count == 0 || layout.u_batch_len == 0 || layout.vt_batch_len == 0 {
1553        return Ok(());
1554    }
1555    for (u_batch, vt_batch) in u
1556        .chunks_exact_mut(layout.u_batch_len)
1557        .zip(vt.chunks_exact_mut(layout.vt_batch_len))
1558    {
1559        for (col, u_column) in u_batch.chunks_exact_mut(layout.m).enumerate() {
1560            let pivot = max_abs_pivot_c32(u_column);
1561            let pivot_value = u_column[pivot];
1562            let pivot_norm = pivot_value.norm();
1563            if pivot_norm == 0.0 {
1564                continue;
1565            }
1566            let phase = pivot_value.conj() / pivot_norm;
1567            let vt_phase = phase.conj();
1568            for value in u_column {
1569                *value *= phase;
1570            }
1571            for vt_column in vt_batch.chunks_exact_mut(layout.k) {
1572                vt_column[col] *= vt_phase;
1573            }
1574        }
1575    }
1576    Ok(())
1577}
1578
1579fn max_abs_pivot_f64(u_column: &[f64]) -> usize {
1580    let mut pivot = 0;
1581    let mut pivot_abs = u_column[0].abs();
1582    for (row, value) in u_column.iter().enumerate().skip(1) {
1583        let candidate_abs = value.abs();
1584        if candidate_abs > pivot_abs {
1585            pivot = row;
1586            pivot_abs = candidate_abs;
1587        }
1588    }
1589    pivot
1590}
1591
1592fn max_abs_pivot_f32(u_column: &[f32]) -> usize {
1593    let mut pivot = 0;
1594    let mut pivot_abs = u_column[0].abs();
1595    for (row, value) in u_column.iter().enumerate().skip(1) {
1596        let candidate_abs = value.abs();
1597        if candidate_abs > pivot_abs {
1598            pivot = row;
1599            pivot_abs = candidate_abs;
1600        }
1601    }
1602    pivot
1603}
1604
1605fn max_abs_pivot_c64(u_column: &[Complex64]) -> usize {
1606    let mut pivot = 0;
1607    let mut pivot_abs = u_column[0].norm_sqr();
1608    for (row, value) in u_column.iter().enumerate().skip(1) {
1609        let candidate_abs = value.norm_sqr();
1610        if candidate_abs > pivot_abs {
1611            pivot = row;
1612            pivot_abs = candidate_abs;
1613        }
1614    }
1615    pivot
1616}
1617
1618fn max_abs_pivot_c32(u_column: &[Complex32]) -> usize {
1619    let mut pivot = 0;
1620    let mut pivot_abs = u_column[0].norm_sqr();
1621    for (row, value) in u_column.iter().enumerate().skip(1) {
1622        let candidate_abs = value.norm_sqr();
1623        if candidate_abs > pivot_abs {
1624            pivot = row;
1625            pivot_abs = candidate_abs;
1626        }
1627    }
1628    pivot
1629}
1630
1631fn require_matrix_meta(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1632    if shape.len() < 2 {
1633        return Err(Error::rank_mismatch(op, 2, shape.len()));
1634    }
1635    Ok(())
1636}
1637
1638fn matrix_meta_parts<'a>(
1639    op: &'static str,
1640    shape: &'a [SymDim],
1641) -> tenferro_tensor::Result<(SymDim, SymDim, &'a [SymDim])> {
1642    require_matrix_meta(op, shape)?;
1643    Ok((shape[0].clone(), shape[1].clone(), &shape[2..]))
1644}
1645
1646fn lu_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1647    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu", shape)?;
1648    let k = m.clone().min(n.clone());
1649    Ok(vec![
1650        (dtype, matrix_shape(m.clone(), m, batch)),
1651        (dtype, matrix_shape(shape[0].clone(), k.clone(), batch)),
1652        (dtype, matrix_shape(k, n, batch)),
1653        (dtype, batch.to_vec()),
1654    ])
1655}
1656
1657fn lu_factor_meta(
1658    dtype: DType,
1659    shape: &[SymDim],
1660) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1661    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.lu_factor", shape)?;
1662    let k = m.min(n);
1663    Ok(vec![
1664        (dtype, shape.to_vec()),
1665        (DType::I32, vector_shape(k, batch)),
1666        (dtype, batch.to_vec()),
1667    ])
1668}
1669
1670fn signdet_from_lu_factor_meta(
1671    input_dtype: DType,
1672    input_shape: &[SymDim],
1673    packed_shape: &[SymDim],
1674    parity_shape: &[SymDim],
1675) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1676    let (_, _, batch) = matrix_meta_parts("tenferro-linalg.signdet_from_lu_factor", input_shape)?;
1677    require_matrix_meta(
1678        "tenferro-linalg.signdet_from_lu_factor_packed",
1679        packed_shape,
1680    )?;
1681    if parity_shape.len() != batch.len() {
1682        return Err(Error::rank_mismatch(
1683            "tenferro-linalg.signdet_from_lu_factor_parity",
1684            batch.len(),
1685            parity_shape.len(),
1686        ));
1687    }
1688    Ok((input_dtype, batch.to_vec()))
1689}
1690
1691fn logabsdet_from_lu_factor_meta(
1692    input_dtype: DType,
1693    input_shape: &[SymDim],
1694    packed_shape: &[SymDim],
1695) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1696    let (_, _, batch) = matrix_meta_parts("tenferro-linalg.logabsdet_from_lu_factor", input_shape)?;
1697    require_matrix_meta(
1698        "tenferro-linalg.logabsdet_from_lu_factor_packed",
1699        packed_shape,
1700    )?;
1701    Ok((singular_values_dtype(input_dtype), batch.to_vec()))
1702}
1703
1704fn full_piv_lu_meta(
1705    dtype: DType,
1706    shape: &[SymDim],
1707) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1708    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.full_piv_lu", shape)?;
1709    Ok(vec![
1710        (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1711        (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1712        (dtype, matrix_shape(n.clone(), n.clone(), batch)),
1713        (dtype, matrix_shape(n.clone(), n, batch)),
1714        (singular_values_dtype(dtype), batch.to_vec()),
1715    ])
1716}
1717
1718fn svd_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1719    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd", shape)?;
1720    let k = m.clone().min(n.clone());
1721    Ok(vec![
1722        (dtype, matrix_shape(m, k.clone(), batch)),
1723        (singular_values_dtype(dtype), vector_shape(k.clone(), batch)),
1724        (dtype, matrix_shape(k, n, batch)),
1725    ])
1726}
1727
1728fn svd_full_meta(
1729    dtype: DType,
1730    shape: &[SymDim],
1731) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1732    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_full", shape)?;
1733    let k = m.clone().min(n.clone());
1734    Ok(vec![
1735        (dtype, matrix_shape(m.clone(), m, batch)),
1736        (singular_values_dtype(dtype), vector_shape(k, batch)),
1737        (dtype, matrix_shape(n.clone(), n, batch)),
1738    ])
1739}
1740
1741fn svd_values_meta(
1742    dtype: DType,
1743    shape: &[SymDim],
1744) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1745    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.svd_values", shape)?;
1746    let k = m.min(n);
1747    Ok((singular_values_dtype(dtype), vector_shape(k, batch)))
1748}
1749
1750fn qr_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1751    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.qr", shape)?;
1752    let k = m.clone().min(n.clone());
1753    Ok(vec![
1754        (dtype, matrix_shape(m, k.clone(), batch)),
1755        (dtype, matrix_shape(k, n, batch)),
1756    ])
1757}
1758
1759fn rank_revealing_qr_meta(
1760    dtype: DType,
1761    shape: &[SymDim],
1762) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1763    let (m, n, batch) = matrix_meta_parts("tenferro-linalg.rank_revealing_qr", shape)?;
1764    let k = m.clone().min(n.clone());
1765    Ok(vec![
1766        (dtype, matrix_shape(m, k.clone(), batch)),
1767        (dtype, matrix_shape(k, n.clone(), batch)),
1768        (DType::I64, vector_shape(n, batch)),
1769        (DType::I64, batch.to_vec()),
1770    ])
1771}
1772
1773fn require_householder_rank2(op: &'static str, shape: &[SymDim]) -> tenferro_tensor::Result<()> {
1774    if shape.len() != 2 {
1775        return Err(Error::rank_mismatch(op, 2, shape.len()));
1776    }
1777    Ok(())
1778}
1779
1780fn householder_qr_factor_meta(
1781    dtype: DType,
1782    shape: &[SymDim],
1783) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1784    require_householder_rank2("tenferro-linalg.householder_qr", shape)?;
1785    let k = shape[0].clone().min(shape[1].clone());
1786    Ok(vec![(dtype, shape.to_vec()), (dtype, vec![k])])
1787}
1788
1789fn householder_qr_from_factors_meta(
1790    dtypes: &[DType],
1791    shapes: &[&[SymDim]],
1792) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1793    const OP: &str = "tenferro-linalg.householder_qr_from_factors";
1794    require_householder_rank2(OP, shapes[0])?;
1795    require_householder_rank2(OP, shapes[1])?;
1796    if dtypes[0] != dtypes[1] {
1797        return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1798    }
1799    require_static_extent_equal(OP, "q.cols/r.rows", &shapes[0][1], &shapes[1][0])?;
1800    if let (Some(q_cols), Some(q_rows), Some(r_cols)) = (
1801        shapes[0][1].constant_value(),
1802        shapes[0][0].constant_value(),
1803        shapes[1][1].constant_value(),
1804    ) && q_cols > q_rows.min(r_cols)
1805    {
1806        return Err(Error::invalid_argument(
1807            OP,
1808            "shape",
1809            "Q column count exceeds min(Q rows, R columns)",
1810        ));
1811    }
1812    let m = shapes[0][0].clone();
1813    let n = shapes[1][1].clone();
1814    let k = m.clone().min(n.clone());
1815    Ok(vec![(dtypes[0], vec![m, n]), (dtypes[0], vec![k])])
1816}
1817
1818fn householder_qr_append_meta(
1819    dtypes: &[DType],
1820    shapes: &[&[SymDim]],
1821) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
1822    const OP: &str = "tenferro-linalg.householder_qr_append";
1823    require_householder_state_meta(OP, dtypes, shapes[0], shapes[1])?;
1824    require_householder_rank2(OP, shapes[2])?;
1825    if dtypes[0] != dtypes[2] {
1826        return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[2]));
1827    }
1828    require_static_extent_equal(OP, "rows", &shapes[0][0], &shapes[2][0])?;
1829    let m = shapes[0][0].clone();
1830    let width = shapes[0][1].clone() + shapes[2][1].clone();
1831    let k = m.clone().min(width.clone());
1832    Ok(vec![(dtypes[0], vec![m, width]), (dtypes[0], vec![k])])
1833}
1834
1835fn require_static_extent_equal(
1836    op: &'static str,
1837    field: &'static str,
1838    lhs: &SymDim,
1839    rhs: &SymDim,
1840) -> tenferro_tensor::Result<()> {
1841    if let (Some(lhs), Some(rhs)) = (lhs.constant_value(), rhs.constant_value())
1842        && lhs != rhs
1843    {
1844        return Err(Error::invalid_argument(
1845            op,
1846            field,
1847            format!("expected equal extents, got {lhs} and {rhs}"),
1848        ));
1849    }
1850    Ok(())
1851}
1852
1853fn require_householder_state_meta(
1854    op: &'static str,
1855    dtypes: &[DType],
1856    packed: &[SymDim],
1857    coeff: &[SymDim],
1858) -> tenferro_tensor::Result<()> {
1859    require_householder_rank2(op, packed)?;
1860    if coeff.len() != 1 {
1861        return Err(Error::rank_mismatch(op, 1, coeff.len()));
1862    }
1863    if dtypes[0] != dtypes[1] {
1864        return Err(Error::dtype_mismatch(op, dtypes[0], dtypes[1]));
1865    }
1866    let expected = packed[0].clone().min(packed[1].clone());
1867    require_static_extent_equal(op, "coeff", &coeff[0], &expected)
1868}
1869
1870fn householder_qr_r_meta(
1871    dtypes: &[DType],
1872    packed: &[SymDim],
1873    coeff: &[SymDim],
1874) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1875    require_householder_state_meta("tenferro-linalg.householder_qr_r", dtypes, packed, coeff)?;
1876    Ok((dtypes[0], vec![coeff[0].clone(), packed[1].clone()]))
1877}
1878
1879fn householder_qr_q_columns_meta(
1880    dtypes: &[DType],
1881    packed: &[SymDim],
1882    coeff: &[SymDim],
1883    start: usize,
1884    end: usize,
1885) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1886    require_householder_state_meta(
1887        "tenferro-linalg.householder_qr_q_columns",
1888        dtypes,
1889        packed,
1890        coeff,
1891    )?;
1892    if start > end {
1893        return Err(Error::invalid_argument(
1894            "tenferro-linalg.householder_qr_q_columns",
1895            "range",
1896            format!("invalid Q-column range {start}..{end}"),
1897        ));
1898    }
1899    // The reachable width is full Q, not thin Q: columns `k..m` span the
1900    // orthogonal complement of the input's column space.
1901    if packed[0].constant_value().is_some_and(|rows| end > rows) {
1902        return Err(Error::invalid_argument(
1903            "tenferro-linalg.householder_qr_q_columns",
1904            "range",
1905            format!("Q-column range {start}..{end} exceeds full-Q width"),
1906        ));
1907    }
1908    Ok((
1909        dtypes[0],
1910        vec![packed[0].clone(), SymDim::from(end - start)],
1911    ))
1912}
1913
1914fn householder_qr_thin_q_meta(
1915    dtypes: &[DType],
1916    packed: &[SymDim],
1917    coeff: &[SymDim],
1918) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1919    require_householder_state_meta(
1920        "tenferro-linalg.householder_qr_thin_q",
1921        dtypes,
1922        packed,
1923        coeff,
1924    )?;
1925    Ok((dtypes[0], vec![packed[0].clone(), coeff[0].clone()]))
1926}
1927
1928fn householder_qr_split_config(
1929    cotangent: &[usize],
1930    left: &[usize],
1931    right_shape: &[usize],
1932    take_right: bool,
1933) -> tenferro_tensor::Result<tenferro_tensor::SliceConfig> {
1934    const OP: &str = "tenferro-linalg.householder_qr_split_tangent";
1935    for shape in [cotangent, left, right_shape] {
1936        if shape.len() != 2 {
1937            return Err(Error::rank_mismatch(OP, 2, shape.len()));
1938        }
1939    }
1940    let total_width = left[1]
1941        .checked_add(right_shape[1])
1942        .ok_or_else(|| Error::invalid_argument(OP, "shape", "column range overflow"))?;
1943    if cotangent[0] != left[0] || cotangent[0] != right_shape[0] || cotangent[1] != total_width {
1944        return Err(Error::invalid_argument(
1945            OP,
1946            "shape",
1947            "cotangent shape does not match appended factors",
1948        ));
1949    }
1950    let selected = if take_right { right_shape } else { left };
1951    let start = if take_right { left[1] } else { 0 };
1952    let end = start
1953        .checked_add(selected[1])
1954        .ok_or_else(|| Error::invalid_argument(OP, "shape", "column range overflow"))?;
1955    Ok(tenferro_tensor::SliceConfig {
1956        starts: vec![0, start],
1957        limits: vec![selected[0], end],
1958        strides: vec![1, 1],
1959    })
1960}
1961
1962fn householder_qr_append_tangent_meta(
1963    dtypes: &[DType],
1964    shapes: &[&[SymDim]],
1965) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1966    const OP: &str = "tenferro-linalg.householder_qr_append_tangent";
1967    for shape in shapes {
1968        require_householder_rank2(OP, shape)?;
1969    }
1970    if dtypes.iter().any(|dtype| *dtype != dtypes[0]) {
1971        return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1972    }
1973    require_static_extent_equal(OP, "rows", &shapes[0][0], &shapes[1][0])?;
1974    require_static_extent_equal(OP, "left tangent", &shapes[0][0], &shapes[2][0])?;
1975    require_static_extent_equal(OP, "right tangent", &shapes[1][0], &shapes[3][0])?;
1976    require_static_extent_equal(OP, "anchor rows", &shapes[2][0], &shapes[3][0])?;
1977    Ok((
1978        dtypes[0],
1979        vec![
1980            shapes[2][0].clone(),
1981            shapes[2][1].clone() + shapes[3][1].clone(),
1982        ],
1983    ))
1984}
1985
1986fn householder_qr_split_tangent_meta(
1987    dtypes: &[DType],
1988    shapes: &[&[SymDim]],
1989    right: bool,
1990) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
1991    const OP: &str = "tenferro-linalg.householder_qr_split_tangent";
1992    for shape in shapes {
1993        require_householder_rank2(OP, shape)?;
1994    }
1995    if dtypes.iter().any(|dtype| *dtype != dtypes[0]) {
1996        return Err(Error::dtype_mismatch(OP, dtypes[0], dtypes[1]));
1997    }
1998    require_static_extent_equal(OP, "left rows", &shapes[0][0], &shapes[1][0])?;
1999    require_static_extent_equal(OP, "right rows", &shapes[0][0], &shapes[2][0])?;
2000    let expected_width = shapes[1][1].clone() + shapes[2][1].clone();
2001    require_static_extent_equal(OP, "width", &shapes[0][1], &expected_width)?;
2002    let selected = if right { shapes[2] } else { shapes[1] };
2003    Ok((dtypes[0], selected.to_vec()))
2004}
2005
2006fn eigh_meta(dtype: DType, shape: &[SymDim]) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
2007    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh", shape)?;
2008    Ok(vec![
2009        (singular_values_dtype(dtype), vector_shape(n.clone(), batch)),
2010        (dtype, matrix_shape(n.clone(), n, batch)),
2011    ])
2012}
2013
2014fn eigh_values_meta(
2015    dtype: DType,
2016    shape: &[SymDim],
2017) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
2018    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eigh_values", shape)?;
2019    Ok((singular_values_dtype(dtype), vector_shape(n, batch)))
2020}
2021
2022fn eig_meta(
2023    input_dtype: DType,
2024    shape: &[SymDim],
2025) -> tenferro_tensor::Result<Vec<(DType, Vec<SymDim>)>> {
2026    let dtype = eig_output_dtype(input_dtype);
2027    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig", shape)?;
2028    Ok(vec![
2029        (dtype, vector_shape(n.clone(), batch)),
2030        (dtype, matrix_shape(n.clone(), n, batch)),
2031    ])
2032}
2033
2034fn eig_values_meta(
2035    input_dtype: DType,
2036    shape: &[SymDim],
2037) -> tenferro_tensor::Result<(DType, Vec<SymDim>)> {
2038    let dtype = eig_output_dtype(input_dtype);
2039    let (n, _, batch) = matrix_meta_parts("tenferro-linalg.eig_values", shape)?;
2040    Ok((dtype, vector_shape(n, batch)))
2041}
2042
2043fn matrix_shape(rows: SymDim, cols: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
2044    let mut shape = vec![rows, cols];
2045    shape.extend_from_slice(batch);
2046    shape
2047}
2048
2049fn vector_shape(len: SymDim, batch: &[SymDim]) -> Vec<SymDim> {
2050    let mut shape = vec![len];
2051    shape.extend_from_slice(batch);
2052    shape
2053}
2054
2055fn eig_output_dtype(dtype: DType) -> DType {
2056    match dtype {
2057        DType::F64 | DType::C64 => DType::C64,
2058        DType::F32 | DType::C32 => DType::C32,
2059        DType::I32 | DType::I64 | DType::Bool => DType::C64,
2060        // INVARIANT: linalg validates its input dtype before mapping outputs, so
2061        // an externally defined scalar never reaches this mapping.
2062        DType::External(_) => unreachable!("linalg validates its input dtype first"),
2063    }
2064}
2065
2066fn singular_values_dtype(dtype: DType) -> DType {
2067    match dtype {
2068        DType::C64 => DType::F64,
2069        DType::C32 => DType::F32,
2070        DType::External(id) => {
2071            // Singular values of an externally defined scalar have no declared
2072            // dtype, so the mapping keeps the identity rather than guessing one.
2073            DType::External(id)
2074        }
2075        other => other,
2076    }
2077}
2078
2079fn promote_dtypes(dtypes: &[DType]) -> DType {
2080    dtypes
2081        .iter()
2082        .copied()
2083        .reduce(tenferro_tensor::validate::promote_dtype)
2084        .unwrap_or(DType::F64)
2085}
2086
2087fn hash_dtype(hasher: &mut dyn Hasher, dtype: DType) {
2088    let tag = match dtype {
2089        DType::F64 => 0,
2090        DType::F32 => 1,
2091        DType::I64 => 2,
2092        DType::C64 => 3,
2093        DType::C32 => 4,
2094        DType::I32 => 5,
2095        DType::Bool => 6,
2096        DType::External(id) => {
2097            // The cache key must distinguish two externally defined scalars, so it
2098            // carries the scalar's own identity rather than one shared code.
2099            // `Hash` needs a sized hasher, so the identity is folded into a
2100            // concrete one first. It is stable within a process, which is the
2101            // scope of this cache.
2102            let mut identity = std::collections::hash_map::DefaultHasher::new();
2103            Hash::hash(&id, &mut identity);
2104            hasher.write_u8(7);
2105            hasher.write_u64(identity.finish());
2106            return;
2107        }
2108    };
2109    hasher.write_u8(tag);
2110}
2111
2112fn hash_svd_gauge(hasher: &mut dyn Hasher, gauge: SvdGauge) {
2113    let tag = match gauge {
2114        SvdGauge::Raw => 0,
2115        SvdGauge::CanonicalPivot => 1,
2116    };
2117    hasher.write_u8(tag);
2118}
2119
2120fn hash_svd_driver(hasher: &mut dyn Hasher, driver: SvdDriver) {
2121    let tag = match driver {
2122        SvdDriver::Auto => 0,
2123        SvdDriver::Gesvdj => 1,
2124        SvdDriver::Gesvd => 2,
2125        SvdDriver::Xgesvdp => 3,
2126    };
2127    hasher.write_u8(tag);
2128}
2129
2130fn hash_eigh_driver(hasher: &mut dyn Hasher, driver: EighDriver) {
2131    let tag = match driver {
2132        EighDriver::Auto => 0,
2133        EighDriver::Syevd => 1,
2134        EighDriver::Syevj => 2,
2135    };
2136    hasher.write_u8(tag);
2137}
2138
2139fn hash_eigh_gauge(hasher: &mut dyn Hasher, gauge: EighGauge) {
2140    let tag = match gauge {
2141        EighGauge::Raw => 0,
2142        EighGauge::CanonicalPivot => 1,
2143    };
2144    hasher.write_u8(tag);
2145}
2146
2147fn hash_qr_gauge(hasher: &mut dyn Hasher, gauge: QrGauge) {
2148    let tag = match gauge {
2149        QrGauge::Raw => 0,
2150        QrGauge::PositiveDiagonal => 1,
2151    };
2152    hasher.write_u8(tag);
2153}