vyre_runtime/
scheduler.rs1use std::ops::Range;
7use std::sync::atomic::{AtomicUsize, Ordering};
8use vyre_driver::BackendError;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Shard {
13 pub backend_id: &'static str,
15 pub work_range: Range<usize>,
17}
18
19pub struct WorkStealingScheduler {
21 backend_ids: Vec<&'static str>,
22 work_index: AtomicUsize,
29}
30
31impl WorkStealingScheduler {
32 pub fn new(backend_ids: Vec<&'static str>) -> Self {
34 Self {
35 backend_ids,
36 work_index: AtomicUsize::new(0),
37 }
38 }
39
40 pub fn partition(&self, total_len: usize) -> Vec<Shard> {
42 self.try_partition(total_len).unwrap_or_default()
43 }
44
45 pub fn try_partition(&self, total_len: usize) -> Result<Vec<Shard>, BackendError> {
48 let mut shards = Vec::new();
49 self.try_partition_into(total_len, &mut shards)?;
50 Ok(shards)
51 }
52
53 #[must_use]
70 pub fn claim_next_unit(&self) -> usize {
71 self.work_index.fetch_add(1, Ordering::AcqRel)
72 }
73
74 pub fn reset_unit_cursor(&self) {
77 self.work_index.store(0, Ordering::Release);
78 }
79
80 pub fn partition_into(&self, total_len: usize, out: &mut Vec<Shard>) {
89 if let Err(error) = self.try_partition_into(total_len, out) {
93 panic!("vyre-runtime work-unit partition failed: {error}");
94 }
95 }
96
97 pub fn try_partition_into(
100 &self,
101 total_len: usize,
102 out: &mut Vec<Shard>,
103 ) -> Result<(), BackendError> {
104 let n = self.backend_ids.len();
105 out.clear();
106 if n == 0 || total_len == 0 {
107 return Ok(());
108 }
109 let work_unit_size = partition_work_unit_size(total_len, n);
110 let num_units = total_len.div_ceil(work_unit_size);
111 vyre_foundation::allocation::try_reserve_vec_to_capacity(out, num_units).map_err(
112 |error| BackendError::InvalidProgram {
113 fix: format!(
114 "Fix: scheduler could not reserve {num_units} GPU work shard(s): {error}. Shard the workload before work-stealing partitioning."
115 ),
116 },
117 )?;
118 let mut start = 0;
119 for i in 0..num_units {
120 let end = (start + work_unit_size).min(total_len);
121 out.push(Shard {
122 backend_id: self.backend_ids[i % n],
123 work_range: start..end,
124 });
125 start = end;
126 }
127 Ok(())
128 }
129}
130
131fn partition_work_unit_size(total_len: usize, backend_count: usize) -> usize {
132 if total_len == 0 || backend_count == 0 {
133 return 1;
134 }
135 let denominator = backend_count.checked_mul(4).unwrap_or(usize::MAX);
136 (total_len / denominator.max(1)).max(1)
137}
138
139#[cfg(test)]
140fn partition_ranges(total_len: usize, backend_count: usize) -> Vec<Range<usize>> {
141 if backend_count == 0 || total_len == 0 {
142 return Vec::new();
143 }
144 let work_unit_size = partition_work_unit_size(total_len, backend_count);
145 let num_units = total_len.div_ceil(work_unit_size);
146 let mut ranges = Vec::with_capacity(num_units);
147 let mut start = 0;
148 for _ in 0..num_units {
149 let end = (start + work_unit_size).min(total_len);
150 ranges.push(start..end);
151 start = end;
152 }
153 ranges
154}
155
156#[cfg(test)]
157mod tests {
158 use super::{partition_ranges, WorkStealingScheduler};
159
160 #[test]
161 fn partition_ranges_produces_fine_grained_units() {
162 let ranges = partition_ranges(10, 3);
163 assert_eq!(ranges.len(), 10);
164 assert_eq!(
165 ranges,
166 vec![0..1, 1..2, 2..3, 3..4, 4..5, 5..6, 6..7, 7..8, 8..9, 9..10]
167 );
168 }
169
170 #[test]
171 fn partition_ranges_never_emits_empty_shards() {
172 let ranges = partition_ranges(2, 8);
173 assert_eq!(ranges, vec![0..1, 1..2]);
174 }
175
176 #[test]
177 fn partition_ranges_uses_overflow_safe_work_unit_math() {
178 let ranges = partition_ranges(2, usize::MAX);
179 assert_eq!(ranges[0], 0..1);
180 assert_eq!(ranges[1], 1..2);
181 assert_eq!(
182 super::partition_work_unit_size(2, usize::MAX),
183 1,
184 "backend_count * 4 overflow must not panic or enlarge the work unit"
185 );
186 }
187
188 #[test]
189 fn scheduler_partition_into_reuses_output_storage() {
190 let scheduler = WorkStealingScheduler::new(vec!["a", "b", "c"]);
191 let mut shards = Vec::with_capacity(10);
192
193 scheduler.partition_into(10, &mut shards);
194 let ptr = shards.as_ptr();
195 scheduler.partition_into(10, &mut shards);
196
197 assert_eq!(shards.as_ptr(), ptr);
198 assert_eq!(shards.len(), 10);
199 assert_eq!(shards[0].backend_id, "a");
200 assert_eq!(shards[0].work_range, 0..1);
201 assert_eq!(shards[1].backend_id, "b");
202 assert_eq!(shards[1].work_range, 1..2);
203 assert_eq!(shards[9].backend_id, "a");
204 assert_eq!(shards[9].work_range, 9..10);
205 assert_eq!(
206 scheduler
207 .work_index
208 .load(std::sync::atomic::Ordering::Relaxed),
209 0
210 );
211 }
212}