oxiphysics_gpu/parallel/types.rs
1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5/// Result of load balancing: a set of ranges assigned to workers.
6pub struct LoadBalancePlan {
7 /// Ranges for each worker.
8 pub ranges: Vec<std::ops::Range<usize>>,
9 /// Total weight assigned to each worker (for Weighted strategy).
10 pub weights: Vec<f64>,
11}
12impl LoadBalancePlan {
13 /// Number of workers in this plan.
14 pub fn num_workers(&self) -> usize {
15 self.ranges.len()
16 }
17 /// Maximum weight across workers (a measure of imbalance).
18 pub fn max_weight(&self) -> f64 {
19 self.weights
20 .iter()
21 .copied()
22 .fold(f64::NEG_INFINITY, f64::max)
23 }
24 /// Imbalance ratio: max_weight / avg_weight. 1.0 is perfect balance.
25 pub fn imbalance_ratio(&self) -> f64 {
26 if self.weights.is_empty() {
27 return 1.0;
28 }
29 let total: f64 = self.weights.iter().sum();
30 let avg = total / self.weights.len() as f64;
31 if avg < 1e-15 {
32 return 1.0;
33 }
34 self.max_weight() / avg
35 }
36}
37/// A simple work-stealing queue for task-parallel scheduling.
38///
39/// Internally backed by a `Vec`T` with a front/back cursor pair. "Stealing"
40/// takes from the front (like a deque), while the owner pushes/pops from the
41/// back.
42pub struct WorkStealQueue<T> {
43 pub(super) items: std::collections::VecDeque<T>,
44}
45impl<T: Send> WorkStealQueue<T> {
46 /// Create an empty work-steal queue.
47 pub fn new() -> Self {
48 Self {
49 items: std::collections::VecDeque::new(),
50 }
51 }
52 /// Push a task onto the owner end (back).
53 pub fn push(&mut self, task: T) {
54 self.items.push_back(task);
55 }
56 /// Pop a task from the owner end (back). Returns `None` if empty.
57 pub fn pop(&mut self) -> Option<T> {
58 self.items.pop_back()
59 }
60 /// Steal a task from the thief end (front). Returns `None` if empty.
61 pub fn steal(&mut self) -> Option<T> {
62 self.items.pop_front()
63 }
64 /// Number of pending tasks.
65 pub fn len(&self) -> usize {
66 self.items.len()
67 }
68 /// Whether the queue is empty.
69 pub fn is_empty(&self) -> bool {
70 self.items.is_empty()
71 }
72}
73/// Configuration for choosing optimal work group sizes.
74///
75/// Models GPU-like work group sizing where the total work is divided into
76/// groups of a fixed size, potentially with padding in the last group.
77pub struct WorkGroupConfig {
78 /// Preferred work group size (e.g. 64, 128, 256).
79 pub preferred_size: usize,
80 /// Maximum work group size supported.
81 pub max_size: usize,
82 /// Minimum work group size (avoid groups too small for efficiency).
83 pub min_size: usize,
84}
85impl WorkGroupConfig {
86 /// Create a new config with preferred group size.
87 pub fn new(preferred_size: usize) -> Self {
88 Self {
89 preferred_size: preferred_size.max(1),
90 max_size: 1024,
91 min_size: 32,
92 }
93 }
94 /// Create a default config suitable for CPU-side Rayon parallelism.
95 pub fn cpu_default() -> Self {
96 let threads = rayon::current_num_threads().max(1);
97 Self {
98 preferred_size: 64,
99 max_size: 1024,
100 min_size: threads,
101 }
102 }
103 /// Compute the optimal work group size for `total` items.
104 ///
105 /// Returns a size in `\[min_size, max_size\]` that balances occupancy.
106 /// Prefers `preferred_size` but adjusts if `total` is small.
107 pub fn optimal_size(&self, total: usize) -> usize {
108 if total == 0 {
109 return self.min_size;
110 }
111 if total <= self.preferred_size {
112 return total.max(self.min_size).min(self.max_size);
113 }
114 let preferred_groups = total.div_ceil(self.preferred_size);
115 let preferred_waste = preferred_groups * self.preferred_size - total;
116 let preferred_waste_ratio = preferred_waste as f64 / total as f64;
117 if preferred_waste_ratio < 0.25 {
118 return self.preferred_size;
119 }
120 let mut best_size = self.preferred_size;
121 let mut best_waste = preferred_waste;
122 for candidate in (self.min_size..=self.max_size).step_by(self.min_size) {
123 let groups = total.div_ceil(candidate);
124 let waste = groups * candidate - total;
125 if waste < best_waste {
126 best_waste = waste;
127 best_size = candidate;
128 }
129 }
130 best_size
131 }
132 /// Compute the number of work groups needed for `total` items.
133 pub fn num_groups(&self, total: usize) -> usize {
134 let size = self.optimal_size(total);
135 total.div_ceil(size)
136 }
137 /// Return ranges for each work group covering `0..total`.
138 pub fn group_ranges(&self, total: usize) -> Vec<std::ops::Range<usize>> {
139 let size = self.optimal_size(total);
140 (0..total)
141 .step_by(size.max(1))
142 .map(|start| start..(start + size).min(total))
143 .collect()
144 }
145}
146/// Load balancing strategy for distributing work across threads.
147#[derive(Debug, Clone, Copy, PartialEq)]
148pub enum LoadBalanceStrategy {
149 /// Static: divide work evenly by index count.
150 Static,
151 /// Weighted: divide work so each thread gets roughly equal weight.
152 Weighted,
153 /// Guided: start with large chunks, decrease chunk size as work progresses.
154 Guided,
155}
156/// Splits `n` particle indices into chunks suitable for parallel work.
157///
158/// `chunk_size` is chosen so that each Rayon worker thread gets at least one
159/// chunk.
160pub struct WorkChunker {
161 /// Total number of items.
162 pub n: usize,
163 /// Size of each chunk.
164 pub chunk_size: usize,
165}
166impl WorkChunker {
167 /// Create a new `WorkChunker` for `n` items.
168 ///
169 /// `chunk_size` is set to `n / rayon::current_num_threads() + 1`.
170 pub fn new(n: usize) -> Self {
171 let threads = rayon::current_num_threads().max(1);
172 let chunk_size = n / threads + 1;
173 Self { n, chunk_size }
174 }
175 /// Return contiguous index ranges covering `0..n` without gaps or overlaps.
176 pub fn chunks(&self) -> Vec<std::ops::Range<usize>> {
177 let cs = self.chunk_size.max(1);
178 (0..self.n)
179 .step_by(cs)
180 .map(|start| start..(start + cs).min(self.n))
181 .collect()
182 }
183}