1#![allow(dead_code)]
15
16use std::sync::Arc;
17
18use oxicuda_blas::GpuFloat;
19use oxicuda_driver::Module;
20use oxicuda_driver::ffi::CUdeviceptr;
21use oxicuda_launch::{Kernel, LaunchParams, grid_size_for};
22use oxicuda_ptx::arch::SmVersion;
23use oxicuda_ptx::builder::KernelBuilder;
24use oxicuda_ptx::ir::PtxType;
25
26use crate::error::{SparseError, SparseResult};
27use crate::format::CsrMatrix;
28use crate::handle::SparseHandle;
29use crate::ptx_helpers::{
30 add_float, fma_float, load_float_imm, load_global_float, mul_float, reinterpret_bits_to_float,
31 store_global_float,
32};
33
34const SDDMM_BLOCK_SIZE: u32 = 256;
36
37#[allow(clippy::too_many_arguments)]
61pub fn sddmm<T: GpuFloat>(
62 handle: &SparseHandle,
63 alpha: T,
64 a_ptr: CUdeviceptr,
65 a_rows: u32,
66 a_cols: u32,
67 a_ld: u32,
68 b_ptr: CUdeviceptr,
69 b_cols: u32,
70 b_ld: u32,
71 beta: T,
72 s: &mut CsrMatrix<T>,
73) -> SparseResult<()> {
74 if s.rows() != a_rows {
76 return Err(SparseError::DimensionMismatch(format!(
77 "S.rows ({}) != A.rows ({})",
78 s.rows(),
79 a_rows
80 )));
81 }
82 if s.cols() != b_cols {
83 return Err(SparseError::DimensionMismatch(format!(
84 "S.cols ({}) != B.cols ({})",
85 s.cols(),
86 b_cols
87 )));
88 }
89
90 if s.nnz() == 0 || a_cols == 0 {
91 return Ok(());
92 }
93
94 let ptx = emit_sddmm_kernel::<T>(handle.sm_version())?;
95 let module = Arc::new(Module::from_ptx(&ptx)?);
96 let kernel = Kernel::from_module(module, "sddmm")?;
97
98 let block_size = SDDMM_BLOCK_SIZE;
99 let grid_size = grid_size_for(s.nnz(), block_size);
100 let params = LaunchParams::new(grid_size, block_size);
101
102 kernel.launch(
103 ¶ms,
104 handle.stream(),
105 &(
106 s.row_ptr().as_device_ptr(),
107 s.col_idx().as_device_ptr(),
108 s.values().as_device_ptr(),
109 a_ptr,
110 b_ptr,
111 alpha.to_bits_u64(),
112 beta.to_bits_u64(),
113 s.rows(),
114 a_cols,
115 a_ld,
116 b_ld,
117 ),
118 )?;
119
120 Ok(())
121}
122
123fn emit_sddmm_kernel<T: GpuFloat>(sm: SmVersion) -> SparseResult<String> {
129 let elem_bytes = T::size_u32();
130 let is_f64 = T::SIZE == 8;
131
132 KernelBuilder::new("sddmm")
133 .target(sm)
134 .param("row_ptr", PtxType::U64)
135 .param("col_idx", PtxType::U64)
136 .param("values", PtxType::U64)
137 .param("a_ptr", PtxType::U64)
138 .param("b_ptr", PtxType::U64)
139 .param("alpha_bits", PtxType::U64)
140 .param("beta_bits", PtxType::U64)
141 .param("m", PtxType::U32)
142 .param("k", PtxType::U32)
143 .param("a_ld", PtxType::U32)
144 .param("b_ld", PtxType::U32)
145 .body(move |b| {
146 let gid = b.global_thread_id_x();
147 let m_param = b.load_param_u32("m");
148 let mov_suffix = if is_f64 { "f64" } else { "f32" };
149
150 let gid_inner = gid.clone();
158 b.if_lt_u32(gid, m_param, move |b| {
159 let row = gid_inner;
160 let row_ptr_base = b.load_param_u64("row_ptr");
161 let col_idx_base = b.load_param_u64("col_idx");
162 let values_base = b.load_param_u64("values");
163 let a_ptr = b.load_param_u64("a_ptr");
164 let b_ptr = b.load_param_u64("b_ptr");
165 let alpha_bits = b.load_param_u64("alpha_bits");
166 let beta_bits = b.load_param_u64("beta_bits");
167 let k_param = b.load_param_u32("k");
168 let a_ld = b.load_param_u32("a_ld");
169 let b_ld = b.load_param_u32("b_ld");
170
171 let alpha = reinterpret_bits_to_float::<T>(b, alpha_bits);
172 let beta = reinterpret_bits_to_float::<T>(b, beta_bits);
173
174 let rp_addr = b.byte_offset_addr(row_ptr_base.clone(), row.clone(), 4);
176 let rs_i32 = b.load_global_i32(rp_addr);
177 let rs = b.alloc_reg(PtxType::U32);
178 b.raw_ptx(&format!("mov.b32 {rs}, {rs_i32};"));
179
180 let row_p1 = b.alloc_reg(PtxType::U32);
181 b.raw_ptx(&format!("add.u32 {row_p1}, {row}, 1;"));
182 let re_addr = b.byte_offset_addr(row_ptr_base, row_p1, 4);
183 let re_i32 = b.load_global_i32(re_addr);
184 let re = b.alloc_reg(PtxType::U32);
185 b.raw_ptx(&format!("mov.b32 {re}, {re_i32};"));
186
187 let nz_idx = b.alloc_reg(PtxType::U32);
189 b.raw_ptx(&format!("mov.u32 {nz_idx}, {rs};"));
190
191 let nz_loop = b.fresh_label("sddmm_nz_loop");
192 let nz_done = b.fresh_label("sddmm_nz_done");
193
194 b.label(&nz_loop);
195 let nz_pred = b.alloc_reg(PtxType::Pred);
198 b.raw_ptx(&format!("setp.hs.u32 {nz_pred}, {nz_idx}, {re};"));
199 b.branch_if(nz_pred, &nz_done);
200
201 let ci_addr = b.byte_offset_addr(col_idx_base.clone(), nz_idx.clone(), 4);
203 let col_i32 = b.load_global_i32(ci_addr);
204 let col = b.alloc_reg(PtxType::U32);
205 b.raw_ptx(&format!("mov.b32 {col}, {col_i32};"));
206
207 let dot = load_float_imm::<T>(b, 0.0);
209
210 let kk = b.alloc_reg(PtxType::U32);
211 b.raw_ptx(&format!("mov.u32 {kk}, 0;"));
212
213 let k_loop = b.fresh_label("sddmm_k_loop");
214 let k_done = b.fresh_label("sddmm_k_done");
215
216 b.label(&k_loop);
217 let k_pred = b.alloc_reg(PtxType::Pred);
219 b.raw_ptx(&format!("setp.hs.u32 {k_pred}, {kk}, {k_param};"));
220 b.branch_if(k_pred, &k_done);
221
222 let a_row_off = b.alloc_reg(PtxType::U32);
224 b.raw_ptx(&format!("mul.lo.u32 {a_row_off}, {row}, {a_ld};"));
225 let a_idx = b.alloc_reg(PtxType::U32);
226 b.raw_ptx(&format!("add.u32 {a_idx}, {a_row_off}, {kk};"));
227 let a_addr = b.byte_offset_addr(a_ptr.clone(), a_idx, elem_bytes);
228 let a_val = load_global_float::<T>(b, a_addr);
229
230 let b_row_off = b.alloc_reg(PtxType::U32);
232 b.raw_ptx(&format!("mul.lo.u32 {b_row_off}, {kk}, {b_ld};"));
233 let b_idx = b.alloc_reg(PtxType::U32);
234 b.raw_ptx(&format!("add.u32 {b_idx}, {b_row_off}, {col};"));
235 let b_addr = b.byte_offset_addr(b_ptr.clone(), b_idx, elem_bytes);
236 let b_val = load_global_float::<T>(b, b_addr);
237
238 let new_dot = fma_float::<T>(b, a_val, b_val, dot.clone());
240 b.raw_ptx(&format!("mov.{mov_suffix} {dot}, {new_dot};"));
241
242 b.raw_ptx(&format!("add.u32 {kk}, {kk}, 1;"));
243 b.branch(&k_loop);
244 b.label(&k_done);
245
246 let s_v_addr = b.byte_offset_addr(values_base.clone(), nz_idx.clone(), elem_bytes);
248 let s_old = load_global_float::<T>(b, s_v_addr.clone());
249
250 let alpha_dot = mul_float::<T>(b, alpha.clone(), dot);
252 let beta_s = mul_float::<T>(b, beta.clone(), s_old);
253 let result = add_float::<T>(b, alpha_dot, beta_s);
254
255 store_global_float::<T>(b, s_v_addr, result);
256
257 b.raw_ptx(&format!("add.u32 {nz_idx}, {nz_idx}, 1;"));
258 b.branch(&nz_loop);
259 b.label(&nz_done);
260 });
261
262 b.ret();
263 })
264 .build()
265 .map_err(|e| SparseError::PtxGeneration(e.to_string()))
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use crate::ptx_helpers::test_support::assert_assembles_and_clean;
272
273 #[test]
276 fn sddmm_f32_f64_assemble_sm86() {
277 let f32_ptx = emit_sddmm_kernel::<f32>(SmVersion::Sm86).expect("f32 SDDMM PTX");
278 assert_assembles_and_clean("sddmm_f32", &f32_ptx);
279
280 let f64_ptx = emit_sddmm_kernel::<f64>(SmVersion::Sm86).expect("f64 SDDMM PTX");
281 assert_assembles_and_clean("sddmm_f64", &f64_ptx);
282 assert!(
283 !f64_ptx.contains("0F00000000"),
284 "f64 SDDMM kernel must not materialize an f32 0.0 immediate:\n{f64_ptx}"
285 );
286 }
287 use oxicuda_ptx::arch::SmVersion;
288
289 #[test]
290 fn sddmm_ptx_generates_f32() {
291 let ptx = emit_sddmm_kernel::<f32>(SmVersion::Sm80);
292 assert!(ptx.is_ok());
293 let ptx_str = ptx.expect("test: PTX gen should succeed");
294 assert!(ptx_str.contains(".entry sddmm"));
295 }
296
297 #[test]
298 fn sddmm_ptx_generates_f64() {
299 let ptx = emit_sddmm_kernel::<f64>(SmVersion::Sm80);
300 assert!(ptx.is_ok());
301 }
302
303 #[test]
304 fn sddmm_ptx_has_correct_target() {
305 let ptx = emit_sddmm_kernel::<f32>(SmVersion::Sm75);
306 assert!(ptx.is_ok());
307 let ptx_str = ptx.expect("test: PTX gen should succeed");
308 assert!(ptx_str.contains(".target sm_75"));
309 }
310}
311
312#[cfg(all(test, feature = "gpu-tests"))]
317mod gpu_device_tests {
318 use super::*;
319 use crate::gpu_test_support::{assert_close, gpu_handle};
320 use crate::host_csr::{f64_to_gpu, gpu_to_f64};
321 use oxicuda_memory::DeviceBuffer;
322
323 #[allow(clippy::too_many_arguments)]
327 fn cpu_sddmm(
328 m: usize,
329 k: usize,
330 n: usize,
331 row_ptr: &[i32],
332 col_idx: &[i32],
333 s_old: &[f64],
334 a: &[f64],
335 b: &[f64],
336 alpha: f64,
337 beta: f64,
338 ) -> Vec<f64> {
339 let mut s = s_old.to_vec();
340 for row in 0..m {
341 for nz in row_ptr[row] as usize..row_ptr[row + 1] as usize {
342 let col = col_idx[nz] as usize;
343 let mut dot = 0.0_f64;
344 for kk in 0..k {
345 dot += a[row * k + kk] * b[kk * n + col];
347 }
348 s[nz] = alpha * dot + beta * s_old[nz];
349 }
350 }
351 s
352 }
353
354 #[allow(clippy::too_many_arguments)]
356 fn run_sddmm<T: GpuFloat>(
357 m: u32,
358 k: u32,
359 n: u32,
360 row_ptr: &[i32],
361 col_idx: &[i32],
362 s_old: &[f64],
363 a: &[f64],
364 b: &[f64],
365 alpha: f64,
366 beta: f64,
367 tol: f64,
368 tag: &str,
369 ) {
370 let Some(handle) = gpu_handle() else {
371 return;
372 };
373 let dev_s: Vec<T> = s_old.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
374 let mut s =
375 CsrMatrix::<T>::from_host(m, n, row_ptr, col_idx, &dev_s).expect("test: build CSR S");
376
377 let dev_a: Vec<T> = a.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
378 let dev_b: Vec<T> = b.iter().map(|&v| f64_to_gpu::<T>(v)).collect();
379 let a_buf = DeviceBuffer::from_host(&dev_a).expect("test: upload A");
380 let b_buf = DeviceBuffer::from_host(&dev_b).expect("test: upload B");
381
382 sddmm::<T>(
383 &handle,
384 f64_to_gpu::<T>(alpha),
385 a_buf.as_device_ptr(),
386 m,
387 k,
388 k, b_buf.as_device_ptr(),
390 n,
391 n, f64_to_gpu::<T>(beta),
393 &mut s,
394 )
395 .expect("test: sddmm launch");
396 handle.stream().synchronize().expect("test: sync");
397
398 let (_rp, _ci, out_vals) = s.to_host().expect("test: download S");
399 let got: Vec<f64> = out_vals.iter().map(|&v| gpu_to_f64(v)).collect();
400 let want = cpu_sddmm(
401 m as usize, k as usize, n as usize, row_ptr, col_idx, s_old, a, b, alpha, beta,
402 );
403 assert_close(&got, &want, tol, tag);
404 }
405
406 fn mask_3x4() -> (u32, Vec<i32>, Vec<i32>, Vec<f64>) {
409 let row_ptr = vec![0, 2, 4, 6];
410 let col_idx = vec![0, 2, 1, 3, 0, 3];
411 let s_old = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
413 (4, row_ptr, col_idx, s_old)
414 }
415
416 fn dense(rows: usize, cols: usize, base: f64) -> Vec<f64> {
417 (0..rows * cols)
418 .map(|idx| base + 0.25 * (idx as f64) - 0.05 * ((idx % 5) as f64))
419 .collect()
420 }
421
422 #[test]
423 fn sddmm_f64_alpha_beta() {
424 let m = 3usize;
425 let k = 5usize;
426 let (n, rp, ci, s_old) = mask_3x4();
427 let a = dense(m, k, 1.0);
428 let b = dense(k, n as usize, -0.5);
429 run_sddmm::<f64>(
430 m as u32,
431 k as u32,
432 n,
433 &rp,
434 &ci,
435 &s_old,
436 &a,
437 &b,
438 1.5,
439 -0.75,
440 1e-10,
441 "sddmm_f64",
442 );
443 }
444
445 #[test]
446 fn sddmm_f32_alpha_beta() {
447 let m = 3usize;
448 let k = 4usize;
449 let (n, rp, ci, s_old) = mask_3x4();
450 let a = dense(m, k, 0.5);
451 let b = dense(k, n as usize, 1.0);
452 run_sddmm::<f32>(
453 m as u32,
454 k as u32,
455 n,
456 &rp,
457 &ci,
458 &s_old,
459 &a,
460 &b,
461 2.0,
462 0.5,
463 1e-4,
464 "sddmm_f32",
465 );
466 }
467
468 #[test]
469 fn sddmm_f64_beta_zero() {
470 let m = 3usize;
472 let k = 6usize;
473 let (n, rp, ci, _s_old) = mask_3x4();
474 let s_old = vec![1e7; 6];
475 let a = dense(m, k, 2.0);
476 let b = dense(k, n as usize, 0.3);
477 run_sddmm::<f64>(
478 m as u32,
479 k as u32,
480 n,
481 &rp,
482 &ci,
483 &s_old,
484 &a,
485 &b,
486 1.0,
487 0.0,
488 1e-10,
489 "sddmm_beta0",
490 );
491 }
492}