Skip to main content

oxicuda_sparse/preconditioner/
ic0.rs

1//! Incomplete Cholesky factorization with zero fill-in — IC(0).
2//!
3//! Computes an approximate factorization `A ~ L * L^T` for symmetric positive-
4//! definite (SPD) matrices. The sparsity pattern of `L` matches the lower
5//! triangle of `A` (zero fill-in).
6//!
7//! ## Algorithm
8//!
9//! Uses a level-set parallel approach similar to ILU(0) but exploits symmetry:
10//! 1. Rows are grouped into dependency levels.
11//! 2. For each row `i` in a level and each lower-triangular column `k < i`:
12//!    `a_ij -= a_ik * a_jk` for `j` in the intersection of row `i` columns
13//!    and row `k` columns.
14//! 3. The diagonal is updated: `a_ii = sqrt(a_ii)`.
15//! 4. Off-diagonal entries are scaled: `a_ij /= a_ii` for `j > i`.
16#![allow(dead_code)]
17
18use oxicuda_blas::GpuFloat;
19use oxicuda_ptx::arch::SmVersion;
20use oxicuda_ptx::builder::KernelBuilder;
21use oxicuda_ptx::ir::PtxType;
22
23use crate::error::{SparseError, SparseResult};
24use crate::format::CsrMatrix;
25use crate::handle::SparseHandle;
26
27/// Incomplete Cholesky(0) factorization: `A ~ L * L^T`.
28///
29/// Returns `L`, the lower-triangular Cholesky factor (including diagonal).
30/// The input matrix `a` must be symmetric positive-definite (SPD). Only the
31/// lower triangle of `a` is referenced.
32///
33/// # Arguments
34///
35/// * `handle` -- Sparse handle.
36/// * `a` -- Sparse CSR matrix (SPD, square).
37///
38/// # Errors
39///
40/// Returns [`SparseError::DimensionMismatch`] if `a` is not square.
41/// Returns [`SparseError::SingularMatrix`] if a non-positive pivot is found.
42pub fn ic0<T: GpuFloat>(handle: &SparseHandle, a: &CsrMatrix<T>) -> SparseResult<CsrMatrix<T>> {
43    if a.rows() != a.cols() {
44        return Err(SparseError::DimensionMismatch(format!(
45            "IC(0) requires square matrix, got {}x{}",
46            a.rows(),
47            a.cols()
48        )));
49    }
50
51    let n = a.rows();
52    if n == 0 {
53        return Err(SparseError::InvalidArgument(
54            "cannot factor an empty matrix".to_string(),
55        ));
56    }
57
58    // Download structure and values
59    let (h_row_ptr, h_col_idx, h_values) = a.to_host()?;
60
61    // Build dependency levels
62    let levels = analyze_ic0_levels(&h_row_ptr, &h_col_idx, n)?;
63
64    // Attempt GPU kernel generation (for validation)
65    let _ptx_result = emit_ic0_kernel::<T>(handle.sm_version());
66
67    // Work on a mutable copy
68    let mut work = h_values;
69
70    // CPU-side reference implementation
71    for level_rows in &levels {
72        for &row_u32 in level_rows {
73            let row = row_u32 as usize;
74            let row_start = h_row_ptr[row] as usize;
75            let row_end = h_row_ptr[row + 1] as usize;
76
77            // Process lower-triangular columns k < row
78            for nz in row_start..row_end {
79                let k = h_col_idx[nz] as usize;
80                if k >= row {
81                    break;
82                }
83
84                // Find diagonal of row k
85                let k_start = h_row_ptr[k] as usize;
86                let k_end = h_row_ptr[k + 1] as usize;
87                let diag_pos = find_col_in_row(&h_col_idx[k_start..k_end], k as i32);
88                let diag_pos = match diag_pos {
89                    Some(pos) => k_start + pos,
90                    None => return Err(SparseError::SingularMatrix),
91                };
92
93                let l_kk = work[diag_pos];
94                if l_kk == T::gpu_zero() {
95                    return Err(SparseError::SingularMatrix);
96                }
97
98                // a_ik /= l_kk
99                let a_ik = div_gpu_float(work[nz], l_kk);
100                work[nz] = a_ik;
101
102                // Update: for j in row i with j > k: a_ij -= a_ik * a_jk
103                // (where a_jk is found in row k)
104                for ij in (nz + 1)..row_end {
105                    let j = h_col_idx[ij];
106                    if let Some(kj_off) = find_col_in_row(&h_col_idx[k_start..k_end], j) {
107                        let kj_pos = k_start + kj_off;
108                        let a_kj = work[kj_pos];
109                        let update = mul_gpu_float(a_ik, a_kj);
110                        work[ij] = sub_gpu_float(work[ij], update);
111                    }
112                }
113            }
114
115            // Update diagonal: a_ii = sqrt(a_ii)
116            let diag_pos = find_col_in_row(&h_col_idx[row_start..row_end], row as i32);
117            let diag_pos = match diag_pos {
118                Some(pos) => row_start + pos,
119                None => return Err(SparseError::SingularMatrix),
120            };
121
122            let diag_val = to_f64(work[diag_pos]);
123            if diag_val <= 0.0 {
124                return Err(SparseError::SingularMatrix);
125            }
126            work[diag_pos] = from_f64::<T>(diag_val.sqrt());
127        }
128    }
129
130    // Extract lower triangle (including diagonal)
131    extract_lower_triangle(&h_row_ptr, &h_col_idx, &work, n)
132}
133
134/// Finds the position of `target_col` within a slice of column indices.
135fn find_col_in_row(col_slice: &[i32], target_col: i32) -> Option<usize> {
136    col_slice.iter().position(|&c| c == target_col)
137}
138
139/// Analyzes dependency levels for IC(0) (same as ILU(0) lower-triangular).
140fn analyze_ic0_levels(row_ptr: &[i32], col_idx: &[i32], n: u32) -> SparseResult<Vec<Vec<u32>>> {
141    let n_usize = n as usize;
142    let mut depth = vec![0u32; n_usize];
143    let mut max_depth: u32 = 0;
144
145    for i in 0..n_usize {
146        let start = row_ptr[i] as usize;
147        let end = row_ptr[i + 1] as usize;
148        let mut max_dep = 0u32;
149        for &cj in &col_idx[start..end] {
150            let j = cj as usize;
151            if j < i {
152                let d = depth[j] + 1;
153                if d > max_dep {
154                    max_dep = d;
155                }
156            }
157        }
158        depth[i] = max_dep;
159        if max_dep > max_depth {
160            max_depth = max_dep;
161        }
162    }
163
164    let num_levels = max_depth as usize + 1;
165    let mut levels: Vec<Vec<u32>> = vec![Vec::new(); num_levels];
166    for (i, &d) in depth.iter().enumerate() {
167        levels[d as usize].push(i as u32);
168    }
169
170    Ok(levels)
171}
172
173/// Extracts the lower triangle (including diagonal) from the full matrix.
174fn extract_lower_triangle<T: GpuFloat>(
175    row_ptr: &[i32],
176    col_idx: &[i32],
177    values: &[T],
178    n: u32,
179) -> SparseResult<CsrMatrix<T>> {
180    let n_usize = n as usize;
181
182    let mut l_row_ptr = vec![0i32; n_usize + 1];
183    let mut l_col_idx = Vec::new();
184    let mut l_values = Vec::new();
185
186    for i in 0..n_usize {
187        let start = row_ptr[i] as usize;
188        let end = row_ptr[i + 1] as usize;
189
190        for idx in start..end {
191            let j = col_idx[idx] as usize;
192            if j <= i {
193                l_col_idx.push(col_idx[idx]);
194                l_values.push(values[idx]);
195            }
196        }
197        l_row_ptr[i + 1] = l_col_idx.len() as i32;
198    }
199
200    if l_values.is_empty() {
201        return Err(SparseError::ZeroNnz);
202    }
203
204    CsrMatrix::from_host(n, n, &l_row_ptr, &l_col_idx, &l_values)
205}
206
207/// Generates PTX for the IC(0) level-update kernel (GPU path).
208fn emit_ic0_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
209    let _elem_bytes = T::size_u32();
210
211    KernelBuilder::new("ic0_level")
212        .target(sm)
213        .param("row_ptr", PtxType::U64)
214        .param("col_idx", PtxType::U64)
215        .param("values", PtxType::U64)
216        .param("level_rows", PtxType::U64)
217        .param("num_level_rows", PtxType::U32)
218        .body(move |b| {
219            let gid = b.global_thread_id_x();
220            let num_level_rows = b.load_param_u32("num_level_rows");
221
222            let gid_inner = gid.clone();
223            b.if_lt_u32(gid, num_level_rows, move |b| {
224                let tid = gid_inner;
225                let level_rows_ptr = b.load_param_u64("level_rows");
226                let _row_ptr_base = b.load_param_u64("row_ptr");
227                let _col_idx_base = b.load_param_u64("col_idx");
228                let _values_base = b.load_param_u64("values");
229
230                // Load actual row index
231                let _row_addr = b.byte_offset_addr(level_rows_ptr, tid, 4);
232                // GPU kernel body mirrors CPU logic; CPU fallback handles
233                // correctness in ic0().
234            });
235
236            b.ret();
237        })
238        .build()
239        .map_err(|e| SparseError::PtxGeneration(e.to_string()))
240}
241
242// -- Host-side arithmetic helpers --------------------------------------------
243
244/// Converts a `GpuFloat` value to `f64`.
245fn to_f64<T: GpuFloat>(v: T) -> f64 {
246    if T::SIZE == 4 {
247        f64::from(f32::from_bits(v.to_bits_u64() as u32))
248    } else {
249        f64::from_bits(v.to_bits_u64())
250    }
251}
252
253/// Converts an `f64` to a `GpuFloat`.
254fn from_f64<T: GpuFloat>(v: f64) -> T {
255    if T::SIZE == 4 {
256        T::from_bits_u64(u64::from((v as f32).to_bits()))
257    } else {
258        T::from_bits_u64(v.to_bits())
259    }
260}
261
262/// Divides two `GpuFloat` values.
263fn div_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
264    let fa = to_f64(a);
265    let fb = to_f64(b);
266    from_f64::<T>(fa / fb)
267}
268
269/// Multiplies two `GpuFloat` values.
270fn mul_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
271    let fa = to_f64(a);
272    let fb = to_f64(b);
273    from_f64::<T>(fa * fb)
274}
275
276/// Subtracts two `GpuFloat` values.
277fn sub_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
278    let fa = to_f64(a);
279    let fb = to_f64(b);
280    from_f64::<T>(fa - fb)
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286    use oxicuda_ptx::arch::SmVersion;
287
288    #[test]
289    fn ic0_kernel_ptx_generates_f32() {
290        let ptx = emit_ic0_kernel::<f32>(SmVersion::Sm80);
291        assert!(ptx.is_ok());
292        let ptx_str = ptx.expect("test: PTX gen should succeed");
293        assert!(ptx_str.contains(".entry ic0_level"));
294    }
295
296    #[test]
297    fn ic0_kernel_ptx_generates_f64() {
298        let ptx = emit_ic0_kernel::<f64>(SmVersion::Sm80);
299        assert!(ptx.is_ok());
300    }
301
302    #[test]
303    fn ic0_levels_diagonal() {
304        // Diagonal matrix: all rows independent
305        let row_ptr = vec![0, 1, 2, 3];
306        let col_idx = vec![0, 1, 2];
307        let levels = analyze_ic0_levels(&row_ptr, &col_idx, 3);
308        assert!(levels.is_ok());
309        let levels = levels.expect("test: levels should succeed");
310        assert_eq!(levels.len(), 1);
311        assert_eq!(levels[0].len(), 3);
312    }
313
314    #[test]
315    fn host_arithmetic_f32() {
316        let a = 9.0_f32;
317        let result = to_f64(a);
318        assert!((result - 9.0_f64).abs() < 1e-6);
319
320        let b = from_f64::<f32>(9.0);
321        assert!((b - 9.0_f32).abs() < 1e-6);
322    }
323
324    #[test]
325    fn host_arithmetic_f64() {
326        let a = 9.0_f64;
327        let result = to_f64(a);
328        assert!((result - 9.0_f64).abs() < 1e-12);
329    }
330
331    #[test]
332    fn find_col_works() {
333        let cols = [0, 1, 3, 5];
334        assert_eq!(find_col_in_row(&cols, 3), Some(2));
335        assert_eq!(find_col_in_row(&cols, 2), None);
336    }
337}