oxicuda_sparse/preconditioner/
ic0.rs1#![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
27pub 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 let (h_row_ptr, h_col_idx, h_values) = a.to_host()?;
60
61 let levels = analyze_ic0_levels(&h_row_ptr, &h_col_idx, n)?;
63
64 let _ptx_result = emit_ic0_kernel::<T>(handle.sm_version());
66
67 let mut work = h_values;
69
70 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 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 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 let a_ik = div_gpu_float(work[nz], l_kk);
100 work[nz] = a_ik;
101
102 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 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(&h_row_ptr, &h_col_idx, &work, n)
132}
133
134fn find_col_in_row(col_slice: &[i32], target_col: i32) -> Option<usize> {
136 col_slice.iter().position(|&c| c == target_col)
137}
138
139fn 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
173fn 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
207fn 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 let _row_addr = b.byte_offset_addr(level_rows_ptr, tid, 4);
232 });
235
236 b.ret();
237 })
238 .build()
239 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
240}
241
242fn 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
253fn 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
262fn 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
269fn 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
276fn 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 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}