Skip to main content

oxicuda_sparse/preconditioner/
graph_coloring.rs

1//! Distance-2 graph coloring for parallel ILU/IC.
2//!
3//! Colors vertices of the adjacency graph of a sparse matrix such that no two
4//! vertices within distance 2 share the same color. This enables parallel
5//! processing of independent rows during ILU(0) or IC(0) factorization:
6//! all rows with the same color can be processed simultaneously without
7//! data races.
8//!
9//! # Algorithm
10//!
11//! Uses a greedy distance-2 coloring:
12//! 1. Process rows in order 0, 1, ..., n-1.
13//! 2. For each row `i`, collect colors of all distance-1 neighbors (columns
14//!    in row `i`) and distance-2 neighbors (columns of the rows of distance-1
15//!    neighbors).
16//! 3. Assign to row `i` the smallest color not in the collected set.
17//!
18//! This yields an upper bound of `Δ^2 + 1` colors where `Δ` is the maximum
19//! degree, which is tight for some graphs but typically much better in
20//! practice for sparse matrices from discretized PDEs.
21//!
22//! # Parallel ILU(0)
23//!
24//! After coloring, ILU(0) proceeds color-by-color: for each color `c`,
25//! all rows with that color can be updated in parallel. Within a color,
26//! no row depends on another (guaranteed by the distance-2 property).
27
28#![allow(dead_code)]
29
30use oxicuda_blas::GpuFloat;
31
32use crate::error::{SparseError, SparseResult};
33use crate::format::CsrMatrix;
34use crate::handle::SparseHandle;
35
36// ---------------------------------------------------------------------------
37// GpuFloat <-> f64 conversion helpers
38// ---------------------------------------------------------------------------
39
40fn to_f64<T: GpuFloat>(val: T) -> f64 {
41    if T::SIZE == 4 {
42        f32::from_bits(val.to_bits_u64() as u32) as f64
43    } else {
44        f64::from_bits(val.to_bits_u64())
45    }
46}
47
48fn from_f64<T: GpuFloat>(val: f64) -> T {
49    if T::SIZE == 4 {
50        T::from_bits_u64(u64::from((val as f32).to_bits()))
51    } else {
52        T::from_bits_u64(val.to_bits())
53    }
54}
55
56// ---------------------------------------------------------------------------
57// Graph coloring result
58// ---------------------------------------------------------------------------
59
60/// Result of a distance-2 graph coloring of a sparse matrix.
61///
62/// Contains the color assignment for each row, plus auxiliary data structures
63/// for efficiently iterating over rows of the same color.
64pub struct GraphColoring {
65    /// Number of distinct colors used.
66    pub num_colors: usize,
67    /// Color assigned to each row: `colors[i]` is the color of row `i`.
68    pub colors: Vec<u32>,
69    /// Start index of each color group in `color_order`.
70    /// `color_offsets[c]..color_offsets[c+1]` gives the range of row indices
71    /// with color `c` in `color_order`.
72    pub color_offsets: Vec<usize>,
73    /// Row indices sorted by color.
74    pub color_order: Vec<u32>,
75}
76
77impl GraphColoring {
78    /// Compute a distance-2 coloring of the adjacency graph of a CSR matrix.
79    ///
80    /// The matrix must be square. The coloring considers the structural
81    /// (non-zero) pattern, not the numerical values.
82    ///
83    /// # Arguments
84    ///
85    /// * `_handle` — sparse handle (reserved for future GPU-accelerated variants).
86    /// * `matrix` — a square CSR matrix.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`SparseError::DimensionMismatch`] if the matrix is not square.
91    pub fn from_csr<T: GpuFloat>(
92        _handle: &SparseHandle,
93        matrix: &CsrMatrix<T>,
94    ) -> SparseResult<Self> {
95        if matrix.rows() != matrix.cols() {
96            return Err(SparseError::DimensionMismatch(format!(
97                "GraphColoring requires square matrix, got {}x{}",
98                matrix.rows(),
99                matrix.cols()
100            )));
101        }
102
103        let n = matrix.rows() as usize;
104        if n == 0 {
105            return Ok(Self {
106                num_colors: 0,
107                colors: Vec::new(),
108                color_offsets: vec![0],
109                color_order: Vec::new(),
110            });
111        }
112
113        // Download structure to host.
114        let (h_row_ptr, h_col_idx, _h_values) = matrix.to_host()?;
115
116        Self::from_csr_host(&h_row_ptr, &h_col_idx, n)
117    }
118
119    /// Compute distance-2 coloring from host-side CSR structure.
120    ///
121    /// This is the core algorithm, operating on host data.
122    pub fn from_csr_host(row_ptr: &[i32], col_idx: &[i32], n: usize) -> SparseResult<Self> {
123        if n == 0 {
124            return Ok(Self {
125                num_colors: 0,
126                colors: Vec::new(),
127                color_offsets: vec![0],
128                color_order: Vec::new(),
129            });
130        }
131
132        let mut colors = vec![u32::MAX; n];
133        let mut max_color: u32 = 0;
134
135        // Temporary buffer for forbidden colors.
136        // Using a Vec<bool> indexed by color to track which colors are forbidden.
137        let mut forbidden = Vec::new();
138
139        for i in 0..n {
140            let row_start = row_ptr[i] as usize;
141            let row_end = row_ptr[i + 1] as usize;
142
143            // Clear forbidden set.
144            for f in forbidden.iter_mut() {
145                *f = false;
146            }
147
148            // Distance-1 neighbors: columns in row i.
149            for nz in row_start..row_end {
150                let j = col_idx[nz] as usize;
151                if j == i || j >= n {
152                    continue;
153                }
154                // If neighbor j is already colored, forbid that color.
155                if colors[j] != u32::MAX {
156                    let c = colors[j] as usize;
157                    if c >= forbidden.len() {
158                        forbidden.resize(c + 1, false);
159                    }
160                    forbidden[c] = true;
161                }
162
163                // Distance-2 neighbors: columns in row j.
164                let j_start = row_ptr[j] as usize;
165                let j_end = row_ptr[j + 1] as usize;
166                for &col in &col_idx[j_start..j_end] {
167                    let k = col as usize;
168                    if k == i || k >= n {
169                        continue;
170                    }
171                    if colors[k] != u32::MAX {
172                        let c = colors[k] as usize;
173                        if c >= forbidden.len() {
174                            forbidden.resize(c + 1, false);
175                        }
176                        forbidden[c] = true;
177                    }
178                }
179            }
180
181            // Find smallest non-forbidden color.
182            let mut chosen = 0u32;
183            loop {
184                let c = chosen as usize;
185                if c >= forbidden.len() || !forbidden[c] {
186                    break;
187                }
188                chosen += 1;
189            }
190
191            colors[i] = chosen;
192            if chosen > max_color {
193                max_color = chosen;
194            }
195        }
196
197        let num_colors = max_color as usize + 1;
198
199        // Build color_offsets and color_order.
200        let mut color_counts = vec![0usize; num_colors];
201        for &c in &colors {
202            color_counts[c as usize] += 1;
203        }
204
205        let mut color_offsets = vec![0usize; num_colors + 1];
206        for c in 0..num_colors {
207            color_offsets[c + 1] = color_offsets[c] + color_counts[c];
208        }
209
210        let mut color_order = vec![0u32; n];
211        let mut write_pos = color_offsets[..num_colors].to_vec();
212        for (i, &c) in colors.iter().enumerate() {
213            let c_usize = c as usize;
214            color_order[write_pos[c_usize]] = i as u32;
215            write_pos[c_usize] += 1;
216        }
217
218        Ok(Self {
219            num_colors,
220            colors,
221            color_offsets,
222            color_order,
223        })
224    }
225
226    /// Returns the row indices for a given color.
227    ///
228    /// # Panics
229    ///
230    /// This method does not panic; returns an empty slice if `color >= num_colors`.
231    pub fn rows_for_color(&self, color: usize) -> &[u32] {
232        if color >= self.num_colors {
233            return &[];
234        }
235        let start = self.color_offsets[color];
236        let end = self.color_offsets[color + 1];
237        &self.color_order[start..end]
238    }
239
240    /// Apply coloring to parallelize ILU(0) factorization.
241    ///
242    /// Performs ILU(0) on a CSR matrix, processing rows color-by-color.
243    /// Within each color, rows are independent and can be processed in parallel.
244    ///
245    /// Returns `(L, U)` where `L` is unit lower triangular and `U` is upper
246    /// triangular (including diagonal).
247    ///
248    /// # Arguments
249    ///
250    /// * `_handle` — sparse handle.
251    /// * `matrix` — the CSR matrix to factor (square, will be consumed for values).
252    ///
253    /// # Errors
254    ///
255    /// Returns [`SparseError::SingularMatrix`] if a zero pivot is encountered.
256    pub fn parallel_ilu0<T: GpuFloat>(
257        &self,
258        _handle: &SparseHandle,
259        matrix: &CsrMatrix<T>,
260    ) -> SparseResult<(CsrMatrix<T>, CsrMatrix<T>)> {
261        let n = matrix.rows() as usize;
262        if n == 0 {
263            return Err(SparseError::InvalidArgument(
264                "cannot factor an empty matrix".to_string(),
265            ));
266        }
267
268        let (h_row_ptr, h_col_idx, h_values) = matrix.to_host()?;
269        let mut work = h_values;
270
271        // Process color by color.
272        for color in 0..self.num_colors {
273            let rows = self.rows_for_color(color);
274
275            // All rows in this color are independent — can be parallelized.
276            for &row_u32 in rows {
277                let row = row_u32 as usize;
278                let row_start = h_row_ptr[row] as usize;
279                let row_end = h_row_ptr[row + 1] as usize;
280
281                for nz in row_start..row_end {
282                    let k = h_col_idx[nz] as usize;
283                    if k >= row {
284                        break;
285                    }
286
287                    // Find diagonal of row k.
288                    let k_start = h_row_ptr[k] as usize;
289                    let k_end = h_row_ptr[k + 1] as usize;
290                    let diag_pos = find_col_in_row(&h_col_idx[k_start..k_end], k as i32);
291                    let diag_pos = match diag_pos {
292                        Some(pos) => k_start + pos,
293                        None => return Err(SparseError::SingularMatrix),
294                    };
295
296                    let a_kk = work[diag_pos];
297                    if a_kk == T::gpu_zero() {
298                        return Err(SparseError::SingularMatrix);
299                    }
300
301                    // a_ik /= a_kk
302                    let ratio = div_gpu_float(work[nz], a_kk);
303                    work[nz] = ratio;
304
305                    // Update: a_ij -= ratio * a_kj for j > k
306                    for k_nz in (diag_pos + 1)..k_end {
307                        let j = h_col_idx[k_nz];
308                        if let Some(ij_off) = find_col_in_row(&h_col_idx[row_start..row_end], j) {
309                            let ij_pos = row_start + ij_off;
310                            let update = mul_gpu_float(ratio, work[k_nz]);
311                            work[ij_pos] = sub_gpu_float(work[ij_pos], update);
312                        }
313                    }
314                }
315            }
316        }
317
318        // Split into L and U.
319        split_lu(&h_row_ptr, &h_col_idx, &work, matrix.rows())
320    }
321
322    /// Validates the coloring: checks that no two vertices within distance 2
323    /// share the same color.
324    ///
325    /// Returns `true` if the coloring is valid.
326    pub fn validate(&self, row_ptr: &[i32], col_idx: &[i32], n: usize) -> bool {
327        for i in 0..n {
328            let row_start = row_ptr[i] as usize;
329            let row_end = row_ptr[i + 1] as usize;
330            let ci = self.colors[i];
331
332            // Check distance-1 neighbors.
333            for nz in row_start..row_end {
334                let j = col_idx[nz] as usize;
335                if j == i || j >= n {
336                    continue;
337                }
338                if self.colors[j] == ci {
339                    return false;
340                }
341
342                // Check distance-2 neighbors.
343                let j_start = row_ptr[j] as usize;
344                let j_end = row_ptr[j + 1] as usize;
345                for &col in &col_idx[j_start..j_end] {
346                    let k = col as usize;
347                    if k == i || k >= n {
348                        continue;
349                    }
350                    if self.colors[k] == ci {
351                        return false;
352                    }
353                }
354            }
355        }
356        true
357    }
358}
359
360// ---------------------------------------------------------------------------
361// Helpers
362// ---------------------------------------------------------------------------
363
364fn find_col_in_row(col_slice: &[i32], target_col: i32) -> Option<usize> {
365    col_slice.iter().position(|&c| c == target_col)
366}
367
368fn div_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
369    let fa = to_f64(a);
370    let fb = to_f64(b);
371    from_f64::<T>(fa / fb)
372}
373
374fn mul_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
375    let fa = to_f64(a);
376    let fb = to_f64(b);
377    from_f64::<T>(fa * fb)
378}
379
380fn sub_gpu_float<T: GpuFloat>(a: T, b: T) -> T {
381    let fa = to_f64(a);
382    let fb = to_f64(b);
383    from_f64::<T>(fa - fb)
384}
385
386/// Splits factored values into L (unit lower triangular) and U (upper + diagonal).
387fn split_lu<T: GpuFloat>(
388    row_ptr: &[i32],
389    col_idx: &[i32],
390    values: &[T],
391    n: u32,
392) -> SparseResult<(CsrMatrix<T>, CsrMatrix<T>)> {
393    let n_usize = n as usize;
394
395    let mut l_nnz = 0usize;
396    let mut u_nnz = 0usize;
397    for i in 0..n_usize {
398        let start = row_ptr[i] as usize;
399        let end = row_ptr[i + 1] as usize;
400        for &cj in &col_idx[start..end] {
401            let j = cj as usize;
402            if j < i {
403                l_nnz += 1;
404            } else {
405                u_nnz += 1;
406            }
407        }
408        l_nnz += 1; // unit diagonal for L
409    }
410
411    let mut l_row_ptr = vec![0i32; n_usize + 1];
412    let mut l_col_idx = Vec::with_capacity(l_nnz);
413    let mut l_values = Vec::with_capacity(l_nnz);
414
415    let mut u_row_ptr = vec![0i32; n_usize + 1];
416    let mut u_col_idx = Vec::with_capacity(u_nnz);
417    let mut u_values = Vec::with_capacity(u_nnz);
418
419    for i in 0..n_usize {
420        let start = row_ptr[i] as usize;
421        let end = row_ptr[i + 1] as usize;
422
423        for idx in start..end {
424            let j = col_idx[idx] as usize;
425            if j < i {
426                l_col_idx.push(col_idx[idx]);
427                l_values.push(values[idx]);
428            }
429        }
430        l_col_idx.push(i as i32);
431        l_values.push(T::gpu_one());
432        l_row_ptr[i + 1] = l_col_idx.len() as i32;
433
434        for idx in start..end {
435            let j = col_idx[idx] as usize;
436            if j >= i {
437                u_col_idx.push(col_idx[idx]);
438                u_values.push(values[idx]);
439            }
440        }
441        u_row_ptr[i + 1] = u_col_idx.len() as i32;
442    }
443
444    let l_mat = CsrMatrix::from_host(n, n, &l_row_ptr, &l_col_idx, &l_values)?;
445    let u_mat = CsrMatrix::from_host(n, n, &u_row_ptr, &u_col_idx, &u_values)?;
446
447    Ok((l_mat, u_mat))
448}
449
450// ---------------------------------------------------------------------------
451// Tests
452// ---------------------------------------------------------------------------
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    // --- Greedy coloring tests on host-side structures ---
459
460    #[test]
461    fn coloring_diagonal_matrix() {
462        // Diagonal matrix: no off-diagonal neighbors => all rows get color 0.
463        let row_ptr = vec![0, 1, 2, 3];
464        let col_idx = vec![0, 1, 2];
465        let n = 3;
466
467        let coloring = GraphColoring::from_csr_host(&row_ptr, &col_idx, n);
468        assert!(coloring.is_ok());
469        let coloring = match coloring {
470            Ok(v) => v,
471            Err(e) => panic!("test: {e}"),
472        };
473        assert_eq!(coloring.num_colors, 1);
474        for &c in &coloring.colors {
475            assert_eq!(c, 0);
476        }
477    }
478
479    #[test]
480    fn coloring_tridiagonal() {
481        // Tridiagonal 4x4:
482        // Row 0: cols [0, 1]
483        // Row 1: cols [0, 1, 2]
484        // Row 2: cols [1, 2, 3]
485        // Row 3: cols [2, 3]
486        let row_ptr = vec![0, 2, 5, 8, 10];
487        let col_idx = vec![0, 1, 0, 1, 2, 1, 2, 3, 2, 3];
488        let n = 4;
489
490        let coloring = GraphColoring::from_csr_host(&row_ptr, &col_idx, n);
491        assert!(coloring.is_ok());
492        let coloring = match coloring {
493            Ok(v) => v,
494            Err(e) => panic!("test: {e}"),
495        };
496
497        // Distance-2 coloring of a path graph needs at least 3 colors.
498        assert!(coloring.num_colors >= 3, "need >= 3 colors for path graph");
499
500        // Validate the coloring.
501        assert!(coloring.validate(&row_ptr, &col_idx, n));
502    }
503
504    #[test]
505    fn coloring_dense_small() {
506        // Fully connected 3x3 (all entries non-zero):
507        // Distance-2 coloring of K_3 needs 3 colors.
508        let row_ptr = vec![0, 3, 6, 9];
509        let col_idx = vec![0, 1, 2, 0, 1, 2, 0, 1, 2];
510        let n = 3;
511
512        let coloring = GraphColoring::from_csr_host(&row_ptr, &col_idx, n);
513        assert!(coloring.is_ok());
514        let coloring = match coloring {
515            Ok(v) => v,
516            Err(e) => panic!("test: {e}"),
517        };
518        assert_eq!(coloring.num_colors, 3);
519        assert!(coloring.validate(&row_ptr, &col_idx, n));
520    }
521
522    #[test]
523    fn coloring_rows_for_color() {
524        let row_ptr = vec![0, 1, 2, 3];
525        let col_idx = vec![0, 1, 2];
526        let n = 3;
527
528        let coloring = match GraphColoring::from_csr_host(&row_ptr, &col_idx, n) {
529            Ok(v) => v,
530            Err(e) => panic!("test: {e}"),
531        };
532
533        // All rows should be in color 0.
534        let rows = coloring.rows_for_color(0);
535        assert_eq!(rows.len(), 3);
536
537        // No rows in color 1.
538        let rows = coloring.rows_for_color(1);
539        assert_eq!(rows.len(), 0);
540    }
541
542    #[test]
543    fn coloring_single_node() {
544        // Single node with self-loop.
545        let row_ptr = vec![0, 1];
546        let col_idx = vec![0];
547        let n = 1;
548
549        let coloring = GraphColoring::from_csr_host(&row_ptr, &col_idx, n);
550        assert!(coloring.is_ok());
551        let coloring = match coloring {
552            Ok(v) => v,
553            Err(e) => panic!("test: {e}"),
554        };
555        assert_eq!(coloring.num_colors, 1);
556        assert_eq!(coloring.colors[0], 0);
557        assert_eq!(coloring.color_order.len(), 1);
558    }
559
560    #[test]
561    fn coloring_color_offsets_consistency() {
562        // 5x5 banded matrix
563        let row_ptr = vec![0, 2, 5, 8, 11, 13];
564        let col_idx = vec![0, 1, 0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4];
565        let n = 5;
566
567        let coloring = match GraphColoring::from_csr_host(&row_ptr, &col_idx, n) {
568            Ok(v) => v,
569            Err(e) => panic!("test: {e}"),
570        };
571
572        // Check that color_offsets is consistent with color_order.
573        let mut total = 0;
574        for c in 0..coloring.num_colors {
575            let count = coloring.rows_for_color(c).len();
576            total += count;
577        }
578        assert_eq!(total, n);
579
580        // Last offset should equal n.
581        assert_eq!(
582            coloring.color_offsets[coloring.num_colors], n,
583            "last color offset should equal n"
584        );
585
586        // Validate coloring.
587        assert!(coloring.validate(&row_ptr, &col_idx, n));
588    }
589
590    #[test]
591    fn coloring_validate_detects_bad_coloring() {
592        // Manually create a BAD coloring for a tridiagonal matrix.
593        let row_ptr = vec![0, 2, 4, 6];
594        let col_idx = vec![0, 1, 1, 2, 0, 2]; // row0=[0,1], row1=[1,2], row2=[0,2]
595        let n = 3;
596
597        // All same color => distance-1 conflict.
598        let bad_coloring = GraphColoring {
599            num_colors: 1,
600            colors: vec![0, 0, 0],
601            color_offsets: vec![0, 3],
602            color_order: vec![0, 1, 2],
603        };
604        assert!(!bad_coloring.validate(&row_ptr, &col_idx, n));
605    }
606
607    // --- Arithmetic helpers ---
608
609    #[test]
610    fn host_float_arithmetic_f32() {
611        let a = 6.0_f32;
612        let b = 2.0_f32;
613        let result = div_gpu_float(a, b);
614        assert!((result - 3.0_f32).abs() < 1e-6);
615
616        let result = mul_gpu_float(a, b);
617        assert!((result - 12.0_f32).abs() < 1e-6);
618
619        let result = sub_gpu_float(a, b);
620        assert!((result - 4.0_f32).abs() < 1e-6);
621    }
622
623    #[test]
624    fn host_float_arithmetic_f64() {
625        let a = 6.0_f64;
626        let b = 2.0_f64;
627        assert!((div_gpu_float(a, b) - 3.0_f64).abs() < 1e-12);
628        assert!((mul_gpu_float(a, b) - 12.0_f64).abs() < 1e-12);
629        assert!((sub_gpu_float(a, b) - 4.0_f64).abs() < 1e-12);
630    }
631
632    #[test]
633    fn find_col_in_row_works() {
634        let cols = [0, 2, 5, 7];
635        assert_eq!(find_col_in_row(&cols, 2), Some(1));
636        assert_eq!(find_col_in_row(&cols, 3), None);
637        assert_eq!(find_col_in_row(&cols, 7), Some(3));
638    }
639}