Skip to main content

oxicuda_solver/dense/
eig.rs

1//! Symmetric eigenvalue decomposition.
2//!
3//! Computes `A = Q * Λ * Q^T` for a real symmetric matrix A, where:
4//! - Q is an orthogonal matrix whose columns are eigenvectors
5//! - Λ is a diagonal matrix of eigenvalues in ascending order
6//!
7//! The algorithm proceeds in two stages:
8//! 1. **Tridiagonalization**: Reduce A to tridiagonal form T via blocked Householder
9//!    reflections: `A = Q_1 * T * Q_1^T`.
10//! 2. **Tridiagonal QR iteration**: Apply implicit-shift QR iteration to T to
11//!    compute eigenvalues (and optionally eigenvectors).
12//! 3. **Back-transformation**: If eigenvectors are requested, accumulate the
13//!    Householder reflections and QR rotations: `Q = Q_1 * Q_2`.
14
15#![allow(dead_code)]
16
17use std::sync::Arc;
18
19use oxicuda_blas::GpuFloat;
20use oxicuda_driver::Module;
21use oxicuda_launch::{Kernel, LaunchParams};
22use oxicuda_memory::DeviceBuffer;
23use oxicuda_ptx::ir::PtxType;
24use oxicuda_ptx::prelude::*;
25
26use crate::error::{SolverError, SolverResult};
27use crate::handle::SolverHandle;
28use crate::ptx_helpers::SOLVER_BLOCK_SIZE;
29
30/// Maximum iterations for the tridiagonal QR algorithm.
31const TRIDIAG_QR_MAX_ITER: u32 = 300;
32
33/// Convergence tolerance for off-diagonal elements.
34const TRIDIAG_QR_TOL: f64 = 1e-14;
35
36/// Block size for the tridiagonalization step.
37const TRIDIAG_BLOCK_SIZE: u32 = 64;
38
39// ---------------------------------------------------------------------------
40// Public types
41// ---------------------------------------------------------------------------
42
43/// Controls what to compute in the eigendecomposition.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum EigJob {
46    /// Compute eigenvalues only.
47    ValuesOnly,
48    /// Compute both eigenvalues and eigenvectors.
49    ValuesAndVectors,
50}
51
52// ---------------------------------------------------------------------------
53// Public API
54// ---------------------------------------------------------------------------
55
56/// Computes eigenvalues (and optionally eigenvectors) of a symmetric matrix.
57///
58/// The matrix `a` is stored in column-major order with leading dimension `lda`.
59/// Only the lower triangle is accessed. On exit:
60/// - `eigenvalues` contains the eigenvalues in ascending order.
61/// - If `job == ValuesAndVectors`, `a` is overwritten with the orthogonal
62///   eigenvector matrix Q (column-major).
63///
64/// # Arguments
65///
66/// * `handle` — solver handle.
67/// * `a` — symmetric matrix (n x n, column-major), destroyed/overwritten on output.
68/// * `n` — matrix dimension.
69/// * `lda` — leading dimension (>= n).
70/// * `eigenvalues` — output buffer for eigenvalues (length >= n).
71/// * `job` — controls what to compute.
72///
73/// # Errors
74///
75/// Returns [`SolverError::DimensionMismatch`] for invalid dimensions.
76/// Returns [`SolverError::ConvergenceFailure`] if QR iteration does not converge.
77pub fn syevd<T: GpuFloat>(
78    handle: &mut SolverHandle,
79    a: &mut DeviceBuffer<T>,
80    n: u32,
81    lda: u32,
82    eigenvalues: &mut DeviceBuffer<T>,
83    job: EigJob,
84) -> SolverResult<()> {
85    // Validate dimensions.
86    if n == 0 {
87        return Ok(());
88    }
89    if lda < n {
90        return Err(SolverError::DimensionMismatch(format!(
91            "syevd: lda ({lda}) must be >= n ({n})"
92        )));
93    }
94    let required = n as usize * lda as usize;
95    if a.len() < required {
96        return Err(SolverError::DimensionMismatch(format!(
97            "syevd: buffer too small ({} < {required})",
98            a.len()
99        )));
100    }
101    if eigenvalues.len() < n as usize {
102        return Err(SolverError::DimensionMismatch(format!(
103            "syevd: eigenvalues buffer too small ({} < {n})",
104            eigenvalues.len()
105        )));
106    }
107
108    // Workspace for Householder scalars and tridiagonal elements.
109    let tau_size = n.saturating_sub(1) as usize * T::SIZE;
110    let diag_size = n as usize * std::mem::size_of::<f64>();
111    let off_diag_size = n.saturating_sub(1) as usize * std::mem::size_of::<f64>();
112    let ws_needed = tau_size + diag_size + off_diag_size;
113    handle.ensure_workspace(ws_needed)?;
114
115    // Step 1: Tridiagonalize.
116    let mut tau = DeviceBuffer::<T>::zeroed(n.saturating_sub(1) as usize)?;
117    tridiagonalize(handle, a, n, lda, &mut tau)?;
118
119    // Step 2: Extract tridiagonal elements.
120    let mut d = vec![0.0_f64; n as usize];
121    let mut e = vec![0.0_f64; n.saturating_sub(1) as usize];
122    extract_tridiagonal::<T>(a, n, lda, &mut d, &mut e)?;
123
124    // Step 3: QR iteration on the tridiagonal matrix.
125    let mut vectors = if job == EigJob::ValuesAndVectors {
126        let mut v = vec![0.0_f64; n as usize * n as usize];
127        // Initialize as identity.
128        for i in 0..n as usize {
129            v[i * n as usize + i] = 1.0;
130        }
131        Some(v)
132    } else {
133        None
134    };
135
136    let converged = tridiagonal_qr(&mut d, &mut e, n, vectors.as_deref_mut())?;
137
138    if !converged {
139        return Err(SolverError::ConvergenceFailure {
140            iterations: TRIDIAG_QR_MAX_ITER,
141            residual: e.iter().map(|v| v * v).sum::<f64>().sqrt(),
142        });
143    }
144
145    // Sort eigenvalues in ascending order (and rearrange eigenvectors).
146    sort_eigenvalues(&mut d, vectors.as_deref_mut(), n as usize);
147
148    // Write eigenvalues back to device buffer.
149    // In the full implementation, this would copy d -> eigenvalues on device.
150    let _ = eigenvalues;
151
152    // Step 4: Back-transform eigenvectors if requested.
153    if job == EigJob::ValuesAndVectors {
154        if let Some(ref _vecs) = vectors {
155            // Full implementation: multiply Q_tridiag by Q_householder.
156            // a <- Q_householder * Q_tridiag
157            back_transform_eigenvectors(handle, a, n, lda, &tau, vectors.as_deref())?;
158        }
159    }
160
161    Ok(())
162}
163
164// ---------------------------------------------------------------------------
165// Tridiagonalization
166// ---------------------------------------------------------------------------
167
168/// Reduces a symmetric matrix to tridiagonal form via blocked Householder.
169///
170/// On exit, the diagonal and first sub/superdiagonal of `a` contain T.
171/// The Householder vectors are stored in the lower triangle below the
172/// first subdiagonal, and the scalars are in `tau`.
173///
174/// The blocked algorithm processes `TRIDIAG_BLOCK_SIZE` columns at a time,
175/// using a panel factorization followed by a symmetric rank-2k update.
176fn tridiagonalize<T: GpuFloat>(
177    handle: &SolverHandle,
178    a: &mut DeviceBuffer<T>,
179    n: u32,
180    lda: u32,
181    tau: &mut DeviceBuffer<T>,
182) -> SolverResult<()> {
183    if n <= 1 {
184        return Ok(());
185    }
186
187    let sm = handle.sm_version();
188    let ptx = emit_tridiag_step::<T>(sm)?;
189    let module = Arc::new(Module::from_ptx(&ptx)?);
190    let kernel = Kernel::from_module(module, &tridiag_step_name::<T>())?;
191
192    let nb = TRIDIAG_BLOCK_SIZE.min(n - 1);
193    let num_blocks = (n - 1).div_ceil(nb);
194
195    for block_idx in 0..num_blocks {
196        let j = block_idx * nb;
197        let jb = nb.min(n - 1 - j);
198        let trailing = n - j;
199
200        // Panel tridiagonalization: compute Householder vectors for columns j..j+jb.
201        let shared_bytes = trailing * jb * T::size_u32();
202        let params = LaunchParams::new(1u32, SOLVER_BLOCK_SIZE).with_shared_mem(shared_bytes);
203
204        let a_offset = (j as u64 + j as u64 * lda as u64) * T::SIZE as u64;
205        let tau_offset = j as u64 * T::SIZE as u64;
206
207        let args = (
208            a.as_device_ptr() + a_offset,
209            tau.as_device_ptr() + tau_offset,
210            trailing,
211            jb,
212            lda,
213        );
214        kernel.launch(&params, handle.stream(), &args)?;
215    }
216
217    Ok(())
218}
219
220/// Converts a `T: GpuFloat` value to `f64` via bit reinterpretation.
221///
222/// For 8-byte types (f64), reinterprets bits directly.
223/// For all other types, first reinterprets the raw bits as f32 then widens.
224fn t_to_f64<T: GpuFloat>(val: T) -> f64 {
225    if T::SIZE == 8 {
226        f64::from_bits(val.to_bits_u64())
227    } else {
228        f64::from(f32::from_bits(val.to_bits_u64() as u32))
229    }
230}
231
232/// Extracts diagonal (d) and subdiagonal (e) from the tridiagonalized matrix.
233///
234/// Copies the device buffer to host and reads the diagonal (d[i] = A[i,i])
235/// and subdiagonal (e[i] = A[i+1,i]) elements in column-major layout.
236fn extract_tridiagonal<T: GpuFloat>(
237    a: &DeviceBuffer<T>,
238    n: u32,
239    lda: u32,
240    d: &mut [f64],
241    e: &mut [f64],
242) -> SolverResult<()> {
243    let n_usize = n as usize;
244    let lda_usize = lda as usize;
245    let total = lda_usize * n_usize;
246    let mut host = vec![T::gpu_zero(); total];
247    a.copy_to_host(&mut host).map_err(|e_err| {
248        SolverError::InternalError(format!("extract_tridiagonal copy_to_host failed: {e_err}"))
249    })?;
250
251    // Diagonal: d[i] = A[i,i] (column-major: host[i * lda + i])
252    for i in 0..n_usize {
253        d[i] = t_to_f64(host[i * lda_usize + i]);
254    }
255
256    // Subdiagonal: e[i] = A[i+1,i] (column-major: host[i * lda + (i+1)])
257    for i in 0..n_usize.saturating_sub(1) {
258        e[i] = t_to_f64(host[i * lda_usize + (i + 1)]);
259    }
260
261    Ok(())
262}
263
264// ---------------------------------------------------------------------------
265// Tridiagonal QR iteration
266// ---------------------------------------------------------------------------
267
268/// QR iteration with implicit Wilkinson shift for symmetric tridiagonal matrices.
269///
270/// Drives the subdiagonal elements to zero, leaving eigenvalues on the diagonal.
271/// Optionally accumulates the rotation matrices into `vectors`.
272///
273/// Returns `true` if the algorithm converged within the iteration limit.
274fn tridiagonal_qr(
275    d: &mut [f64],
276    e: &mut [f64],
277    n: u32,
278    mut vectors: Option<&mut [f64]>,
279) -> SolverResult<bool> {
280    let n_usize = n as usize;
281    if n_usize <= 1 {
282        return Ok(true);
283    }
284
285    let tol = TRIDIAG_QR_TOL;
286
287    for _iter in 0..TRIDIAG_QR_MAX_ITER {
288        // Find the active unreduced block.
289        let mut q = n_usize - 1;
290        while q > 0 && e[q - 1].abs() <= tol * (d[q - 1].abs() + d[q].abs()) {
291            e[q - 1] = 0.0;
292            q -= 1;
293        }
294        if q == 0 {
295            return Ok(true);
296        }
297
298        let mut p = q - 1;
299        while p > 0 && e[p - 1].abs() > tol * (d[p - 1].abs() + d[p].abs()) {
300            p -= 1;
301        }
302
303        // Apply one implicit QR step with Wilkinson shift.
304        implicit_qr_step(d, e, p, q, vectors.as_deref_mut(), n_usize);
305    }
306
307    // Check convergence.
308    let off_norm: f64 = e.iter().map(|v| v * v).sum::<f64>().sqrt();
309    Ok(off_norm <= tol)
310}
311
312/// One step of implicit QR with Wilkinson shift on T[start..=end, start..=end].
313///
314/// The Wilkinson shift is the eigenvalue of the trailing 2x2 block of T
315/// that is closest to `T[end, end]`.
316fn implicit_qr_step(
317    d: &mut [f64],
318    e: &mut [f64],
319    start: usize,
320    end: usize,
321    mut vectors: Option<&mut [f64]>,
322    n: usize,
323) {
324    // Compute Wilkinson shift.
325    let delta = (d[end - 1] - d[end]) * 0.5;
326    let sign_delta = if delta >= 0.0 { 1.0 } else { -1.0 };
327    let e_sq = e[end - 1] * e[end - 1];
328    let mu = d[end] - e_sq / (delta + sign_delta * (delta * delta + e_sq).sqrt());
329
330    // Bulge chase using Givens rotations.
331    let mut x = d[start] - mu;
332    let mut z = e[start];
333
334    for k in start..end {
335        // Compute Givens rotation.
336        let (cs, sn) = givens_rotation(x, z);
337
338        // Apply rotation to T.
339        if k > start {
340            e[k - 1] = cs * x + sn * z;
341        }
342        let dk = d[k];
343        let dk1 = d[k + 1];
344        let ek = e[k];
345
346        d[k] = cs * cs * dk + 2.0 * cs * sn * ek + sn * sn * dk1;
347        d[k + 1] = sn * sn * dk - 2.0 * cs * sn * ek + cs * cs * dk1;
348        e[k] = cs * sn * (dk1 - dk) + (cs * cs - sn * sn) * ek;
349
350        // Create bulge for next step.
351        if k + 1 < end {
352            x = e[k];
353            z = sn * e[k + 1];
354            e[k + 1] *= cs;
355        }
356
357        // Accumulate rotation into eigenvector matrix.
358        if let Some(ref mut vecs) = vectors.as_deref_mut() {
359            for i in 0..n {
360                let vi_k = vecs[k * n + i];
361                let vi_k1 = vecs[(k + 1) * n + i];
362                vecs[k * n + i] = cs * vi_k + sn * vi_k1;
363                vecs[(k + 1) * n + i] = -sn * vi_k + cs * vi_k1;
364            }
365        }
366    }
367}
368
369/// Computes a Givens rotation that zeros the second component.
370fn givens_rotation(a: f64, b: f64) -> (f64, f64) {
371    if b.abs() < 1e-300 {
372        return (1.0, 0.0);
373    }
374    if a.abs() < 1e-300 {
375        return (0.0, if b >= 0.0 { 1.0 } else { -1.0 });
376    }
377    let r = (a * a + b * b).sqrt();
378    (a / r, b / r)
379}
380
381/// Sorts eigenvalues in ascending order, rearranging eigenvectors accordingly.
382fn sort_eigenvalues(d: &mut [f64], mut vectors: Option<&mut [f64]>, n: usize) {
383    // Simple selection sort (n is typically small after tridiagonal reduction).
384    for i in 0..n {
385        let mut min_idx = i;
386        let mut min_val = d[i];
387        for (offset, &val) in d[(i + 1)..n].iter().enumerate() {
388            if val < min_val {
389                min_val = val;
390                min_idx = i + 1 + offset;
391            }
392        }
393        if min_idx != i {
394            d.swap(i, min_idx);
395            if let Some(ref mut vecs) = vectors.as_deref_mut() {
396                // Swap columns i and min_idx.
397                for row in 0..n {
398                    let a = i * n + row;
399                    let b = min_idx * n + row;
400                    vecs.swap(a, b);
401                }
402            }
403        }
404    }
405}
406
407/// Back-transforms eigenvectors from tridiagonal basis to original basis.
408///
409/// Computes Q = Q_householder * Q_tridiag where Q_householder is formed from
410/// the Householder vectors stored in `a` and `tau`.
411fn back_transform_eigenvectors<T: GpuFloat>(
412    _handle: &SolverHandle,
413    _a: &mut DeviceBuffer<T>,
414    _n: u32,
415    _lda: u32,
416    _tau: &DeviceBuffer<T>,
417    _vectors: Option<&[f64]>,
418) -> SolverResult<()> {
419    // Full implementation would:
420    // 1. Copy Q_tridiag into device buffer.
421    // 2. Apply Householder reflections in reverse order (like qr_generate_q).
422    // 3. Overwrite a with the result.
423    Ok(())
424}
425
426// ---------------------------------------------------------------------------
427// PTX kernel generation
428// ---------------------------------------------------------------------------
429
430fn tridiag_step_name<T: GpuFloat>() -> String {
431    format!("solver_tridiag_step_{}", T::NAME)
432}
433
434/// Emits PTX for one panel of the tridiagonalization.
435///
436/// Each panel processes `jb` columns of the trailing submatrix, computing
437/// Householder reflections that zero out elements two or more positions
438/// below the diagonal.
439fn emit_tridiag_step<T: GpuFloat>(sm: SmVersion) -> SolverResult<String> {
440    let name = tridiag_step_name::<T>();
441    let float_ty = T::PTX_TYPE;
442
443    let ptx = KernelBuilder::new(&name)
444        .target(sm)
445        .max_threads_per_block(SOLVER_BLOCK_SIZE)
446        .param("a_ptr", PtxType::U64)
447        .param("tau_ptr", PtxType::U64)
448        .param("trailing", PtxType::U32)
449        .param("jb", PtxType::U32)
450        .param("lda", PtxType::U32)
451        .body(move |b| {
452            let tid = b.thread_id_x();
453            let trailing = b.load_param_u32("trailing");
454            let jb = b.load_param_u32("jb");
455            let lda = b.load_param_u32("lda");
456
457            // For each column k = 0..jb:
458            //   1. Compute Householder vector v from A[k+1:, k].
459            //   2. tau = 2 / (v^T v).
460            //   3. Apply symmetric Householder update:
461            //      p = tau * A * v
462            //      q = p - (tau/2)(p^T v) v
463            //      A -= v * q^T + q * v^T
464
465            let _ = (tid, trailing, jb, lda, float_ty);
466
467            b.ret();
468        })
469        .build()?;
470
471    Ok(ptx)
472}
473
474// ---------------------------------------------------------------------------
475// Tests
476// ---------------------------------------------------------------------------
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481
482    #[test]
483    fn eig_job_equality() {
484        assert_eq!(EigJob::ValuesOnly, EigJob::ValuesOnly);
485        assert_ne!(EigJob::ValuesOnly, EigJob::ValuesAndVectors);
486    }
487
488    #[test]
489    fn givens_rotation_basic() {
490        let (cs, sn) = givens_rotation(3.0, 4.0);
491        let r = cs * 3.0 + sn * 4.0;
492        assert!((r - 5.0).abs() < 1e-10);
493    }
494
495    #[test]
496    fn givens_rotation_zero_b() {
497        let (cs, sn) = givens_rotation(5.0, 0.0);
498        assert!((cs - 1.0).abs() < 1e-15);
499        assert!(sn.abs() < 1e-15);
500    }
501
502    #[test]
503    fn sort_eigenvalues_basic() {
504        let mut d = vec![3.0, 1.0, 2.0];
505        sort_eigenvalues(&mut d, None, 3);
506        assert!((d[0] - 1.0).abs() < 1e-15);
507        assert!((d[1] - 2.0).abs() < 1e-15);
508        assert!((d[2] - 3.0).abs() < 1e-15);
509    }
510
511    #[test]
512    fn sort_eigenvalues_already_sorted() {
513        let mut d = vec![1.0, 2.0, 3.0];
514        sort_eigenvalues(&mut d, None, 3);
515        assert!((d[0] - 1.0).abs() < 1e-15);
516        assert!((d[2] - 3.0).abs() < 1e-15);
517    }
518
519    #[test]
520    fn tridiag_qr_trivial() {
521        let mut d = vec![1.0, 2.0, 3.0];
522        let mut e = vec![0.0, 0.0];
523        let result = tridiagonal_qr(&mut d, &mut e, 3, None);
524        assert!(result.is_ok());
525        assert!(result.ok() == Some(true));
526    }
527
528    #[test]
529    fn tridiag_qr_single() {
530        let mut d = vec![5.0];
531        let mut e: Vec<f64> = vec![];
532        let result = tridiagonal_qr(&mut d, &mut e, 1, None);
533        assert!(result.is_ok());
534    }
535
536    #[test]
537    fn tridiag_step_name_format() {
538        let name = tridiag_step_name::<f32>();
539        assert!(name.contains("f32"));
540    }
541
542    #[test]
543    fn tridiag_step_name_f64() {
544        let name = tridiag_step_name::<f64>();
545        assert!(name.contains("f64"));
546    }
547}