1#![allow(clippy::needless_range_loop)]
31
32use crate::types::SvdFloat;
33use ndarray::{s, Array2, ArrayView2, ArrayViewMut2, Axis};
34use rayon::prelude::*;
35use sprs::{CsMatViewI, SpIndex};
36
37pub const DEFAULT_SCRATCH_BUDGET: usize = 64 << 20;
43
44const SERIAL_ELEMS: usize = 8 << 10;
46
47#[inline]
48fn threads() -> usize {
49 rayon::current_num_threads().max(1)
50}
51
52#[inline]
54fn axpy<T: SvdFloat>(alpha: T, x: &[T], y: &mut [T]) {
55 debug_assert_eq!(x.len(), y.len());
56 for (yi, &xi) in y.iter_mut().zip(x.iter()) {
57 *yi += alpha * xi;
58 }
59}
60
61fn nnz_balanced_split<N, I: SpIndex, Iptr: SpIndex>(
67 m: &CsMatViewI<N, I, Iptr>,
68 p: usize,
69) -> Vec<usize> {
70 let outer = m.outer_dims();
71 let total = m.nnz();
72 let mut bounds = Vec::with_capacity(p + 1);
73 bounds.push(0);
74 if p <= 1 || outer == 0 || total == 0 {
75 bounds.push(outer);
76 while bounds.len() < p + 1 {
77 bounds.push(outer);
78 }
79 return bounds;
80 }
81 let mut acc = 0usize;
82 let mut next = 1usize;
83 for i in 0..outer {
84 acc += m.outer_view(i).map_or(0, |v| v.nnz());
85 while next < p && acc * p >= total * next {
88 bounds.push(i + 1);
89 next += 1;
90 }
91 }
92 while bounds.len() < p + 1 {
93 bounds.push(outer);
94 }
95 bounds
96}
97
98pub fn gather_mul<T, I, Iptr>(
102 lhs: CsMatViewI<T, I, Iptr>,
103 rhs: ArrayView2<T>,
104 mut out: ArrayViewMut2<T>,
105) where
106 T: SvdFloat,
107 I: SpIndex,
108 Iptr: SpIndex,
109{
110 assert!(lhs.is_csr(), "gather_mul requires CSR storage");
111 assert_eq!(lhs.cols(), rhs.nrows(), "gather_mul: lhs.cols != rhs.rows");
112 assert_eq!(lhs.rows(), out.nrows(), "gather_mul: lhs.rows != out.rows");
113 assert_eq!(rhs.ncols(), out.ncols(), "gather_mul: rhs.cols != out.cols");
114
115 let k = rhs.ncols();
116 let m = lhs.rows();
117 if m == 0 || k == 0 {
118 out.fill(T::zero());
119 return;
120 }
121
122 let row_op = |i: usize, orow: &mut [T], rhs: &ArrayView2<T>| {
123 orow.fill(T::zero());
124 let Some(row) = lhs.outer_view(i) else { return };
125 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
126 let rrow = rhs.row(j.index());
127 match rrow.as_slice() {
129 Some(sl) => axpy(v, sl, orow),
130 None => {
131 for (o, &r) in orow.iter_mut().zip(rrow.iter()) {
132 *o += v * r;
133 }
134 }
135 }
136 }
137 };
138
139 if m * k <= SERIAL_ELEMS {
140 for i in 0..m {
141 let mut orow = out.row_mut(i);
142 match orow.as_slice_mut() {
143 Some(sl) => row_op(i, sl, &rhs),
144 None => {
145 let mut tmp = vec![T::zero(); k];
146 row_op(i, &mut tmp, &rhs);
147 for (o, t) in orow.iter_mut().zip(tmp) {
148 *o = t;
149 }
150 }
151 }
152 }
153 return;
154 }
155
156 let chunk = m.div_ceil(threads() * 4).max(1);
158 out.axis_chunks_iter_mut(Axis(0), chunk)
159 .into_par_iter()
160 .enumerate()
161 .for_each(|(ci, mut block)| {
162 let base = ci * chunk;
163 for (local, mut orow) in block.rows_mut().into_iter().enumerate() {
164 let i = base + local;
165 match orow.as_slice_mut() {
166 Some(sl) => row_op(i, sl, &rhs),
167 None => {
168 let mut tmp = vec![T::zero(); k];
169 row_op(i, &mut tmp, &rhs);
170 for (o, t) in orow.iter_mut().zip(tmp) {
171 *o = t;
172 }
173 }
174 }
175 }
176 });
177}
178
179pub fn scatter_mul<T, I, Iptr>(
184 lhs: CsMatViewI<T, I, Iptr>,
185 rhs: ArrayView2<T>,
186 mut out: ArrayViewMut2<T>,
187 budget: usize,
188) where
189 T: SvdFloat,
190 I: SpIndex,
191 Iptr: SpIndex,
192{
193 assert!(lhs.is_csr(), "scatter_mul requires CSR storage");
194 assert_eq!(lhs.rows(), rhs.nrows(), "scatter_mul: lhs.rows != rhs.rows");
195 assert_eq!(lhs.cols(), out.nrows(), "scatter_mul: lhs.cols != out.rows");
196 assert_eq!(
197 rhs.ncols(),
198 out.ncols(),
199 "scatter_mul: rhs.cols != out.cols"
200 );
201
202 let (m, n, k) = (lhs.rows(), lhs.cols(), rhs.ncols());
203 out.fill(T::zero());
204 if m == 0 || n == 0 || k == 0 {
205 return;
206 }
207
208 let p = threads();
210 if p == 1 || n * k <= SERIAL_ELEMS {
211 for i in 0..m {
212 let Some(row) = lhs.outer_view(i) else {
213 continue;
214 };
215 let rrow = rhs.row(i);
216 let rslice = rrow.as_slice();
217 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
218 let mut orow = out.row_mut(j.index());
219 match (orow.as_slice_mut(), rslice) {
220 (Some(o), Some(r)) => axpy(v, r, o),
221 _ => {
222 for (o, &r) in orow.iter_mut().zip(rrow.iter()) {
223 *o += v * r;
224 }
225 }
226 }
227 }
228 }
229 return;
230 }
231
232 let bytes_per_col = p.saturating_mul(n).saturating_mul(std::mem::size_of::<T>());
234 let kb = budget
235 .checked_div(bytes_per_col)
236 .map_or(k, |wide| wide.clamp(1, k));
237
238 let bounds = nnz_balanced_split(&lhs, p);
239
240 for cstart in (0..k).step_by(kb) {
241 let cend = (cstart + kb).min(k);
242 let width = cend - cstart;
243 let rhs_blk = rhs.slice(s![.., cstart..cend]);
244
245 let partials: Vec<Array2<T>> = (0..p)
246 .into_par_iter()
247 .map(|t| {
248 let (lo, hi) = (bounds[t], bounds[t + 1]);
249 let mut acc = Array2::<T>::zeros((n, width));
250 for i in lo..hi {
251 let Some(row) = lhs.outer_view(i) else {
252 continue;
253 };
254 let rrow = rhs_blk.row(i);
255 let rslice = rrow.as_slice();
258 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
259 let jj = j.index();
260 let mut arow = acc.row_mut(jj);
261 let aslice = arow.as_slice_mut().expect("owned array is contiguous");
262 match rslice {
263 Some(r) => axpy(v, r, aslice),
264 None => {
265 for (a, &r) in aslice.iter_mut().zip(rrow.iter()) {
266 *a += v * r;
267 }
268 }
269 }
270 }
271 }
272 acc
273 })
274 .collect();
275
276 let mut out_blk = out.slice_mut(s![.., cstart..cend]);
278 let rchunk = n.div_ceil(p * 4).max(1);
279 out_blk
280 .axis_chunks_iter_mut(Axis(0), rchunk)
281 .into_par_iter()
282 .enumerate()
283 .for_each(|(ci, mut block)| {
284 let base = ci * rchunk;
285 for (local, mut orow) in block.rows_mut().into_iter().enumerate() {
286 let gi = base + local;
287 for acc in &partials {
288 let arow = acc.row(gi);
289 for (o, &a) in orow.iter_mut().zip(arow.iter()) {
290 *o += a;
291 }
292 }
293 }
294 });
295 }
296}
297
298pub fn gather_mul_vec<T, I, Iptr>(lhs: CsMatViewI<T, I, Iptr>, x: &[T], y: &mut [T])
300where
301 T: SvdFloat,
302 I: SpIndex,
303 Iptr: SpIndex,
304{
305 assert!(lhs.is_csr(), "gather_mul_vec requires CSR storage");
306 assert_eq!(lhs.cols(), x.len(), "gather_mul_vec: lhs.cols != x.len");
307 assert_eq!(lhs.rows(), y.len(), "gather_mul_vec: lhs.rows != y.len");
308
309 let dot = |i: usize| -> T {
310 let Some(row) = lhs.outer_view(i) else {
311 return T::zero();
312 };
313 let mut sum = T::zero();
314 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
315 sum += v * x[j.index()];
316 }
317 sum
318 };
319
320 if y.len() <= SERIAL_ELEMS {
321 for (i, yi) in y.iter_mut().enumerate() {
322 *yi = dot(i);
323 }
324 return;
325 }
326 let chunk = y.len().div_ceil(threads() * 4).max(1);
327 y.par_chunks_mut(chunk).enumerate().for_each(|(ci, blk)| {
328 let base = ci * chunk;
329 for (local, yi) in blk.iter_mut().enumerate() {
330 *yi = dot(base + local);
331 }
332 });
333}
334
335pub fn scatter_mul_vec<T, I, Iptr>(lhs: CsMatViewI<T, I, Iptr>, x: &[T], y: &mut [T])
337where
338 T: SvdFloat,
339 I: SpIndex,
340 Iptr: SpIndex,
341{
342 assert!(lhs.is_csr(), "scatter_mul_vec requires CSR storage");
343 assert_eq!(lhs.rows(), x.len(), "scatter_mul_vec: lhs.rows != x.len");
344 assert_eq!(lhs.cols(), y.len(), "scatter_mul_vec: lhs.cols != y.len");
345
346 let (m, n) = (lhs.rows(), lhs.cols());
347 y.fill(T::zero());
348 if m == 0 || n == 0 {
349 return;
350 }
351
352 let p = threads();
353 if p == 1 || lhs.nnz() <= SERIAL_ELEMS {
356 for i in 0..m {
357 let Some(row) = lhs.outer_view(i) else {
358 continue;
359 };
360 let xi = x[i];
361 if xi.is_zero() {
362 continue;
363 }
364 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
365 y[j.index()] += v * xi;
366 }
367 }
368 return;
369 }
370
371 let bounds = nnz_balanced_split(&lhs, p);
372 let partials: Vec<Vec<T>> = (0..p)
373 .into_par_iter()
374 .map(|t| {
375 let (lo, hi) = (bounds[t], bounds[t + 1]);
376 let mut acc = vec![T::zero(); n];
377 for i in lo..hi {
378 let Some(row) = lhs.outer_view(i) else {
379 continue;
380 };
381 let xi = x[i];
382 if xi.is_zero() {
383 continue;
384 }
385 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
386 acc[j.index()] += v * xi;
387 }
388 }
389 acc
390 })
391 .collect();
392
393 let chunk = n.div_ceil(p * 4).max(1);
394 y.par_chunks_mut(chunk).enumerate().for_each(|(ci, blk)| {
395 let base = ci * chunk;
396 for (local, yi) in blk.iter_mut().enumerate() {
397 let gi = base + local;
398 for acc in &partials {
399 *yi += acc[gi];
400 }
401 }
402 });
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use ndarray::{arr2, Array2};
409 use sprs::{CsMatI, TriMatI};
410
411 fn tiny() -> CsMatI<f64, u32, u64> {
412 let mut t = TriMatI::<f64, u32>::new((4, 3));
417 t.add_triplet(0, 0, 1.0);
418 t.add_triplet(0, 2, 2.0);
419 t.add_triplet(1, 1, 3.0);
420 t.add_triplet(2, 0, 4.0);
421 t.add_triplet(2, 2, 5.0);
422 t.add_triplet(3, 1, 6.0);
423 t.to_csr::<u64>()
424 }
425
426 use crate::testing::dense_of;
427
428 #[test]
429 fn gather_matches_dense() {
430 let a = tiny();
431 let rhs = arr2(&[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]);
432 let mut out = Array2::zeros((4, 2));
433 gather_mul(a.view(), rhs.view(), out.view_mut());
434 let expect = dense_of(&a).dot(&rhs);
435 assert_eq!(out, expect);
436 }
437
438 #[test]
439 fn scatter_matches_dense() {
440 let a = tiny();
441 let rhs = arr2(&[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0], [7.0, 8.0]]);
442 let mut out = Array2::zeros((3, 2));
443 scatter_mul(a.view(), rhs.view(), out.view_mut(), DEFAULT_SCRATCH_BUDGET);
444 let expect = dense_of(&a).t().dot(&rhs);
445 assert_eq!(out, expect);
446 }
447
448 #[test]
450 fn scatter_column_blocking_matches_single_pass() {
451 let a = tiny();
452 let rhs = arr2(&[
453 [1.0, 2.0, 9.0],
454 [3.0, 4.0, 8.0],
455 [5.0, 6.0, 7.0],
456 [7.0, 8.0, 6.0],
457 ]);
458 let mut wide = Array2::zeros((3, 3));
459 let mut narrow = Array2::zeros((3, 3));
460 scatter_mul(a.view(), rhs.view(), wide.view_mut(), usize::MAX);
461 scatter_mul(a.view(), rhs.view(), narrow.view_mut(), 0);
462 assert_eq!(wide, narrow);
463 assert_eq!(wide, dense_of(&a).t().dot(&rhs));
464 }
465
466 #[test]
467 fn matvecs_match_dense() {
468 let a = tiny();
469 let d = dense_of(&a);
470 let x = vec![1.0, 2.0, 3.0];
471 let mut y = vec![0.0; 4];
472 gather_mul_vec(a.view(), &x, &mut y);
473 assert_eq!(y, d.dot(&ndarray::arr1(&x)).to_vec());
474
475 let xt = vec![1.0, 2.0, 3.0, 4.0];
476 let mut yt = vec![0.0; 3];
477 scatter_mul_vec(a.view(), &xt, &mut yt);
478 assert_eq!(yt, d.t().dot(&ndarray::arr1(&xt)).to_vec());
479 }
480
481 #[test]
482 fn nnz_split_covers_all_rows_and_is_monotone() {
483 let a = tiny();
484 for p in 1..=8 {
485 let b = nnz_balanced_split(&a.view(), p);
486 assert_eq!(b.len(), p + 1);
487 assert_eq!(b[0], 0);
488 assert_eq!(*b.last().unwrap(), a.rows());
489 assert!(b.windows(2).all(|w| w[0] <= w[1]), "not monotone: {b:?}");
490 }
491 }
492
493 #[test]
496 fn nnz_split_handles_skew() {
497 let mut t = TriMatI::<f64, u32>::new((100, 50));
498 for j in 0..50 {
499 t.add_triplet(0, j, 1.0);
500 }
501 for i in 1..100 {
502 t.add_triplet(i, 0, 1.0);
503 }
504 let a: CsMatI<f64, u32, u64> = t.to_csr();
505 let b = nnz_balanced_split(&a.view(), 4);
506 assert_eq!(b[0], 0);
507 assert_eq!(*b.last().unwrap(), 100);
508 assert!(b.windows(2).all(|w| w[0] <= w[1]));
509 assert_eq!(
511 b[1], 1,
512 "expected the 50-nnz row to form its own partition: {b:?}"
513 );
514 }
515}