1use crate::context::{GpuContext, GpuContextConfig};
7use crate::error::{GpuError, GpuResult};
8use std::collections::HashMap;
9use std::sync::{Arc, Mutex};
10use tracing::{debug, info, warn};
11use wgpu::{Adapter, AdapterInfo, Backend, Backends, BufferUsages, Instance, PollType};
12
13#[derive(Debug, Clone)]
15pub struct MultiGpuConfig {
16 pub backends: Backends,
18 pub min_devices: usize,
20 pub max_devices: usize,
22 pub auto_load_balance: bool,
24 pub enable_p2p: bool,
26}
27
28impl Default for MultiGpuConfig {
29 fn default() -> Self {
30 Self {
31 backends: Backends::all(),
32 min_devices: 1,
33 max_devices: 8,
34 auto_load_balance: true,
35 enable_p2p: false,
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct GpuDeviceInfo {
43 pub index: usize,
45 pub adapter_info: AdapterInfo,
47 pub backend: Backend,
49 pub vram_bytes: Option<u64>,
51 pub active: bool,
53}
54
55impl GpuDeviceInfo {
56 pub fn description(&self) -> String {
58 format!(
59 "GPU {} : {} ({:?})",
60 self.index, self.adapter_info.name, self.backend
61 )
62 }
63}
64
65pub struct MultiGpuManager {
67 devices: Vec<Arc<GpuContext>>,
69 device_info: Vec<GpuDeviceInfo>,
71 config: MultiGpuConfig,
73 load_state: Arc<Mutex<LoadBalanceState>>,
75}
76
77impl MultiGpuManager {
78 #[cfg(test)]
84 pub(crate) fn new_empty_for_testing() -> Self {
85 Self {
86 devices: Vec::new(),
87 device_info: Vec::new(),
88 config: MultiGpuConfig::default(),
89 load_state: Arc::new(Mutex::new(LoadBalanceState::new(0))),
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
95struct LoadBalanceState {
96 task_counts: HashMap<usize, usize>,
98 workload: HashMap<usize, f64>,
100}
101
102impl LoadBalanceState {
103 fn new(num_devices: usize) -> Self {
104 let mut task_counts = HashMap::new();
105 let mut workload = HashMap::new();
106
107 for i in 0..num_devices {
108 task_counts.insert(i, 0);
109 workload.insert(i, 0.0);
110 }
111
112 Self {
113 task_counts,
114 workload,
115 }
116 }
117
118 fn select_device(&self) -> usize {
119 self.workload
121 .iter()
122 .min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
123 .map(|(idx, _)| *idx)
124 .unwrap_or(0)
125 }
126
127 fn add_task(&mut self, device: usize, workload: f64) {
128 *self.task_counts.entry(device).or_insert(0) += 1;
129 *self.workload.entry(device).or_insert(0.0) += workload;
130 }
131
132 fn complete_task(&mut self, device: usize, workload: f64) {
133 if let Some(count) = self.task_counts.get_mut(&device) {
134 *count = count.saturating_sub(1);
135 }
136 if let Some(load) = self.workload.get_mut(&device) {
137 *load = load.max(workload) - workload;
138 }
139 }
140}
141
142impl MultiGpuManager {
143 pub async fn new(config: MultiGpuConfig) -> GpuResult<Self> {
149 info!("Initializing multi-GPU manager");
150
151 let instance = Instance::new(wgpu::InstanceDescriptor {
152 backends: config.backends,
153 ..wgpu::InstanceDescriptor::new_without_display_handle()
154 });
155
156 let adapters = Self::enumerate_adapters(&instance).await;
158
159 if adapters.len() < config.min_devices {
160 return Err(GpuError::no_adapter(format!(
161 "Found {} GPUs, but {} required",
162 adapters.len(),
163 config.min_devices
164 )));
165 }
166
167 let num_devices = adapters.len().min(config.max_devices);
168 info!(
169 "Found {} compatible GPUs, using {}",
170 adapters.len(),
171 num_devices
172 );
173
174 let mut devices = Vec::new();
176 let mut device_info = Vec::new();
177
178 for (index, adapter) in adapters.into_iter().take(num_devices).enumerate() {
179 match Self::create_device_context(adapter, index).await {
180 Ok((context, info)) => {
181 devices.push(Arc::new(context));
182 device_info.push(info);
183 info!(
184 "Initialized: {}",
185 device_info
186 .last()
187 .map(|i| i.description())
188 .unwrap_or_default()
189 );
190 }
191 Err(e) => {
192 warn!("Failed to initialize GPU {}: {}", index, e);
193 }
194 }
195 }
196
197 if devices.len() < config.min_devices {
198 return Err(GpuError::device_request(format!(
199 "Successfully initialized {} GPUs, but {} required",
200 devices.len(),
201 config.min_devices
202 )));
203 }
204
205 let load_state = Arc::new(Mutex::new(LoadBalanceState::new(devices.len())));
206
207 Ok(Self {
208 devices,
209 device_info,
210 config,
211 load_state,
212 })
213 }
214
215 pub fn num_devices(&self) -> usize {
217 self.devices.len()
218 }
219
220 pub fn device(&self, index: usize) -> Option<&Arc<GpuContext>> {
222 self.devices.get(index)
223 }
224
225 pub fn devices(&self) -> &[Arc<GpuContext>] {
227 &self.devices
228 }
229
230 pub fn device_info(&self, index: usize) -> Option<&GpuDeviceInfo> {
232 self.device_info.get(index)
233 }
234
235 pub fn all_device_info(&self) -> &[GpuDeviceInfo] {
237 &self.device_info
238 }
239
240 pub fn select_device(&self) -> usize {
242 if !self.config.auto_load_balance {
243 return 0; }
246
247 self.load_state
248 .lock()
249 .map(|state| state.select_device())
250 .unwrap_or(0)
251 }
252
253 pub fn dispatch<F, T>(&self, workload: f64, f: F) -> GpuResult<T>
255 where
256 F: FnOnce(&GpuContext) -> GpuResult<T>,
257 {
258 let device_idx = self.select_device();
259
260 if let Ok(mut state) = self.load_state.lock() {
261 state.add_task(device_idx, workload);
262 }
263
264 let context = self
265 .devices
266 .get(device_idx)
267 .ok_or_else(|| GpuError::internal("Invalid device index"))?;
268
269 let result = f(context);
270
271 if let Ok(mut state) = self.load_state.lock() {
272 state.complete_task(device_idx, workload);
273 }
274
275 result
276 }
277
278 pub async fn distribute<F, T>(&self, items: Vec<(f64, F)>) -> Vec<GpuResult<T>>
280 where
281 F: FnOnce(&GpuContext) -> GpuResult<T> + Send + 'static,
282 T: Send + 'static,
283 {
284 let mut tasks = Vec::new();
285
286 for (workload, work_fn) in items {
287 let device_idx = self.select_device();
288
289 if let Ok(mut state) = self.load_state.lock() {
290 state.add_task(device_idx, workload);
291 }
292
293 let context = match self.devices.get(device_idx) {
294 Some(ctx) => Arc::clone(ctx),
295 None => continue,
296 };
297
298 let load_state = Arc::clone(&self.load_state);
299
300 let task = tokio::spawn(async move {
301 let result = work_fn(&context);
302
303 if let Ok(mut state) = load_state.lock() {
304 state.complete_task(device_idx, workload);
305 }
306
307 result
308 });
309
310 tasks.push(task);
311 }
312
313 let mut results = Vec::new();
315 for task in tasks {
316 match task.await {
317 Ok(result) => results.push(result),
318 Err(e) => results.push(Err(GpuError::internal(e.to_string()))),
319 }
320 }
321
322 results
323 }
324
325 pub fn load_stats(&self) -> HashMap<usize, (usize, f64)> {
327 self.load_state
328 .lock()
329 .map(|state| {
330 let mut stats = HashMap::new();
331 for i in 0..self.devices.len() {
332 let tasks = *state.task_counts.get(&i).unwrap_or(&0);
333 let workload = *state.workload.get(&i).unwrap_or(&0.0);
334 stats.insert(i, (tasks, workload));
335 }
336 stats
337 })
338 .unwrap_or_default()
339 }
340
341 async fn enumerate_adapters(_instance: &Instance) -> Vec<Adapter> {
342 let mut adapters = Vec::new();
343
344 for backend in &[
346 Backends::VULKAN,
347 Backends::METAL,
348 Backends::DX12,
349 Backends::BROWSER_WEBGPU,
350 ] {
351 let backend_instance = Instance::new(wgpu::InstanceDescriptor {
352 backends: *backend,
353 ..wgpu::InstanceDescriptor::new_without_display_handle()
354 });
355
356 if let Ok(adapter) = backend_instance
357 .request_adapter(&wgpu::RequestAdapterOptions {
358 power_preference: wgpu::PowerPreference::HighPerformance,
359 force_fallback_adapter: false,
360 compatible_surface: None,
361 apply_limit_buckets: false,
362 })
363 .await
364 {
365 adapters.push(adapter);
366 }
367 }
368
369 adapters
370 }
371
372 async fn create_device_context(
373 adapter: Adapter,
374 index: usize,
375 ) -> GpuResult<(GpuContext, GpuDeviceInfo)> {
376 let adapter_info = adapter.get_info();
377 let backend = adapter_info.backend;
378
379 let vram_bytes = Self::estimate_vram(&adapter_info);
381
382 let config = GpuContextConfig::default().with_label(format!("GPU {}", index));
383
384 let context = GpuContext::with_config(config).await?;
385
386 let info = GpuDeviceInfo {
387 index,
388 adapter_info,
389 backend,
390 vram_bytes,
391 active: true,
392 };
393
394 Ok((context, info))
395 }
396
397 fn estimate_vram(adapter_info: &AdapterInfo) -> Option<u64> {
398 match adapter_info.device_type {
400 wgpu::DeviceType::DiscreteGpu => Some(8 * 1024 * 1024 * 1024), wgpu::DeviceType::IntegratedGpu => Some(2 * 1024 * 1024 * 1024), wgpu::DeviceType::VirtualGpu => Some(4 * 1024 * 1024 * 1024), _ => None,
404 }
405 }
406}
407
408pub struct InterGpuTransfer {
416 manager: Arc<MultiGpuManager>,
417 per_device_buffers: Vec<Arc<Mutex<Option<(Arc<wgpu::Buffer>, u64)>>>>,
420}
421
422impl InterGpuTransfer {
423 pub fn new(manager: Arc<MultiGpuManager>) -> Self {
425 let num_devices = manager.num_devices();
426 let per_device_buffers = (0..num_devices)
427 .map(|_| Arc::new(Mutex::new(None)))
428 .collect();
429 Self {
430 manager,
431 per_device_buffers,
432 }
433 }
434
435 pub fn set_device_buffer(
445 &self,
446 device_idx: usize,
447 buffer: Arc<wgpu::Buffer>,
448 size_bytes: u64,
449 ) -> GpuResult<()> {
450 let slot = self
451 .per_device_buffers
452 .get(device_idx)
453 .ok_or_else(|| GpuError::invalid_buffer("device_idx out of range"))?;
454 *slot
455 .lock()
456 .map_err(|_| GpuError::internal("per_device_buffers lock poisoned"))? =
457 Some((buffer, size_bytes));
458 Ok(())
459 }
460
461 pub async fn copy_buffer(
467 &self,
468 src_device: usize,
469 dst_device: usize,
470 data: &[u8],
471 ) -> GpuResult<()> {
472 let _src_ctx = self
473 .manager
474 .device(src_device)
475 .ok_or_else(|| GpuError::invalid_buffer("Invalid source device"))?;
476
477 let dst_ctx = self
478 .manager
479 .device(dst_device)
480 .ok_or_else(|| GpuError::invalid_buffer("Invalid destination device"))?;
481
482 let dst_buffer = dst_ctx.device().create_buffer(&wgpu::BufferDescriptor {
484 label: Some("Inter-GPU Transfer"),
485 size: data.len() as u64,
486 usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
487 mapped_at_creation: false,
488 });
489
490 dst_ctx.queue().write_buffer(&dst_buffer, 0, data);
492
493 debug!(
494 "Transferred {} bytes from GPU {} to GPU {}",
495 data.len(),
496 src_device,
497 dst_device
498 );
499
500 Ok(())
501 }
502
503 pub async fn broadcast(&self, data: &[u8]) -> GpuResult<()> {
509 for i in 1..self.manager.num_devices() {
510 self.copy_buffer(0, i, data).await?;
511 }
512
513 Ok(())
514 }
515
516 pub async fn gather(&self, dst_device: usize) -> GpuResult<Vec<Vec<u8>>> {
541 let num_devices = self.per_device_buffers.len();
546
547 if dst_device >= num_devices {
548 return Err(GpuError::invalid_buffer(format!(
549 "dst_device {} is out of range (num_devices = {})",
550 dst_device, num_devices
551 )));
552 }
553
554 let mut results: Vec<Vec<u8>> = Vec::with_capacity(num_devices.saturating_sub(1));
555
556 for src_idx in 0..num_devices {
557 if src_idx == dst_device {
558 continue;
559 }
560
561 let slot = self
565 .per_device_buffers
566 .get(src_idx)
567 .ok_or_else(|| GpuError::internal("per_device_buffers shorter than num_devices"))?;
568
569 let maybe_buffer_info: Option<(Arc<wgpu::Buffer>, u64)> = {
573 let guard = slot
574 .lock()
575 .map_err(|_| GpuError::internal("per_device_buffers lock poisoned"))?;
576 guard.as_ref().map(|(buf, sz)| (Arc::clone(buf), *sz))
577 };
579
580 let (src_buffer, size_bytes) = match maybe_buffer_info {
582 Some(pair) => pair,
583 None => {
584 debug!(
585 "gather: device {} has no registered buffer, returning empty slice",
586 src_idx
587 );
588 results.push(Vec::new());
589 continue;
590 }
591 };
592
593 let ctx = self
598 .manager
599 .device(src_idx)
600 .ok_or_else(|| GpuError::invalid_buffer("source device context missing"))?;
601
602 let wgpu_device = ctx.device();
607 let wgpu_queue = ctx.queue();
608
609 let staging = wgpu_device.create_buffer(&wgpu::BufferDescriptor {
610 label: Some("gather_staging"),
611 size: size_bytes,
612 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
613 mapped_at_creation: false,
614 });
615
616 let mut encoder = wgpu_device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
618 label: Some("gather_copy_encoder"),
619 });
620 encoder.copy_buffer_to_buffer(&src_buffer, 0, &staging, 0, size_bytes);
621 wgpu_queue.submit(std::iter::once(encoder.finish()));
622
623 wgpu_device
627 .poll(PollType::wait_indefinitely())
628 .map_err(|e| GpuError::execution_failed(format!("device poll error: {e:?}")))?;
629
630 let (tx, rx) =
632 futures::channel::oneshot::channel::<Result<(), wgpu::BufferAsyncError>>();
633 staging.slice(..).map_async(wgpu::MapMode::Read, move |r| {
634 let _ = tx.send(r);
635 });
636
637 wgpu_device
639 .poll(PollType::wait_indefinitely())
640 .map_err(|e| {
641 GpuError::execution_failed(format!("device poll (map) error: {e:?}"))
642 })?;
643
644 rx.await
645 .map_err(|_| GpuError::buffer_mapping("gather: oneshot channel closed"))?
646 .map_err(|e| GpuError::buffer_mapping(format!("gather: map_async failed: {e}")))?;
647
648 let raw_data: Vec<u8> = staging
649 .slice(..)
650 .get_mapped_range()
651 .map_err(|e| {
652 GpuError::buffer_mapping(format!("gather: get_mapped_range failed: {e}"))
653 })?
654 .to_vec();
655 staging.unmap();
656
657 debug!(
658 "gather: read {} bytes from device {} (staging buffer)",
659 raw_data.len(),
660 src_idx
661 );
662
663 results.push(raw_data);
664 }
665
666 Ok(results)
667 }
668
669 pub fn gather_blocking(&self, dst_device: usize) -> GpuResult<Vec<Vec<u8>>> {
679 pollster::block_on(self.gather(dst_device))
680 }
681}
682
683pub struct GpuAffinityManager {
685 affinity_groups: HashMap<usize, Vec<usize>>,
687}
688
689impl GpuAffinityManager {
690 pub fn new() -> Self {
692 Self {
693 affinity_groups: HashMap::new(),
694 }
695 }
696
697 pub fn set_affinity_group(&mut self, group_id: usize, devices: Vec<usize>) {
699 self.affinity_groups.insert(group_id, devices);
700 }
701
702 pub fn get_affinity_group(&self, device: usize) -> Vec<usize> {
704 for (_, devices) in &self.affinity_groups {
705 if devices.contains(&device) {
706 return devices.clone();
707 }
708 }
709 vec![device]
710 }
711
712 pub fn same_affinity(&self, device_a: usize, device_b: usize) -> bool {
714 let group_a = self.get_affinity_group(device_a);
715 group_a.contains(&device_b)
716 }
717
718 pub fn optimal_device(&self, data_device: usize, available: &[usize]) -> Option<usize> {
720 let group = self.get_affinity_group(data_device);
722
723 for device in available {
724 if group.contains(device) {
725 return Some(*device);
726 }
727 }
728
729 available.first().copied()
731 }
732}
733
734impl Default for GpuAffinityManager {
735 fn default() -> Self {
736 Self::new()
737 }
738}
739
740#[derive(Debug, Clone, Copy, PartialEq, Eq)]
742pub enum DistributionStrategy {
743 RoundRobin,
745 LoadBalanced,
747 DataLocal,
749 SingleDevice,
751}
752
753pub struct WorkDistributor {
755 manager: Arc<MultiGpuManager>,
756 strategy: DistributionStrategy,
757 affinity: GpuAffinityManager,
758}
759
760impl WorkDistributor {
761 pub fn new(manager: Arc<MultiGpuManager>, strategy: DistributionStrategy) -> Self {
763 Self {
764 manager,
765 strategy,
766 affinity: GpuAffinityManager::new(),
767 }
768 }
769
770 pub fn set_affinity_group(&mut self, group_id: usize, devices: Vec<usize>) {
772 self.affinity.set_affinity_group(group_id, devices);
773 }
774
775 pub fn distribute_work<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
777 match self.strategy {
778 DistributionStrategy::RoundRobin => self.round_robin(items),
779 DistributionStrategy::LoadBalanced => self.load_balanced(items),
780 DistributionStrategy::DataLocal => self.data_local(items),
781 DistributionStrategy::SingleDevice => self.single_device(items),
782 }
783 }
784
785 fn round_robin<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
786 let num_devices = self.manager.num_devices();
787 if num_devices == 0 {
791 return Vec::new();
792 }
793 let mut device_items: Vec<Vec<T>> = (0..num_devices).map(|_| Vec::new()).collect();
794
795 for (idx, item) in items.into_iter().enumerate() {
796 device_items[idx % num_devices].push(item);
797 }
798
799 device_items
800 .into_iter()
801 .enumerate()
802 .filter(|(_, items)| !items.is_empty())
803 .collect()
804 }
805
806 fn load_balanced<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
807 let stats = self.manager.load_stats();
808 let num_devices = self.manager.num_devices();
809 if num_devices == 0 {
812 return Vec::new();
813 }
814 let items_len = items.len();
815
816 let mut weights: Vec<f64> = (0..num_devices)
818 .map(|i| {
819 let (_, load) = stats.get(&i).unwrap_or(&(0, 0.0));
820 1.0 / (1.0 + load)
821 })
822 .collect();
823
824 let total: f64 = weights.iter().sum();
826 if total > 0.0 {
827 for w in &mut weights {
828 *w /= total;
829 }
830 }
831
832 let mut device_items: Vec<Vec<T>> = (0..num_devices).map(|_| Vec::new()).collect();
834
835 for (idx, item) in items.into_iter().enumerate() {
836 let target = (idx as f64 + 0.5) / items_len as f64;
837 let mut device = 0;
838 let mut cumulative = 0.0;
839
840 for (dev, weight) in weights.iter().enumerate() {
841 cumulative += weight;
842 if cumulative >= target {
843 device = dev;
844 break;
845 }
846 }
847
848 device_items[device].push(item);
849 }
850
851 device_items
852 .into_iter()
853 .enumerate()
854 .filter(|(_, items)| !items.is_empty())
855 .collect()
856 }
857
858 fn data_local<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
859 self.round_robin(items)
862 }
863
864 fn single_device<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
865 vec![(0, items)]
866 }
867}
868
869#[cfg(test)]
870mod tests {
871 use super::*;
872
873 #[test]
874 fn test_multi_gpu_config() {
875 let config = MultiGpuConfig::default();
876 assert_eq!(config.min_devices, 1);
877 assert_eq!(config.max_devices, 8);
878 assert!(config.auto_load_balance);
879 }
880
881 #[test]
882 fn test_load_balance_state() {
883 let mut state = LoadBalanceState::new(3);
884
885 state.add_task(0, 100.0);
886 state.add_task(1, 50.0);
887 state.add_task(2, 75.0);
888
889 assert_eq!(state.select_device(), 1);
891
892 state.complete_task(1, 50.0);
893 assert_eq!(state.select_device(), 1);
894 }
895
896 #[test]
897 fn test_affinity_manager() {
898 let mut manager = GpuAffinityManager::new();
899
900 manager.set_affinity_group(0, vec![0, 1]);
901 manager.set_affinity_group(1, vec![2, 3]);
902
903 assert!(manager.same_affinity(0, 1));
904 assert!(manager.same_affinity(2, 3));
905 assert!(!manager.same_affinity(0, 2));
906
907 let group = manager.get_affinity_group(0);
908 assert_eq!(group, vec![0, 1]);
909 }
910
911 #[test]
912 fn test_work_distributor_zero_devices_no_panic() {
913 let manager = Arc::new(MultiGpuManager::new_empty_for_testing());
917 assert_eq!(manager.num_devices(), 0);
918
919 for strategy in [
920 DistributionStrategy::RoundRobin,
921 DistributionStrategy::LoadBalanced,
922 DistributionStrategy::DataLocal,
923 ] {
924 let distributor = WorkDistributor::new(Arc::clone(&manager), strategy);
925 let work: Vec<u32> = (0..10).collect();
926 let distribution = distributor.distribute_work(work);
927 assert!(
928 distribution.is_empty(),
929 "strategy {strategy:?} must return an empty distribution with zero devices"
930 );
931 }
932 }
933
934 #[test]
935 fn test_distribution_strategy() {
936 assert_eq!(
937 DistributionStrategy::RoundRobin,
938 DistributionStrategy::RoundRobin
939 );
940 }
941
942 fn zero_device_transfer() -> InterGpuTransfer {
958 InterGpuTransfer {
959 manager: Arc::new(MultiGpuManager::new_empty_for_testing()),
960 per_device_buffers: Vec::new(),
961 }
962 }
963
964 fn n_slot_transfer(n: usize) -> InterGpuTransfer {
969 let per_device_buffers = (0..n).map(|_| Arc::new(Mutex::new(None))).collect();
970 InterGpuTransfer {
971 manager: Arc::new(MultiGpuManager::new_empty_for_testing()),
972 per_device_buffers,
973 }
974 }
975
976 #[test]
980 fn test_set_device_buffer_out_of_range_returns_error() {
981 let transfer = zero_device_transfer();
982 let result = transfer.per_device_buffers.get(0);
991 assert!(result.is_none(), "zero-device transfer must have no slots");
992 }
998
999 #[test]
1002 fn test_gather_blocking_available() {
1003 let transfer = zero_device_transfer();
1006 let result = transfer.gather_blocking(0);
1007 assert!(
1008 result.is_err(),
1009 "gather_blocking with 0 devices must error on any dst_device"
1010 );
1011 }
1012
1013 #[test]
1016 fn test_gather_no_source_devices_returns_empty() {
1017 let transfer = n_slot_transfer(1);
1021 let result = pollster::block_on(transfer.gather(0));
1022 let gathered = result.expect("gather with single-device stub must succeed");
1023 assert!(
1024 gathered.is_empty(),
1025 "no source devices → gather result must be empty"
1026 );
1027 }
1028
1029 #[test]
1031 fn test_gather_invalid_dst_device_returns_error() {
1032 let transfer = n_slot_transfer(1);
1034 let result = pollster::block_on(transfer.gather(1));
1035 assert!(
1036 result.is_err(),
1037 "dst_device=1 when num_devices=1 must be an error"
1038 );
1039 }
1040
1041 #[ignore]
1046 #[tokio::test]
1047 async fn test_gather_real_readback_returns_nonzero_bytes() {
1048 let config = MultiGpuConfig {
1051 min_devices: 2,
1052 max_devices: 8,
1053 ..MultiGpuConfig::default()
1054 };
1055 let manager = match MultiGpuManager::new(config).await {
1056 Ok(m) => Arc::new(m),
1057 Err(_) => {
1058 return;
1060 }
1061 };
1062
1063 let transfer = InterGpuTransfer::new(Arc::clone(&manager));
1064
1065 let src_ctx = manager
1068 .device(1)
1069 .expect("device 1 must exist when num_devices >= 2");
1070
1071 const PAYLOAD: &[u8] = b"OxiGDAL-gather-test-payload-12345678";
1072 let payload_size = PAYLOAD.len() as u64;
1073 let aligned = ((payload_size + 3) / 4) * 4;
1075
1076 let src_buffer = Arc::new(src_ctx.device().create_buffer(&wgpu::BufferDescriptor {
1077 label: Some("test_src_buffer"),
1078 size: aligned,
1079 usage: BufferUsages::COPY_SRC | BufferUsages::COPY_DST | BufferUsages::STORAGE,
1080 mapped_at_creation: false,
1081 }));
1082
1083 src_ctx.queue().write_buffer(&src_buffer, 0, PAYLOAD);
1085
1086 transfer
1088 .set_device_buffer(1, Arc::clone(&src_buffer), aligned)
1089 .expect("set_device_buffer must succeed for device 1");
1090
1091 let gathered = transfer
1093 .gather(0)
1094 .await
1095 .expect("gather must succeed when GPU is available");
1096
1097 assert_eq!(
1098 gathered.len(),
1099 1,
1100 "should have exactly 1 result (device 1 → device 0)"
1101 );
1102 assert!(!gathered[0].is_empty(), "gathered bytes must not be empty");
1103 assert_eq!(
1105 &gathered[0][..PAYLOAD.len()],
1106 PAYLOAD,
1107 "gathered payload must match the uploaded data"
1108 );
1109 }
1110}