Skip to main content

scirs2_core/distributed/
parallel_scan.rs

1//! Work-efficient parallel prefix sum (scan) operations
2//!
3//! This module implements the Blelloch parallel scan algorithm, which computes
4//! prefix sums (and generalised prefix scans with arbitrary associative
5//! operators) in O(n) work and O(log n) span.
6//!
7//! Two variants are provided:
8//!
9//! - **Exclusive scan** (`parallel_prefix_sum_exclusive`): element `i` of the
10//!   output is the sum of elements `0..i` (the first element is `identity`).
11//! - **Inclusive scan** (`parallel_prefix_sum`): element `i` of the output
12//!   is the sum of elements `0..=i`.
13//!
14//! A generic `parallel_scan` function accepts any associative binary operator.
15//!
16//! When the `parallel` feature is enabled, the up-sweep and down-sweep phases
17//! use `rayon` for parallel execution.  Without `parallel`, all operations
18//! fall back to sequential execution.
19//!
20//! ## Example
21//!
22//! ```rust
23//! use scirs2_core::distributed::parallel_scan::{parallel_prefix_sum, parallel_scan};
24//!
25//! // Inclusive prefix sum: [1, 3, 6, 10, 15]
26//! let data = vec![1, 2, 3, 4, 5];
27//! let result = parallel_prefix_sum(&data);
28//! assert_eq!(result, vec![1, 3, 6, 10, 15]);
29//!
30//! // Generic scan with multiplication
31//! let data = vec![1, 2, 3, 4];
32//! let result = parallel_scan(&data, 1, |a, b| a * b);
33//! assert_eq!(result, vec![1, 2, 6, 24]);
34//! ```
35
36use crate::error::{CoreError, CoreResult, ErrorContext, ErrorLocation};
37
38// ─────────────────────────────────────────────────────────────────────────────
39// Core algorithms
40// ─────────────────────────────────────────────────────────────────────────────
41
42/// Inclusive prefix sum for a slice of values that support addition.
43///
44/// Returns a `Vec<T>` where `result[i] = data[0] + data[1] + ... + data[i]`.
45///
46/// For empty input, returns an empty vector.
47///
48/// This is a specialisation of [`parallel_scan`] with the addition operator
49/// and a zero identity element.
50pub fn parallel_prefix_sum<T>(data: &[T]) -> Vec<T>
51where
52    T: Clone + Send + Sync + Default + std::ops::Add<Output = T>,
53{
54    parallel_scan(data, T::default(), |a, b| a + b)
55}
56
57/// Exclusive prefix sum for a slice of values that support addition.
58///
59/// Returns a `Vec<T>` where `result[i] = data[0] + ... + data[i-1]` and
60/// `result[0] = identity`.
61///
62/// For empty input, returns an empty vector.
63pub fn parallel_prefix_sum_exclusive<T>(data: &[T]) -> Vec<T>
64where
65    T: Clone + Send + Sync + Default + std::ops::Add<Output = T>,
66{
67    parallel_scan_exclusive(data, T::default(), |a, b| a + b)
68}
69
70/// Inclusive generalised parallel scan with an arbitrary associative operator.
71///
72/// Given `data = [a0, a1, a2, ...]` and binary operator `op`, returns
73/// `[a0, op(a0, a1), op(op(a0, a1), a2), ...]`.
74///
75/// The `identity` element must satisfy `op(identity, x) == x` for all `x`.
76///
77/// # Algorithm
78///
79/// For small inputs (< `SEQUENTIAL_THRESHOLD` elements), a simple sequential
80/// scan is used.  For larger inputs, the **Blelloch three-phase** algorithm
81/// is used:
82///
83/// 1. **Tile reduce**: Divide the input into tiles, compute partial reductions
84///    of each tile (in parallel when the `parallel` feature is enabled).
85/// 2. **Prefix on reductions**: Compute an exclusive prefix scan on the tile
86///    reductions (recursive, but the number of tiles is small).
87/// 3. **Tile scan**: Each tile performs a local inclusive scan starting from
88///    its tile prefix (in parallel when `parallel` is enabled).
89pub fn parallel_scan<T, F>(data: &[T], identity: T, op: F) -> Vec<T>
90where
91    T: Clone + Send + Sync,
92    F: Fn(T, T) -> T + Send + Sync + Clone,
93{
94    if data.is_empty() {
95        return Vec::new();
96    }
97
98    let n = data.len();
99
100    // For small inputs, use sequential scan
101    if n < SEQUENTIAL_THRESHOLD {
102        return sequential_inclusive_scan(data, &identity, &op);
103    }
104
105    blelloch_inclusive_scan(data, &identity, &op)
106}
107
108/// Exclusive generalised parallel scan.
109///
110/// Like [`parallel_scan`] but the output is shifted right: `result[0] =
111/// identity`, `result[i] = op(data[0], ..., data[i-1])`.
112pub fn parallel_scan_exclusive<T, F>(data: &[T], identity: T, op: F) -> Vec<T>
113where
114    T: Clone + Send + Sync,
115    F: Fn(T, T) -> T + Send + Sync + Clone,
116{
117    if data.is_empty() {
118        return Vec::new();
119    }
120
121    let n = data.len();
122
123    if n < SEQUENTIAL_THRESHOLD {
124        return sequential_exclusive_scan(data, &identity, &op);
125    }
126
127    blelloch_exclusive_scan(data, &identity, &op)
128}
129
130/// Parallel prefix sum that returns a `CoreResult`, for ergonomic error
131/// handling at call sites.
132pub fn try_parallel_prefix_sum<T>(data: &[T]) -> CoreResult<Vec<T>>
133where
134    T: Clone + Send + Sync + Default + std::ops::Add<Output = T>,
135{
136    Ok(parallel_prefix_sum(data))
137}
138
139/// Parallel scan that validates the input is non-empty.
140pub fn try_parallel_scan<T, F>(data: &[T], identity: T, op: F) -> CoreResult<Vec<T>>
141where
142    T: Clone + Send + Sync,
143    F: Fn(T, T) -> T + Send + Sync + Clone,
144{
145    if data.is_empty() {
146        return Err(CoreError::ValueError(
147            ErrorContext::new("parallel_scan requires non-empty input".to_string())
148                .with_location(ErrorLocation::new(file!(), line!())),
149        ));
150    }
151    Ok(parallel_scan(data, identity, op))
152}
153
154// ─────────────────────────────────────────────────────────────────────────────
155// Internal implementation
156// ─────────────────────────────────────────────────────────────────────────────
157
158/// Below this many elements, we fall back to sequential scan.
159const SEQUENTIAL_THRESHOLD: usize = 1024;
160
161/// Tile size for the parallel Blelloch algorithm.
162/// Each tile is processed sequentially; tiles are processed in parallel.
163const TILE_SIZE: usize = 256;
164
165fn sequential_inclusive_scan<T, F>(data: &[T], identity: &T, op: &F) -> Vec<T>
166where
167    T: Clone,
168    F: Fn(T, T) -> T,
169{
170    let mut result = Vec::with_capacity(data.len());
171    let mut acc = identity.clone();
172    for item in data {
173        acc = op(acc, item.clone());
174        result.push(acc.clone());
175    }
176    result
177}
178
179fn sequential_exclusive_scan<T, F>(data: &[T], identity: &T, op: &F) -> Vec<T>
180where
181    T: Clone,
182    F: Fn(T, T) -> T,
183{
184    let mut result = Vec::with_capacity(data.len());
185    let mut acc = identity.clone();
186    for item in data {
187        result.push(acc.clone());
188        acc = op(acc, item.clone());
189    }
190    result
191}
192
193/// Blelloch three-phase inclusive scan.
194#[cfg(feature = "parallel")]
195fn blelloch_inclusive_scan<T, F>(data: &[T], identity: &T, op: &F) -> Vec<T>
196where
197    T: Clone + Send + Sync,
198    F: Fn(T, T) -> T + Send + Sync + Clone,
199{
200    use rayon::prelude::*;
201
202    let n = data.len();
203    let num_tiles = (n + TILE_SIZE - 1) / TILE_SIZE;
204
205    // Phase 1: Reduce each tile to a single value (in parallel)
206    let tile_reductions: Vec<T> = (0..num_tiles)
207        .into_par_iter()
208        .map(|tile_idx| {
209            let start = tile_idx * TILE_SIZE;
210            let end = (start + TILE_SIZE).min(n);
211            let mut acc = identity.clone();
212            for item in &data[start..end] {
213                acc = op(acc, item.clone());
214            }
215            acc
216        })
217        .collect();
218
219    // Phase 2: Exclusive prefix scan on tile reductions (sequential — num_tiles is small)
220    let mut tile_prefixes: Vec<T> = Vec::with_capacity(num_tiles);
221    {
222        let mut acc = identity.clone();
223        for red in &tile_reductions {
224            tile_prefixes.push(acc.clone());
225            acc = op(acc, red.clone());
226        }
227    }
228
229    // Phase 3: Local inclusive scan per tile, starting from tile prefix (in parallel)
230    let mut result: Vec<T> = vec![identity.clone(); n];
231    let result_chunks: Vec<&mut [T]> = result.chunks_mut(TILE_SIZE).collect();
232
233    // We need to use indices-based approach to avoid lifetime issues
234    let result_ptr = result.as_mut_ptr();
235    let data_ref = data;
236
237    // Safety: each tile writes to a disjoint range of `result`
238    // We use thread::scope to avoid unsafe code
239    std::thread::scope(|s| {
240        let mut handles = Vec::with_capacity(num_tiles);
241        for tile_idx in 0..num_tiles {
242            let start = tile_idx * TILE_SIZE;
243            let end = (start + TILE_SIZE).min(n);
244            let tile_prefix = tile_prefixes[tile_idx].clone();
245            let op_clone = op.clone();
246            let tile_data = &data_ref[start..end];
247
248            let handle = s.spawn(move || {
249                let mut acc = tile_prefix;
250                let mut local_result = Vec::with_capacity(end - start);
251                for item in tile_data {
252                    acc = op_clone(acc, item.clone());
253                    local_result.push(acc.clone());
254                }
255                (start, local_result)
256            });
257            handles.push(handle);
258        }
259
260        for handle in handles {
261            if let Ok((start, local_result)) = handle.join() {
262                for (i, val) in local_result.into_iter().enumerate() {
263                    // Safety: each tile writes to non-overlapping range
264                    unsafe {
265                        std::ptr::write(result_ptr.add(start + i), val);
266                    }
267                }
268            }
269        }
270    });
271
272    result
273}
274
275/// Blelloch three-phase exclusive scan.
276#[cfg(feature = "parallel")]
277fn blelloch_exclusive_scan<T, F>(data: &[T], identity: &T, op: &F) -> Vec<T>
278where
279    T: Clone + Send + Sync,
280    F: Fn(T, T) -> T + Send + Sync + Clone,
281{
282    use rayon::prelude::*;
283
284    let n = data.len();
285    let num_tiles = (n + TILE_SIZE - 1) / TILE_SIZE;
286
287    // Phase 1: Reduce each tile
288    let tile_reductions: Vec<T> = (0..num_tiles)
289        .into_par_iter()
290        .map(|tile_idx| {
291            let start = tile_idx * TILE_SIZE;
292            let end = (start + TILE_SIZE).min(n);
293            let mut acc = identity.clone();
294            for item in &data[start..end] {
295                acc = op(acc, item.clone());
296            }
297            acc
298        })
299        .collect();
300
301    // Phase 2: Exclusive prefix on tile reductions
302    let mut tile_prefixes: Vec<T> = Vec::with_capacity(num_tiles);
303    {
304        let mut acc = identity.clone();
305        for red in &tile_reductions {
306            tile_prefixes.push(acc.clone());
307            acc = op(acc, red.clone());
308        }
309    }
310
311    // Phase 3: Local exclusive scan per tile
312    let mut result: Vec<T> = vec![identity.clone(); n];
313    let result_ptr = result.as_mut_ptr();
314    let data_ref = data;
315
316    std::thread::scope(|s| {
317        let mut handles = Vec::with_capacity(num_tiles);
318        for tile_idx in 0..num_tiles {
319            let start = tile_idx * TILE_SIZE;
320            let end = (start + TILE_SIZE).min(n);
321            let tile_prefix = tile_prefixes[tile_idx].clone();
322            let op_clone = op.clone();
323            let tile_data = &data_ref[start..end];
324
325            let handle = s.spawn(move || {
326                let mut acc = tile_prefix;
327                let mut local_result = Vec::with_capacity(end - start);
328                for item in tile_data {
329                    local_result.push(acc.clone());
330                    acc = op_clone(acc, item.clone());
331                }
332                (start, local_result)
333            });
334            handles.push(handle);
335        }
336
337        for handle in handles {
338            if let Ok((start, local_result)) = handle.join() {
339                for (i, val) in local_result.into_iter().enumerate() {
340                    unsafe {
341                        std::ptr::write(result_ptr.add(start + i), val);
342                    }
343                }
344            }
345        }
346    });
347
348    result
349}
350
351/// Sequential fallback for inclusive scan (no parallel feature).
352#[cfg(not(feature = "parallel"))]
353fn blelloch_inclusive_scan<T, F>(data: &[T], identity: &T, op: &F) -> Vec<T>
354where
355    T: Clone + Send + Sync,
356    F: Fn(T, T) -> T + Send + Sync + Clone,
357{
358    sequential_inclusive_scan(data, identity, op)
359}
360
361/// Sequential fallback for exclusive scan (no parallel feature).
362#[cfg(not(feature = "parallel"))]
363fn blelloch_exclusive_scan<T, F>(data: &[T], identity: &T, op: &F) -> Vec<T>
364where
365    T: Clone + Send + Sync,
366    F: Fn(T, T) -> T + Send + Sync + Clone,
367{
368    sequential_exclusive_scan(data, identity, op)
369}
370
371// ─────────────────────────────────────────────────────────────────────────────
372// Specialised numeric prefix sums
373// ─────────────────────────────────────────────────────────────────────────────
374
375/// Fast inclusive prefix sum for `f64` slices.
376///
377/// Uses SIMD-friendly sequential accumulation for small inputs and the
378/// tiled parallel algorithm for large inputs.
379pub fn parallel_prefix_sum_f64(data: &[f64]) -> Vec<f64> {
380    parallel_prefix_sum(data)
381}
382
383/// Fast inclusive prefix sum for `i64` slices.
384pub fn parallel_prefix_sum_i64(data: &[i64]) -> Vec<i64> {
385    parallel_prefix_sum(data)
386}
387
388/// Parallel prefix minimum — `result[i] = min(data[0..=i])`.
389pub fn parallel_prefix_min<T>(data: &[T]) -> Vec<T>
390where
391    T: Clone + Send + Sync + Ord,
392{
393    if data.is_empty() {
394        return Vec::new();
395    }
396    let identity = data[0].clone();
397    parallel_scan(data, identity, |a, b| if a <= b { a } else { b })
398}
399
400/// Parallel prefix maximum — `result[i] = max(data[0..=i])`.
401pub fn parallel_prefix_max<T>(data: &[T]) -> Vec<T>
402where
403    T: Clone + Send + Sync + Ord,
404{
405    if data.is_empty() {
406        return Vec::new();
407    }
408    let identity = data[0].clone();
409    parallel_scan(data, identity, |a, b| if a >= b { a } else { b })
410}
411
412/// Segmented prefix sum.
413///
414/// `flags[i]` is `true` at the start of a new segment. The prefix sum
415/// resets at each segment boundary.
416///
417/// # Example
418///
419/// ```rust
420/// use scirs2_core::distributed::parallel_scan::segmented_prefix_sum;
421///
422/// let data = vec![1, 2, 3, 1, 2, 3];
423/// let flags = vec![true, false, false, true, false, false];
424/// let result = segmented_prefix_sum(&data, &flags);
425/// assert_eq!(result, vec![1, 3, 6, 1, 3, 6]);
426/// ```
427pub fn segmented_prefix_sum<T>(data: &[T], flags: &[bool]) -> Vec<T>
428where
429    T: Clone + Send + Sync + Default + std::ops::Add<Output = T>,
430{
431    let n = data.len().min(flags.len());
432    if n == 0 {
433        return Vec::new();
434    }
435
436    let mut result = Vec::with_capacity(n);
437    let mut acc = T::default();
438
439    for i in 0..n {
440        if flags[i] {
441            acc = T::default();
442        }
443        acc = acc + data[i].clone();
444        result.push(acc.clone());
445    }
446
447    result
448}
449
450// ─────────────────────────────────────────────────────────────────────────────
451// Tests
452// ─────────────────────────────────────────────────────────────────────────────
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    #[test]
459    fn test_empty_prefix_sum() {
460        let data: Vec<i32> = Vec::new();
461        assert!(parallel_prefix_sum(&data).is_empty());
462        assert!(parallel_prefix_sum_exclusive(&data).is_empty());
463    }
464
465    #[test]
466    fn test_single_element() {
467        assert_eq!(parallel_prefix_sum(&[42]), vec![42]);
468        assert_eq!(parallel_prefix_sum_exclusive(&[42]), vec![0]);
469    }
470
471    #[test]
472    fn test_small_inclusive_sum() {
473        let data = vec![1, 2, 3, 4, 5];
474        let result = parallel_prefix_sum(&data);
475        assert_eq!(result, vec![1, 3, 6, 10, 15]);
476    }
477
478    #[test]
479    fn test_small_exclusive_sum() {
480        let data = vec![1, 2, 3, 4, 5];
481        let result = parallel_prefix_sum_exclusive(&data);
482        assert_eq!(result, vec![0, 1, 3, 6, 10]);
483    }
484
485    #[test]
486    fn test_generic_scan_multiplication() {
487        let data = vec![1, 2, 3, 4, 5];
488        let result = parallel_scan(&data, 1, |a, b| a * b);
489        assert_eq!(result, vec![1, 2, 6, 24, 120]);
490    }
491
492    #[test]
493    fn test_generic_scan_max() {
494        let data = vec![3, 1, 4, 1, 5, 9, 2, 6];
495        let result = parallel_prefix_max(&data);
496        assert_eq!(result, vec![3, 3, 4, 4, 5, 9, 9, 9]);
497    }
498
499    #[test]
500    fn test_generic_scan_min() {
501        let data = vec![5, 3, 7, 1, 4, 2, 8, 6];
502        let result = parallel_prefix_min(&data);
503        assert_eq!(result, vec![5, 3, 3, 1, 1, 1, 1, 1]);
504    }
505
506    #[test]
507    fn test_large_prefix_sum() {
508        // Test with data larger than SEQUENTIAL_THRESHOLD
509        let n = 5000;
510        let data: Vec<i64> = (1..=n).collect();
511        let result = parallel_prefix_sum(&data);
512        // Verify a few known values
513        assert_eq!(result[0], 1);
514        assert_eq!(result[n as usize - 1], n * (n + 1) / 2);
515        // Verify it's monotonically increasing
516        for i in 1..result.len() {
517            assert!(result[i] > result[i - 1]);
518        }
519    }
520
521    #[test]
522    fn test_large_exclusive_sum() {
523        let n = 5000;
524        let data: Vec<i64> = (1..=n).collect();
525        let result = parallel_prefix_sum_exclusive(&data);
526        assert_eq!(result[0], 0);
527        assert_eq!(result[1], 1);
528        assert_eq!(result[n as usize - 1], n * (n - 1) / 2);
529    }
530
531    #[test]
532    fn test_segmented_prefix_sum() {
533        let data = vec![1, 2, 3, 1, 2, 3];
534        let flags = vec![true, false, false, true, false, false];
535        let result = segmented_prefix_sum(&data, &flags);
536        assert_eq!(result, vec![1, 3, 6, 1, 3, 6]);
537    }
538
539    #[test]
540    fn test_segmented_prefix_sum_single_segment() {
541        let data = vec![1, 2, 3, 4];
542        let flags = vec![true, false, false, false];
543        let result = segmented_prefix_sum(&data, &flags);
544        assert_eq!(result, vec![1, 3, 6, 10]);
545    }
546
547    #[test]
548    fn test_segmented_prefix_sum_all_segments() {
549        let data = vec![10, 20, 30];
550        let flags = vec![true, true, true];
551        let result = segmented_prefix_sum(&data, &flags);
552        assert_eq!(result, vec![10, 20, 30]);
553    }
554
555    #[test]
556    fn test_f64_prefix_sum() {
557        let data = vec![1.0, 2.0, 3.0, 4.0];
558        let result = parallel_prefix_sum_f64(&data);
559        assert!((result[0] - 1.0).abs() < 1e-10);
560        assert!((result[1] - 3.0).abs() < 1e-10);
561        assert!((result[2] - 6.0).abs() < 1e-10);
562        assert!((result[3] - 10.0).abs() < 1e-10);
563    }
564
565    #[test]
566    fn test_try_parallel_scan_empty_error() {
567        let data: Vec<i32> = Vec::new();
568        let result = try_parallel_scan(&data, 0, |a, b| a + b);
569        assert!(result.is_err());
570    }
571
572    #[test]
573    fn test_try_parallel_prefix_sum() {
574        let data = vec![1, 2, 3];
575        let result = try_parallel_prefix_sum(&data).expect("should succeed");
576        assert_eq!(result, vec![1, 3, 6]);
577    }
578
579    #[test]
580    fn test_consistency_inclusive_vs_exclusive() {
581        let data: Vec<i32> = (1..=100).collect();
582        let inclusive = parallel_prefix_sum(&data);
583        let exclusive = parallel_prefix_sum_exclusive(&data);
584
585        // exclusive[i] + data[i] == inclusive[i]
586        for i in 0..data.len() {
587            assert_eq!(exclusive[i] + data[i], inclusive[i]);
588        }
589    }
590
591    #[test]
592    fn test_string_concat_scan() {
593        let data = vec!["a".to_string(), "b".to_string(), "c".to_string()];
594        let result = parallel_scan(&data, String::new(), |mut a, b| {
595            a.push_str(&b);
596            a
597        });
598        assert_eq!(result, vec!["a", "ab", "abc"]);
599    }
600
601    #[test]
602    fn test_large_parallel_correctness() {
603        // Verify parallel result matches sequential for large inputs
604        let n = 10_000;
605        let data: Vec<i64> = (0..n).collect();
606        let par_result = parallel_prefix_sum(&data);
607        let seq_result = sequential_inclusive_scan(&data, &0i64, &|a, b| a + b);
608        assert_eq!(par_result, seq_result);
609    }
610}