1#![allow(dead_code)]
17
18use std::sync::Arc;
19
20use oxicuda_blas::GpuFloat;
21use oxicuda_driver::Module;
22use oxicuda_driver::ffi::CUdeviceptr;
23use oxicuda_launch::{Kernel, LaunchParams, grid_size_for};
24use oxicuda_ptx::arch::SmVersion;
25use oxicuda_ptx::builder::KernelBuilder;
26use oxicuda_ptx::ir::PtxType;
27
28use crate::error::{SparseError, SparseResult};
29use crate::format::CsrMatrix;
30use crate::handle::SparseHandle;
31use crate::ptx_helpers::{fma_float, load_float_imm, load_global_float, store_global_float};
32
33const SPGEMM_BLOCK_SIZE: u32 = 256;
35
36const HASH_TABLE_SIZE: u32 = 512;
40
41pub fn spgemm_symbolic<T: GpuFloat>(
58 handle: &SparseHandle,
59 a: &CsrMatrix<T>,
60 b: &CsrMatrix<T>,
61) -> SparseResult<Vec<i32>> {
62 validate_spgemm_dims(a, b)?;
63
64 let m = a.rows();
65 if m == 0 {
66 return Ok(vec![0]);
67 }
68
69 let d_row_nnz = oxicuda_memory::DeviceBuffer::<i32>::zeroed(m as usize)?;
71
72 let ptx = emit_spgemm_symbolic_kernel::<T>(handle.sm_version())?;
73 let module = Arc::new(Module::from_ptx(&ptx)?);
74 let kernel = Kernel::from_module(module, "spgemm_symbolic")?;
75
76 let block_size = SPGEMM_BLOCK_SIZE;
77 let grid_size = grid_size_for(m, block_size);
78 let params = LaunchParams::new(grid_size, block_size);
79
80 kernel.launch(
81 ¶ms,
82 handle.stream(),
83 &(
84 a.row_ptr().as_device_ptr(),
85 a.col_idx().as_device_ptr(),
86 b.row_ptr().as_device_ptr(),
87 b.col_idx().as_device_ptr(),
88 d_row_nnz.as_device_ptr(),
89 m,
90 b.cols(),
91 ),
92 )?;
93
94 let mut h_row_nnz = vec![0i32; m as usize];
96 d_row_nnz.copy_to_host(&mut h_row_nnz)?;
97
98 let mut row_ptr = vec![0i32; m as usize + 1];
99 for i in 0..m as usize {
100 row_ptr[i + 1] = row_ptr[i] + h_row_nnz[i];
101 }
102
103 Ok(row_ptr)
104}
105
106#[allow(clippy::too_many_arguments)]
125pub fn spgemm_numeric<T: GpuFloat>(
126 handle: &SparseHandle,
127 a: &CsrMatrix<T>,
128 b: &CsrMatrix<T>,
129 c_row_ptr: CUdeviceptr,
130 c_col_idx: CUdeviceptr,
131 c_values: CUdeviceptr,
132) -> SparseResult<()> {
133 validate_spgemm_dims(a, b)?;
134
135 let m = a.rows();
136 if m == 0 {
137 return Ok(());
138 }
139
140 let ptx = emit_spgemm_numeric_kernel::<T>(handle.sm_version())?;
141 let module = Arc::new(Module::from_ptx(&ptx)?);
142 let kernel = Kernel::from_module(module, "spgemm_numeric")?;
143
144 let block_size = SPGEMM_BLOCK_SIZE;
145 let grid_size = grid_size_for(m, block_size);
146 let params = LaunchParams::new(grid_size, block_size);
147
148 kernel.launch(
149 ¶ms,
150 handle.stream(),
151 &(
152 a.row_ptr().as_device_ptr(),
153 a.col_idx().as_device_ptr(),
154 a.values().as_device_ptr(),
155 b.row_ptr().as_device_ptr(),
156 b.col_idx().as_device_ptr(),
157 b.values().as_device_ptr(),
158 c_row_ptr,
159 c_col_idx,
160 c_values,
161 m,
162 b.cols(),
163 ),
164 )?;
165
166 Ok(())
167}
168
169fn validate_spgemm_dims<T: GpuFloat>(a: &CsrMatrix<T>, b: &CsrMatrix<T>) -> SparseResult<()> {
171 if a.cols() != b.rows() {
172 return Err(SparseError::DimensionMismatch(format!(
173 "A.cols ({}) != B.rows ({})",
174 a.cols(),
175 b.rows()
176 )));
177 }
178 Ok(())
179}
180
181fn emit_spgemm_symbolic_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
187 let _ = T::PTX_TYPE; KernelBuilder::new("spgemm_symbolic")
190 .target(sm)
191 .param("a_row_ptr", PtxType::U64)
192 .param("a_col_idx", PtxType::U64)
193 .param("b_row_ptr", PtxType::U64)
194 .param("b_col_idx", PtxType::U64)
195 .param("row_nnz", PtxType::U64)
196 .param("m", PtxType::U32)
197 .param("n", PtxType::U32)
198 .body(move |b| {
199 let gid = b.global_thread_id_x();
200 let m_param = b.load_param_u32("m");
201
202 let gid_inner = gid.clone();
203 b.if_lt_u32(gid, m_param, move |b| {
204 let row = gid_inner;
205 let a_row_ptr = b.load_param_u64("a_row_ptr");
206 let a_col_idx = b.load_param_u64("a_col_idx");
207 let b_row_ptr = b.load_param_u64("b_row_ptr");
208 let _b_col_idx = b.load_param_u64("b_col_idx");
209 let row_nnz_ptr = b.load_param_u64("row_nnz");
210
211 let a_rs_addr = b.byte_offset_addr(a_row_ptr.clone(), row.clone(), 4);
213 let a_rs_i32 = b.load_global_i32(a_rs_addr);
214 let a_rs = b.alloc_reg(PtxType::U32);
215 b.raw_ptx(&format!("mov.b32 {a_rs}, {a_rs_i32};"));
216
217 let row_p1 = b.alloc_reg(PtxType::U32);
218 b.raw_ptx(&format!("add.u32 {row_p1}, {row}, 1;"));
219 let a_re_addr = b.byte_offset_addr(a_row_ptr, row_p1, 4);
220 let a_re_i32 = b.load_global_i32(a_re_addr);
221 let a_re = b.alloc_reg(PtxType::U32);
222 b.raw_ptx(&format!("mov.b32 {a_re}, {a_re_i32};"));
223
224 let count = b.alloc_reg(PtxType::U32);
226 b.raw_ptx(&format!("mov.u32 {count}, 0;"));
227
228 let a_k = b.alloc_reg(PtxType::U32);
230 b.raw_ptx(&format!("mov.u32 {a_k}, {a_rs};"));
231
232 let outer_loop = b.fresh_label("spgemm_sym_outer");
233 let outer_done = b.fresh_label("spgemm_sym_outer_done");
234
235 b.label(&outer_loop);
236 let a_pred = b.alloc_reg(PtxType::Pred);
237 b.raw_ptx(&format!("setp.lo.u32 {a_pred}, {a_k}, {a_re};"));
238 b.raw_ptx(&format!("@!{a_pred} bra {outer_done};"));
239
240 let a_ci_addr = b.byte_offset_addr(a_col_idx.clone(), a_k.clone(), 4);
242 let a_col_i32 = b.load_global_i32(a_ci_addr);
243 let a_col = b.alloc_reg(PtxType::U32);
244 b.raw_ptx(&format!("mov.b32 {a_col}, {a_col_i32};"));
245
246 let b_rs_addr = b.byte_offset_addr(b_row_ptr.clone(), a_col.clone(), 4);
248 let b_rs_i32 = b.load_global_i32(b_rs_addr);
249 let b_rs = b.alloc_reg(PtxType::U32);
250 b.raw_ptx(&format!("mov.b32 {b_rs}, {b_rs_i32};"));
251
252 let a_col_p1 = b.alloc_reg(PtxType::U32);
253 b.raw_ptx(&format!("add.u32 {a_col_p1}, {a_col}, 1;"));
254 let b_re_addr = b.byte_offset_addr(b_row_ptr.clone(), a_col_p1, 4);
255 let b_re_i32 = b.load_global_i32(b_re_addr);
256 let b_re = b.alloc_reg(PtxType::U32);
257 b.raw_ptx(&format!("mov.b32 {b_re}, {b_re_i32};"));
258
259 let b_j = b.alloc_reg(PtxType::U32);
261 b.raw_ptx(&format!("mov.u32 {b_j}, {b_rs};"));
262
263 let inner_loop = b.fresh_label("spgemm_sym_inner");
264 let inner_done = b.fresh_label("spgemm_sym_inner_done");
265
266 b.label(&inner_loop);
267 let b_pred = b.alloc_reg(PtxType::Pred);
268 b.raw_ptx(&format!("setp.lo.u32 {b_pred}, {b_j}, {b_re};"));
269 b.raw_ptx(&format!("@!{b_pred} bra {inner_done};"));
270
271 b.raw_ptx(&format!("add.u32 {count}, {count}, 1;"));
276
277 b.raw_ptx(&format!("add.u32 {b_j}, {b_j}, 1;"));
278 b.branch(&inner_loop);
279 b.label(&inner_done);
280
281 b.raw_ptx(&format!("add.u32 {a_k}, {a_k}, 1;"));
282 b.branch(&outer_loop);
283 b.label(&outer_done);
284
285 let out_addr = b.byte_offset_addr(row_nnz_ptr, row, 4);
287 b.store_global_i32(out_addr, count);
288 });
289
290 b.ret();
291 })
292 .build()
293 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
294}
295
296fn emit_spgemm_numeric_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
302 let elem_bytes = T::size_u32();
303 let _is_f64 = T::SIZE == 8;
304
305 KernelBuilder::new("spgemm_numeric")
306 .target(sm)
307 .param("a_row_ptr", PtxType::U64)
308 .param("a_col_idx", PtxType::U64)
309 .param("a_values", PtxType::U64)
310 .param("b_row_ptr", PtxType::U64)
311 .param("b_col_idx", PtxType::U64)
312 .param("b_values", PtxType::U64)
313 .param("c_row_ptr", PtxType::U64)
314 .param("c_col_idx", PtxType::U64)
315 .param("c_values", PtxType::U64)
316 .param("m", PtxType::U32)
317 .param("n", PtxType::U32)
318 .body(move |b| {
319 let gid = b.global_thread_id_x();
320 let m_param = b.load_param_u32("m");
321
322 let gid_inner = gid.clone();
323 b.if_lt_u32(gid, m_param, move |b| {
324 let row = gid_inner;
325 let a_row_ptr = b.load_param_u64("a_row_ptr");
326 let a_col_idx = b.load_param_u64("a_col_idx");
327 let a_values = b.load_param_u64("a_values");
328 let b_row_ptr = b.load_param_u64("b_row_ptr");
329 let b_col_idx_p = b.load_param_u64("b_col_idx");
330 let b_values = b.load_param_u64("b_values");
331 let c_row_ptr = b.load_param_u64("c_row_ptr");
332 let c_col_idx_p = b.load_param_u64("c_col_idx");
333 let c_values = b.load_param_u64("c_values");
334
335 let a_rs_addr = b.byte_offset_addr(a_row_ptr.clone(), row.clone(), 4);
337 let a_rs_i32 = b.load_global_i32(a_rs_addr);
338 let a_rs = b.alloc_reg(PtxType::U32);
339 b.raw_ptx(&format!("mov.b32 {a_rs}, {a_rs_i32};"));
340
341 let row_p1 = b.alloc_reg(PtxType::U32);
342 b.raw_ptx(&format!("add.u32 {row_p1}, {row}, 1;"));
343 let a_re_addr = b.byte_offset_addr(a_row_ptr, row_p1, 4);
344 let a_re_i32 = b.load_global_i32(a_re_addr);
345 let a_re = b.alloc_reg(PtxType::U32);
346 b.raw_ptx(&format!("mov.b32 {a_re}, {a_re_i32};"));
347
348 let c_rs_addr = b.byte_offset_addr(c_row_ptr, row, 4);
350 let c_rs_i32 = b.load_global_i32(c_rs_addr);
351 let c_pos = b.alloc_reg(PtxType::U32);
352 b.raw_ptx(&format!("mov.b32 {c_pos}, {c_rs_i32};"));
353
354 let a_k = b.alloc_reg(PtxType::U32);
356 b.raw_ptx(&format!("mov.u32 {a_k}, {a_rs};"));
357
358 let outer_loop = b.fresh_label("spgemm_num_outer");
359 let outer_done = b.fresh_label("spgemm_num_outer_done");
360
361 b.label(&outer_loop);
362 let a_pred = b.alloc_reg(PtxType::Pred);
363 b.raw_ptx(&format!("setp.lo.u32 {a_pred}, {a_k}, {a_re};"));
364 b.raw_ptx(&format!("@!{a_pred} bra {outer_done};"));
365
366 let a_ci_addr = b.byte_offset_addr(a_col_idx.clone(), a_k.clone(), 4);
368 let a_col_i32 = b.load_global_i32(a_ci_addr);
369 let a_col = b.alloc_reg(PtxType::U32);
370 b.raw_ptx(&format!("mov.b32 {a_col}, {a_col_i32};"));
371
372 let a_v_addr = b.byte_offset_addr(a_values.clone(), a_k.clone(), elem_bytes);
373 let a_val = load_global_float::<T>(b, a_v_addr);
374
375 let b_rs_addr = b.byte_offset_addr(b_row_ptr.clone(), a_col.clone(), 4);
377 let b_rs_i32 = b.load_global_i32(b_rs_addr);
378 let b_rs = b.alloc_reg(PtxType::U32);
379 b.raw_ptx(&format!("mov.b32 {b_rs}, {b_rs_i32};"));
380
381 let a_col_p1 = b.alloc_reg(PtxType::U32);
382 b.raw_ptx(&format!("add.u32 {a_col_p1}, {a_col}, 1;"));
383 let b_re_addr = b.byte_offset_addr(b_row_ptr.clone(), a_col_p1, 4);
384 let b_re_i32 = b.load_global_i32(b_re_addr);
385 let b_re = b.alloc_reg(PtxType::U32);
386 b.raw_ptx(&format!("mov.b32 {b_re}, {b_re_i32};"));
387
388 let b_j = b.alloc_reg(PtxType::U32);
390 b.raw_ptx(&format!("mov.u32 {b_j}, {b_rs};"));
391
392 let inner_loop = b.fresh_label("spgemm_num_inner");
393 let inner_done = b.fresh_label("spgemm_num_inner_done");
394
395 b.label(&inner_loop);
396 let b_pred = b.alloc_reg(PtxType::Pred);
397 b.raw_ptx(&format!("setp.lo.u32 {b_pred}, {b_j}, {b_re};"));
398 b.raw_ptx(&format!("@!{b_pred} bra {inner_done};"));
399
400 let b_ci_addr = b.byte_offset_addr(b_col_idx_p.clone(), b_j.clone(), 4);
402 let b_col_i32 = b.load_global_i32(b_ci_addr);
403
404 let b_v_addr = b.byte_offset_addr(b_values.clone(), b_j.clone(), elem_bytes);
405 let b_val = load_global_float::<T>(b, b_v_addr);
406
407 let zero = load_float_imm::<T>(b, 0.0);
409 let c_val = fma_float::<T>(b, a_val.clone(), b_val, zero);
410
411 let c_ci_addr = b.byte_offset_addr(c_col_idx_p.clone(), c_pos.clone(), 4);
413 b.store_global_i32(c_ci_addr, b_col_i32);
414
415 let c_v_addr = b.byte_offset_addr(c_values.clone(), c_pos.clone(), elem_bytes);
417 store_global_float::<T>(b, c_v_addr, c_val);
418
419 b.raw_ptx(&format!("add.u32 {c_pos}, {c_pos}, 1;"));
421
422 b.raw_ptx(&format!("add.u32 {b_j}, {b_j}, 1;"));
423 b.branch(&inner_loop);
424 b.label(&inner_done);
425
426 b.raw_ptx(&format!("add.u32 {a_k}, {a_k}, 1;"));
427 b.branch(&outer_loop);
428 b.label(&outer_done);
429 });
430
431 b.ret();
432 })
433 .build()
434 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use oxicuda_ptx::arch::SmVersion;
441
442 #[test]
443 fn spgemm_symbolic_ptx_generates_f32() {
444 let ptx = emit_spgemm_symbolic_kernel::<f32>(SmVersion::Sm80);
445 assert!(ptx.is_ok());
446 let ptx_str = ptx.expect("test: PTX gen should succeed");
447 assert!(ptx_str.contains(".entry spgemm_symbolic"));
448 }
449
450 #[test]
451 fn spgemm_symbolic_ptx_generates_f64() {
452 let ptx = emit_spgemm_symbolic_kernel::<f64>(SmVersion::Sm80);
453 assert!(ptx.is_ok());
454 }
455
456 #[test]
457 fn spgemm_numeric_ptx_generates_f32() {
458 let ptx = emit_spgemm_numeric_kernel::<f32>(SmVersion::Sm80);
459 assert!(ptx.is_ok());
460 let ptx_str = ptx.expect("test: PTX gen should succeed");
461 assert!(ptx_str.contains(".entry spgemm_numeric"));
462 }
463
464 #[test]
465 fn spgemm_numeric_ptx_generates_f64() {
466 let ptx = emit_spgemm_numeric_kernel::<f64>(SmVersion::Sm80);
467 assert!(ptx.is_ok());
468 }
469
470 #[test]
471 fn validate_dims_mismatch() {
472 let err = SparseError::DimensionMismatch("A.cols (3) != B.rows (4)".to_string());
474 assert!(err.to_string().contains("A.cols"));
475 }
476}