vyre_driver_wgpu/engine/multi_gpu/
partition.rs1use std::cmp::{Ordering, Reverse};
2use std::collections::BinaryHeap;
3
4#[derive(Clone, Debug, Eq, PartialEq)]
6pub struct WeightedWorkItem {
7 pub id: usize,
9 pub cost: u64,
12}
13
14#[derive(Clone, Debug, Eq, PartialEq)]
16pub struct DeviceLoad {
17 pub device_index: usize,
19 pub queued_cost: u64,
21}
22
23#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct Partition {
26 pub device_index: usize,
28 pub item_ids: Vec<usize>,
30 pub total_cost: u64,
32}
33
34#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35struct PartitionHeapEntry {
36 total_cost: u64,
37 device_index: usize,
38 partition_index: usize,
39}
40
41impl Ord for PartitionHeapEntry {
42 fn cmp(&self, other: &Self) -> Ordering {
43 self.total_cost
44 .cmp(&other.total_cost)
45 .then_with(|| self.device_index.cmp(&other.device_index))
46 .then_with(|| self.partition_index.cmp(&other.partition_index))
47 }
48}
49
50impl PartialOrd for PartitionHeapEntry {
51 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
52 Some(self.cmp(other))
53 }
54}
55
56pub fn partition_work_stealing(
64 devices: &[DeviceLoad],
65 items: &[WeightedWorkItem],
66) -> Result<Vec<Partition>, String> {
67 validate_inputs(devices, items)?;
68 let mut partitions = Vec::new();
69 vyre_driver::allocation::try_reserve_vec_to_capacity(&mut partitions, devices.len()).map_err(|error| {
70 format!(
71 "partition table allocation failed for {} GPU devices: {error}. Fix: lower the adapter fanout or memory pressure before scheduling.",
72 devices.len()
73 )
74 })?;
75 let mut least_loaded = BinaryHeap::new();
76 vyre_foundation::allocation::try_reserve_binary_heap_to_capacity(
77 &mut least_loaded,
78 devices.len(),
79 )
80 .map_err(|error| {
81 format!(
82 "partition heap allocation failed for {} GPU devices: {error}. Fix: lower the adapter fanout or memory pressure before scheduling.",
83 devices.len()
84 )
85 })?;
86 let target_item_capacity = items.len().div_ceil(devices.len());
87 for device in devices {
88 let partition_index = partitions.len();
89 let mut item_ids = Vec::new();
90 vyre_driver::allocation::try_reserve_vec_to_capacity(&mut item_ids, target_item_capacity).map_err(|error| {
91 format!(
92 "partition assignment allocation failed for GPU device {} and {} target work slots: {error}. Fix: split the multi-GPU batch.",
93 device.device_index, target_item_capacity
94 )
95 })?;
96 partitions.push(Partition {
97 device_index: device.device_index,
98 item_ids,
99 total_cost: device.queued_cost,
100 });
101 least_loaded.push(Reverse(PartitionHeapEntry {
102 total_cost: device.queued_cost,
103 device_index: device.device_index,
104 partition_index,
105 }));
106 }
107
108 let mut ordered = Vec::new();
109 vyre_driver::allocation::try_reserve_vec_to_capacity(&mut ordered, items.len()).map_err(|error| {
110 format!(
111 "partition work-order allocation failed for {} items: {error}. Fix: split the multi-GPU batch.",
112 items.len()
113 )
114 })?;
115 ordered.extend(items.iter());
116 ordered.sort_by(|left, right| {
117 right
118 .cost
119 .cmp(&left.cost)
120 .then_with(|| left.id.cmp(&right.id))
121 });
122
123 for item in ordered {
124 let Some(mut target) = least_loaded.pop().map(|entry| entry.0) else {
125 return Err(
126 "partition target not found. Fix: validate non-empty device list before partitioning."
127 .to_string()
128 );
129 };
130 let partition = &mut partitions[target.partition_index];
131 ensure_vec_spare(&mut partition.item_ids, 1, "partition assignment list")?;
132 partition.item_ids.push(item.id);
133 partition.total_cost = partition.total_cost.checked_add(item.cost).ok_or_else(|| {
134 "partition cost overflow. Fix: split the batch before multi-GPU scheduling.".to_string()
135 })?;
136 target.total_cost = partition.total_cost;
137 least_loaded.push(Reverse(target));
138 }
139 Ok(partitions)
140}
141
142fn validate_inputs(devices: &[DeviceLoad], items: &[WeightedWorkItem]) -> Result<(), String> {
143 if devices.is_empty() {
144 return Err(
145 "no GPU devices supplied. Fix: probe adapters before partitioning.".to_string(),
146 );
147 }
148 let mut seen = rustc_hash::FxHashSet::default();
149 vyre_foundation::allocation::try_reserve_hash_set_to_capacity(&mut seen, devices.len()).map_err(|error| {
150 format!(
151 "GPU device validation allocation failed for {} devices: {error}. Fix: lower adapter fanout or memory pressure before scheduling.",
152 devices.len()
153 )
154 })?;
155 for device in devices {
156 if !seen.insert(device.device_index) {
157 return Err(format!(
158 "duplicate GPU device index {}. Fix: pass each adapter exactly once.",
159 device.device_index
160 ));
161 }
162 }
163 for item in items {
164 if item.cost == 0 {
165 return Err(format!(
166 "work item {} has zero cost. Fix: assign at least one cost unit or remove it.",
167 item.id
168 ));
169 }
170 }
171 let mut seen_items = rustc_hash::FxHashSet::default();
172 vyre_foundation::allocation::try_reserve_hash_set_to_capacity(&mut seen_items, items.len()).map_err(|error| {
173 format!(
174 "work item validation allocation failed for {} items: {error}. Fix: split the multi-GPU batch.",
175 items.len()
176 )
177 })?;
178 for item in items {
179 if !seen_items.insert(item.id) {
180 return Err(format!(
181 "duplicate work item id {}. Fix: assign a unique stable id to every multi-GPU work item.",
182 item.id
183 ));
184 }
185 }
186 Ok(())
187}
188
189fn ensure_vec_spare<T>(vec: &mut Vec<T>, additional: usize, label: &str) -> Result<(), String> {
190 let spare = vec.capacity().saturating_sub(vec.len());
191 if spare >= additional {
192 return Ok(());
193 }
194 vec.try_reserve_exact(additional - spare).map_err(|error| {
195 format!(
196 "{label} allocation failed while extending by {additional} entries: {error}. Fix: split the multi-GPU batch."
197 )
198 })
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn multi_gpu_partition_unit() {
207 let devices = [
208 DeviceLoad {
209 device_index: 0,
210 queued_cost: 0,
211 },
212 DeviceLoad {
213 device_index: 1,
214 queued_cost: 4,
215 },
216 ];
217 let items = [
218 WeightedWorkItem { id: 10, cost: 9 },
219 WeightedWorkItem { id: 11, cost: 4 },
220 WeightedWorkItem { id: 12, cost: 4 },
221 WeightedWorkItem { id: 13, cost: 1 },
222 ];
223
224 let partitions = partition_work_stealing(&devices, &items)
225 .expect("Fix: valid synthetic device loads must partition");
226 let mut assigned = partitions
227 .iter()
228 .flat_map(|partition| partition.item_ids.iter().copied())
229 .collect::<Vec<_>>();
230 assigned.sort_unstable();
231
232 assert_eq!(assigned, vec![10, 11, 12, 13]);
233 let spread = partitions
234 .iter()
235 .map(|partition| partition.total_cost)
236 .max()
237 .zip(
238 partitions
239 .iter()
240 .map(|partition| partition.total_cost)
241 .min(),
242 )
243 .map(|(max, min)| max - min)
244 .expect("Fix: partitions must be non-empty");
245 assert!(
246 spread <= 5,
247 "synthetic work stealing left an avoidable load spread: {partitions:?}"
248 );
249 }
250
251 #[test]
252 fn rejects_duplicate_device_ordinals() {
253 let devices = [
254 DeviceLoad {
255 device_index: 0,
256 queued_cost: 0,
257 },
258 DeviceLoad {
259 device_index: 0,
260 queued_cost: 1,
261 },
262 ];
263
264 let error = partition_work_stealing(&devices, &[WeightedWorkItem { id: 1, cost: 1 }])
265 .expect_err("Fix: duplicate synthetic device indices must be rejected");
266 assert!(error.contains("duplicate GPU device index"));
267 }
268
269 #[test]
270 fn partition_source_has_no_release_path_infallible_allocation() {
271 let source = include_str!("partition.rs");
272 let production = source
273 .split("#[cfg(test)]")
274 .next()
275 .expect("Fix: partition production source must precede tests");
276 assert!(
277 !production.contains("Vec::with_capacity")
278 && !production.contains("BinaryHeap::with_capacity")
279 && !production.contains("with_capacity_and_hasher"),
280 "Fix: WGPU multi-GPU partitioning must report allocation pressure instead of aborting on infallible capacity constructors."
281 );
282 assert!(
283 production.contains("try_reserve_vec_to_capacity")
284 && production.contains("try_reserve_binary_heap_to_capacity")
285 && production.contains("try_reserve_hash_set_to_capacity")
286 && production.contains("ensure_vec_spare"),
287 "Fix: WGPU multi-GPU partitioning must use fallible allocation for schedule tables and per-device assignments."
288 );
289 }
290}