vyre_runtime/
scheduler.rs1use std::ops::Range;
7use std::sync::atomic::{AtomicUsize, Ordering};
8use std::sync::Arc;
9use vyre_driver::{BackendError, VyreBackend};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Shard {
14 pub backend_id: &'static str,
16 pub work_range: Range<usize>,
18}
19
20pub struct WorkStealingScheduler {
22 backends: Vec<Arc<dyn VyreBackend>>,
23 work_index: AtomicUsize,
30}
31
32impl WorkStealingScheduler {
33 pub fn new(backends: Vec<Arc<dyn VyreBackend>>) -> Self {
35 Self {
36 backends,
37 work_index: AtomicUsize::new(0),
38 }
39 }
40
41 pub fn partition(&self, total_len: usize) -> Vec<Shard> {
43 match self.try_partition(total_len) {
44 Ok(shards) => shards,
45 Err(_error) => Vec::new(),
46 }
47 }
48
49 pub fn try_partition(&self, total_len: usize) -> Result<Vec<Shard>, BackendError> {
52 let mut shards = Vec::new();
53 self.try_partition_into(total_len, &mut shards)?;
54 Ok(shards)
55 }
56
57 #[must_use]
74 pub fn claim_next_unit(&self) -> usize {
75 self.work_index.fetch_add(1, Ordering::AcqRel)
76 }
77
78 pub fn reset_unit_cursor(&self) {
81 self.work_index.store(0, Ordering::Release);
82 }
83
84 pub fn partition_into(&self, total_len: usize, out: &mut Vec<Shard>) {
93 if let Err(error) = self.try_partition_into(total_len, out) {
97 panic!("vyre-runtime work-unit partition failed: {error}");
98 }
99 }
100
101 pub fn try_partition_into(
104 &self,
105 total_len: usize,
106 out: &mut Vec<Shard>,
107 ) -> Result<(), BackendError> {
108 let n = self.backends.len();
109 out.clear();
110 if n == 0 || total_len == 0 {
111 return Ok(());
112 }
113 let work_unit_size = partition_work_unit_size(total_len, n);
114 let num_units = total_len.div_ceil(work_unit_size);
115 vyre_foundation::allocation::try_reserve_vec_to_capacity(out, num_units).map_err(
116 |error| BackendError::InvalidProgram {
117 fix: format!(
118 "Fix: scheduler could not reserve {num_units} GPU work shard(s): {error}. Shard the workload before work-stealing partitioning."
119 ),
120 },
121 )?;
122 let mut start = 0;
123 for i in 0..num_units {
124 let end = (start + work_unit_size).min(total_len);
125 out.push(Shard {
126 backend_id: self.backends[i % n].id(),
127 work_range: start..end,
128 });
129 start = end;
130 }
131 Ok(())
132 }
133}
134
135fn partition_work_unit_size(total_len: usize, backend_count: usize) -> usize {
136 if total_len == 0 || backend_count == 0 {
137 return 1;
138 }
139 let denominator = backend_count.checked_mul(4).unwrap_or(usize::MAX);
140 (total_len / denominator.max(1)).max(1)
141}
142
143#[cfg(test)]
144fn partition_ranges(total_len: usize, backend_count: usize) -> Vec<Range<usize>> {
145 if backend_count == 0 || total_len == 0 {
146 return Vec::new();
147 }
148 let work_unit_size = partition_work_unit_size(total_len, backend_count);
149 let num_units = total_len.div_ceil(work_unit_size);
150 let mut ranges = Vec::with_capacity(num_units);
151 let mut start = 0;
152 for _ in 0..num_units {
153 let end = (start + work_unit_size).min(total_len);
154 ranges.push(start..end);
155 start = end;
156 }
157 ranges
158}
159
160#[cfg(test)]
161mod tests {
162 use super::{partition_ranges, WorkStealingScheduler};
163 use std::sync::Arc;
164 use vyre_driver::backend::{DispatchConfig, VyreBackend};
165 use vyre_foundation::ir::Program;
166
167 struct TestBackend(&'static str);
168
169 impl vyre_driver::backend::private::Sealed for TestBackend {}
170
171 impl VyreBackend for TestBackend {
172 fn id(&self) -> &'static str {
173 self.0
174 }
175
176 fn dispatch(
177 &self,
178 _program: &Program,
179 _inputs: &[Vec<u8>],
180 _config: &DispatchConfig,
181 ) -> Result<Vec<Vec<u8>>, vyre_driver::BackendError> {
182 Ok(Vec::new())
183 }
184 }
185
186 #[test]
187 fn partition_ranges_produces_fine_grained_units() {
188 let ranges = partition_ranges(10, 3);
189 assert_eq!(ranges.len(), 10);
190 assert_eq!(
191 ranges,
192 vec![0..1, 1..2, 2..3, 3..4, 4..5, 5..6, 6..7, 7..8, 8..9, 9..10]
193 );
194 }
195
196 #[test]
197 fn partition_ranges_never_emits_empty_shards() {
198 let ranges = partition_ranges(2, 8);
199 assert_eq!(ranges, vec![0..1, 1..2]);
200 }
201
202 #[test]
203 fn partition_ranges_uses_overflow_safe_work_unit_math() {
204 let ranges = partition_ranges(2, usize::MAX);
205 assert_eq!(ranges[0], 0..1);
206 assert_eq!(ranges[1], 1..2);
207 assert_eq!(
208 super::partition_work_unit_size(2, usize::MAX),
209 1,
210 "backend_count * 4 overflow must not panic or enlarge the work unit"
211 );
212 }
213
214 #[test]
215 fn scheduler_partition_into_reuses_output_storage() {
216 let scheduler = WorkStealingScheduler::new(vec![
217 Arc::new(TestBackend("a")),
218 Arc::new(TestBackend("b")),
219 Arc::new(TestBackend("c")),
220 ]);
221 let mut shards = Vec::with_capacity(10);
222
223 scheduler.partition_into(10, &mut shards);
224 let ptr = shards.as_ptr();
225 scheduler.partition_into(10, &mut shards);
226
227 assert_eq!(shards.as_ptr(), ptr);
228 assert_eq!(shards.len(), 10);
229 assert_eq!(shards[0].backend_id, "a");
230 assert_eq!(shards[0].work_range, 0..1);
231 assert_eq!(shards[1].backend_id, "b");
232 assert_eq!(shards[1].work_range, 1..2);
233 assert_eq!(shards[9].backend_id, "a");
234 assert_eq!(shards[9].work_range, 9..10);
235 assert_eq!(
236 scheduler
237 .work_index
238 .load(std::sync::atomic::Ordering::Relaxed),
239 0
240 );
241 }
242}