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 })
362 .await
363 {
364 adapters.push(adapter);
365 }
366 }
367
368 adapters
369 }
370
371 async fn create_device_context(
372 adapter: Adapter,
373 index: usize,
374 ) -> GpuResult<(GpuContext, GpuDeviceInfo)> {
375 let adapter_info = adapter.get_info();
376 let backend = adapter_info.backend;
377
378 let vram_bytes = Self::estimate_vram(&adapter_info);
380
381 let config = GpuContextConfig::default().with_label(format!("GPU {}", index));
382
383 let context = GpuContext::with_config(config).await?;
384
385 let info = GpuDeviceInfo {
386 index,
387 adapter_info,
388 backend,
389 vram_bytes,
390 active: true,
391 };
392
393 Ok((context, info))
394 }
395
396 fn estimate_vram(adapter_info: &AdapterInfo) -> Option<u64> {
397 match adapter_info.device_type {
399 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,
403 }
404 }
405}
406
407pub struct InterGpuTransfer {
415 manager: Arc<MultiGpuManager>,
416 per_device_buffers: Vec<Arc<Mutex<Option<(Arc<wgpu::Buffer>, u64)>>>>,
419}
420
421impl InterGpuTransfer {
422 pub fn new(manager: Arc<MultiGpuManager>) -> Self {
424 let num_devices = manager.num_devices();
425 let per_device_buffers = (0..num_devices)
426 .map(|_| Arc::new(Mutex::new(None)))
427 .collect();
428 Self {
429 manager,
430 per_device_buffers,
431 }
432 }
433
434 pub fn set_device_buffer(
444 &self,
445 device_idx: usize,
446 buffer: Arc<wgpu::Buffer>,
447 size_bytes: u64,
448 ) -> GpuResult<()> {
449 let slot = self
450 .per_device_buffers
451 .get(device_idx)
452 .ok_or_else(|| GpuError::invalid_buffer("device_idx out of range"))?;
453 *slot
454 .lock()
455 .map_err(|_| GpuError::internal("per_device_buffers lock poisoned"))? =
456 Some((buffer, size_bytes));
457 Ok(())
458 }
459
460 pub async fn copy_buffer(
466 &self,
467 src_device: usize,
468 dst_device: usize,
469 data: &[u8],
470 ) -> GpuResult<()> {
471 let _src_ctx = self
472 .manager
473 .device(src_device)
474 .ok_or_else(|| GpuError::invalid_buffer("Invalid source device"))?;
475
476 let dst_ctx = self
477 .manager
478 .device(dst_device)
479 .ok_or_else(|| GpuError::invalid_buffer("Invalid destination device"))?;
480
481 let dst_buffer = dst_ctx.device().create_buffer(&wgpu::BufferDescriptor {
483 label: Some("Inter-GPU Transfer"),
484 size: data.len() as u64,
485 usage: BufferUsages::COPY_DST | BufferUsages::STORAGE,
486 mapped_at_creation: false,
487 });
488
489 dst_ctx.queue().write_buffer(&dst_buffer, 0, data);
491
492 debug!(
493 "Transferred {} bytes from GPU {} to GPU {}",
494 data.len(),
495 src_device,
496 dst_device
497 );
498
499 Ok(())
500 }
501
502 pub async fn broadcast(&self, data: &[u8]) -> GpuResult<()> {
508 for i in 1..self.manager.num_devices() {
509 self.copy_buffer(0, i, data).await?;
510 }
511
512 Ok(())
513 }
514
515 pub async fn gather(&self, dst_device: usize) -> GpuResult<Vec<Vec<u8>>> {
540 let num_devices = self.per_device_buffers.len();
545
546 if dst_device >= num_devices {
547 return Err(GpuError::invalid_buffer(format!(
548 "dst_device {} is out of range (num_devices = {})",
549 dst_device, num_devices
550 )));
551 }
552
553 let mut results: Vec<Vec<u8>> = Vec::with_capacity(num_devices.saturating_sub(1));
554
555 for src_idx in 0..num_devices {
556 if src_idx == dst_device {
557 continue;
558 }
559
560 let slot = self
564 .per_device_buffers
565 .get(src_idx)
566 .ok_or_else(|| GpuError::internal("per_device_buffers shorter than num_devices"))?;
567
568 let maybe_buffer_info: Option<(Arc<wgpu::Buffer>, u64)> = {
572 let guard = slot
573 .lock()
574 .map_err(|_| GpuError::internal("per_device_buffers lock poisoned"))?;
575 guard.as_ref().map(|(buf, sz)| (Arc::clone(buf), *sz))
576 };
578
579 let (src_buffer, size_bytes) = match maybe_buffer_info {
581 Some(pair) => pair,
582 None => {
583 debug!(
584 "gather: device {} has no registered buffer, returning empty slice",
585 src_idx
586 );
587 results.push(Vec::new());
588 continue;
589 }
590 };
591
592 let ctx = self
597 .manager
598 .device(src_idx)
599 .ok_or_else(|| GpuError::invalid_buffer("source device context missing"))?;
600
601 let wgpu_device = ctx.device();
606 let wgpu_queue = ctx.queue();
607
608 let staging = wgpu_device.create_buffer(&wgpu::BufferDescriptor {
609 label: Some("gather_staging"),
610 size: size_bytes,
611 usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST,
612 mapped_at_creation: false,
613 });
614
615 let mut encoder = wgpu_device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
617 label: Some("gather_copy_encoder"),
618 });
619 encoder.copy_buffer_to_buffer(&src_buffer, 0, &staging, 0, size_bytes);
620 wgpu_queue.submit(std::iter::once(encoder.finish()));
621
622 wgpu_device
626 .poll(PollType::wait_indefinitely())
627 .map_err(|e| GpuError::execution_failed(format!("device poll error: {e:?}")))?;
628
629 let (tx, rx) =
631 futures::channel::oneshot::channel::<Result<(), wgpu::BufferAsyncError>>();
632 staging.slice(..).map_async(wgpu::MapMode::Read, move |r| {
633 let _ = tx.send(r);
634 });
635
636 wgpu_device
638 .poll(PollType::wait_indefinitely())
639 .map_err(|e| {
640 GpuError::execution_failed(format!("device poll (map) error: {e:?}"))
641 })?;
642
643 rx.await
644 .map_err(|_| GpuError::buffer_mapping("gather: oneshot channel closed"))?
645 .map_err(|e| GpuError::buffer_mapping(format!("gather: map_async failed: {e}")))?;
646
647 let raw_data: Vec<u8> = staging.slice(..).get_mapped_range().to_vec();
648 staging.unmap();
649
650 debug!(
651 "gather: read {} bytes from device {} (staging buffer)",
652 raw_data.len(),
653 src_idx
654 );
655
656 results.push(raw_data);
657 }
658
659 Ok(results)
660 }
661
662 pub fn gather_blocking(&self, dst_device: usize) -> GpuResult<Vec<Vec<u8>>> {
672 pollster::block_on(self.gather(dst_device))
673 }
674}
675
676pub struct GpuAffinityManager {
678 affinity_groups: HashMap<usize, Vec<usize>>,
680}
681
682impl GpuAffinityManager {
683 pub fn new() -> Self {
685 Self {
686 affinity_groups: HashMap::new(),
687 }
688 }
689
690 pub fn set_affinity_group(&mut self, group_id: usize, devices: Vec<usize>) {
692 self.affinity_groups.insert(group_id, devices);
693 }
694
695 pub fn get_affinity_group(&self, device: usize) -> Vec<usize> {
697 for (_, devices) in &self.affinity_groups {
698 if devices.contains(&device) {
699 return devices.clone();
700 }
701 }
702 vec![device]
703 }
704
705 pub fn same_affinity(&self, device_a: usize, device_b: usize) -> bool {
707 let group_a = self.get_affinity_group(device_a);
708 group_a.contains(&device_b)
709 }
710
711 pub fn optimal_device(&self, data_device: usize, available: &[usize]) -> Option<usize> {
713 let group = self.get_affinity_group(data_device);
715
716 for device in available {
717 if group.contains(device) {
718 return Some(*device);
719 }
720 }
721
722 available.first().copied()
724 }
725}
726
727impl Default for GpuAffinityManager {
728 fn default() -> Self {
729 Self::new()
730 }
731}
732
733#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735pub enum DistributionStrategy {
736 RoundRobin,
738 LoadBalanced,
740 DataLocal,
742 SingleDevice,
744}
745
746pub struct WorkDistributor {
748 manager: Arc<MultiGpuManager>,
749 strategy: DistributionStrategy,
750 affinity: GpuAffinityManager,
751}
752
753impl WorkDistributor {
754 pub fn new(manager: Arc<MultiGpuManager>, strategy: DistributionStrategy) -> Self {
756 Self {
757 manager,
758 strategy,
759 affinity: GpuAffinityManager::new(),
760 }
761 }
762
763 pub fn set_affinity_group(&mut self, group_id: usize, devices: Vec<usize>) {
765 self.affinity.set_affinity_group(group_id, devices);
766 }
767
768 pub fn distribute_work<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
770 match self.strategy {
771 DistributionStrategy::RoundRobin => self.round_robin(items),
772 DistributionStrategy::LoadBalanced => self.load_balanced(items),
773 DistributionStrategy::DataLocal => self.data_local(items),
774 DistributionStrategy::SingleDevice => self.single_device(items),
775 }
776 }
777
778 fn round_robin<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
779 let num_devices = self.manager.num_devices();
780 let mut device_items: Vec<Vec<T>> = (0..num_devices).map(|_| Vec::new()).collect();
781
782 for (idx, item) in items.into_iter().enumerate() {
783 device_items[idx % num_devices].push(item);
784 }
785
786 device_items
787 .into_iter()
788 .enumerate()
789 .filter(|(_, items)| !items.is_empty())
790 .collect()
791 }
792
793 fn load_balanced<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
794 let stats = self.manager.load_stats();
795 let num_devices = self.manager.num_devices();
796 let items_len = items.len();
797
798 let mut weights: Vec<f64> = (0..num_devices)
800 .map(|i| {
801 let (_, load) = stats.get(&i).unwrap_or(&(0, 0.0));
802 1.0 / (1.0 + load)
803 })
804 .collect();
805
806 let total: f64 = weights.iter().sum();
808 if total > 0.0 {
809 for w in &mut weights {
810 *w /= total;
811 }
812 }
813
814 let mut device_items: Vec<Vec<T>> = (0..num_devices).map(|_| Vec::new()).collect();
816
817 for (idx, item) in items.into_iter().enumerate() {
818 let target = (idx as f64 + 0.5) / items_len as f64;
819 let mut device = 0;
820 let mut cumulative = 0.0;
821
822 for (dev, weight) in weights.iter().enumerate() {
823 cumulative += weight;
824 if cumulative >= target {
825 device = dev;
826 break;
827 }
828 }
829
830 device_items[device].push(item);
831 }
832
833 device_items
834 .into_iter()
835 .enumerate()
836 .filter(|(_, items)| !items.is_empty())
837 .collect()
838 }
839
840 fn data_local<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
841 self.round_robin(items)
844 }
845
846 fn single_device<T>(&self, items: Vec<T>) -> Vec<(usize, Vec<T>)> {
847 vec![(0, items)]
848 }
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854
855 #[test]
856 fn test_multi_gpu_config() {
857 let config = MultiGpuConfig::default();
858 assert_eq!(config.min_devices, 1);
859 assert_eq!(config.max_devices, 8);
860 assert!(config.auto_load_balance);
861 }
862
863 #[test]
864 fn test_load_balance_state() {
865 let mut state = LoadBalanceState::new(3);
866
867 state.add_task(0, 100.0);
868 state.add_task(1, 50.0);
869 state.add_task(2, 75.0);
870
871 assert_eq!(state.select_device(), 1);
873
874 state.complete_task(1, 50.0);
875 assert_eq!(state.select_device(), 1);
876 }
877
878 #[test]
879 fn test_affinity_manager() {
880 let mut manager = GpuAffinityManager::new();
881
882 manager.set_affinity_group(0, vec![0, 1]);
883 manager.set_affinity_group(1, vec![2, 3]);
884
885 assert!(manager.same_affinity(0, 1));
886 assert!(manager.same_affinity(2, 3));
887 assert!(!manager.same_affinity(0, 2));
888
889 let group = manager.get_affinity_group(0);
890 assert_eq!(group, vec![0, 1]);
891 }
892
893 #[test]
894 fn test_distribution_strategy() {
895 assert_eq!(
896 DistributionStrategy::RoundRobin,
897 DistributionStrategy::RoundRobin
898 );
899 }
900
901 fn zero_device_transfer() -> InterGpuTransfer {
917 InterGpuTransfer {
918 manager: Arc::new(MultiGpuManager::new_empty_for_testing()),
919 per_device_buffers: Vec::new(),
920 }
921 }
922
923 fn n_slot_transfer(n: usize) -> InterGpuTransfer {
928 let per_device_buffers = (0..n).map(|_| Arc::new(Mutex::new(None))).collect();
929 InterGpuTransfer {
930 manager: Arc::new(MultiGpuManager::new_empty_for_testing()),
931 per_device_buffers,
932 }
933 }
934
935 #[test]
939 fn test_set_device_buffer_out_of_range_returns_error() {
940 let transfer = zero_device_transfer();
941 let result = transfer.per_device_buffers.get(0);
950 assert!(result.is_none(), "zero-device transfer must have no slots");
951 }
957
958 #[test]
961 fn test_gather_blocking_available() {
962 let transfer = zero_device_transfer();
965 let result = transfer.gather_blocking(0);
966 assert!(
967 result.is_err(),
968 "gather_blocking with 0 devices must error on any dst_device"
969 );
970 }
971
972 #[test]
975 fn test_gather_no_source_devices_returns_empty() {
976 let transfer = n_slot_transfer(1);
980 let result = pollster::block_on(transfer.gather(0));
981 let gathered = result.expect("gather with single-device stub must succeed");
982 assert!(
983 gathered.is_empty(),
984 "no source devices → gather result must be empty"
985 );
986 }
987
988 #[test]
990 fn test_gather_invalid_dst_device_returns_error() {
991 let transfer = n_slot_transfer(1);
993 let result = pollster::block_on(transfer.gather(1));
994 assert!(
995 result.is_err(),
996 "dst_device=1 when num_devices=1 must be an error"
997 );
998 }
999
1000 #[ignore]
1005 #[tokio::test]
1006 async fn test_gather_real_readback_returns_nonzero_bytes() {
1007 let config = MultiGpuConfig {
1010 min_devices: 2,
1011 max_devices: 8,
1012 ..MultiGpuConfig::default()
1013 };
1014 let manager = match MultiGpuManager::new(config).await {
1015 Ok(m) => Arc::new(m),
1016 Err(_) => {
1017 return;
1019 }
1020 };
1021
1022 let transfer = InterGpuTransfer::new(Arc::clone(&manager));
1023
1024 let src_ctx = manager
1027 .device(1)
1028 .expect("device 1 must exist when num_devices >= 2");
1029
1030 const PAYLOAD: &[u8] = b"OxiGDAL-gather-test-payload-12345678";
1031 let payload_size = PAYLOAD.len() as u64;
1032 let aligned = ((payload_size + 3) / 4) * 4;
1034
1035 let src_buffer = Arc::new(src_ctx.device().create_buffer(&wgpu::BufferDescriptor {
1036 label: Some("test_src_buffer"),
1037 size: aligned,
1038 usage: BufferUsages::COPY_SRC | BufferUsages::COPY_DST | BufferUsages::STORAGE,
1039 mapped_at_creation: false,
1040 }));
1041
1042 src_ctx.queue().write_buffer(&src_buffer, 0, PAYLOAD);
1044
1045 transfer
1047 .set_device_buffer(1, Arc::clone(&src_buffer), aligned)
1048 .expect("set_device_buffer must succeed for device 1");
1049
1050 let gathered = transfer
1052 .gather(0)
1053 .await
1054 .expect("gather must succeed when GPU is available");
1055
1056 assert_eq!(
1057 gathered.len(),
1058 1,
1059 "should have exactly 1 result (device 1 → device 0)"
1060 );
1061 assert!(!gathered[0].is_empty(), "gathered bytes must not be empty");
1062 assert_eq!(
1064 &gathered[0][..PAYLOAD.len()],
1065 PAYLOAD,
1066 "gathered payload must match the uploaded data"
1067 );
1068 }
1069}