Skip to main content

trueno/backends/gpu/
pool.rs

1//! Multi-GPU device pool for data-parallel workloads
2//!
3//! Provides [`GpuDevicePool`] for managing multiple GPU devices simultaneously.
4//! Designed for data-parallel training where each GPU processes a shard of the
5//! mini-batch independently.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use trueno::backends::gpu::GpuDevicePool;
11//!
12//! // Open all available GPUs
13//! let pool = GpuDevicePool::all()?;
14//! println!("Found {} GPUs", pool.len());
15//!
16//! // Or select specific GPUs by index
17//! let pool = GpuDevicePool::with_indices(&[0, 1])?;
18//! # Ok::<(), String>(())
19//! ```
20
21use super::GpuDevice;
22
23/// Pool of GPU devices for multi-GPU workloads
24///
25/// Thin wrapper over `Vec<GpuDevice>` that handles enumeration
26/// and selective device opening.
27pub struct GpuDevicePool {
28    devices: Vec<GpuDevice>,
29    indices: Vec<u32>,
30}
31
32impl GpuDevicePool {
33    /// Open all available non-CPU GPU adapters
34    ///
35    /// Filters out adapters with `Noop` backend (CPU fallback).
36    #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
37    pub fn all() -> Result<Self, String> {
38        use super::runtime;
39        runtime::block_on(Self::all_async())
40    }
41
42    /// Open all available non-CPU GPU adapters (async)
43    pub async fn all_async() -> Result<Self, String> {
44        // PMAT-778: reuse the process-global instance so the broken freedreno
45        // ICD (GB10/aarch64) is enumerated exactly once, never concurrently.
46        let instance = super::device::shared_instance();
47        // PMAT-925: exclude GLES so the broken GLES/EGL adapter (SIGABRT-in-Drop
48        // on Linux/AMD-RADV) is never instantiated (see `device::gpu_backends`).
49        let adapters = instance.enumerate_adapters(super::device::gpu_backends());
50
51        if adapters.is_empty() {
52            return Err("No GPU adapters found".to_string());
53        }
54
55        // Filter out CPU/Noop backends
56        let gpu_adapters: Vec<(usize, _)> = adapters
57            .into_iter()
58            .enumerate()
59            .filter(|(_, adapter)| adapter.get_info().backend != wgpu::Backend::Noop)
60            .collect();
61
62        if gpu_adapters.is_empty() {
63            return Err("No non-CPU GPU adapters found".to_string());
64        }
65
66        let mut devices = Vec::with_capacity(gpu_adapters.len());
67        let mut indices = Vec::with_capacity(gpu_adapters.len());
68
69        for (idx, adapter) in gpu_adapters {
70            let mut limits = wgpu::Limits::default();
71            limits.max_buffer_size = adapter.limits().max_buffer_size;
72            limits.max_storage_buffer_binding_size =
73                adapter.limits().max_storage_buffer_binding_size;
74
75            let (device, queue) = adapter
76                .request_device(&wgpu::DeviceDescriptor {
77                    label: Some(&format!("Trueno GPU Device [{}]", idx)),
78                    required_features: wgpu::Features::empty(),
79                    required_limits: limits,
80                    memory_hints: wgpu::MemoryHints::Performance,
81                    experimental_features: Default::default(),
82                    trace: Default::default(),
83                })
84                .await
85                .map_err(|e| format!("Failed to create device at index {}: {}", idx, e))?;
86
87            devices.push(GpuDevice { device, queue });
88            indices.push(idx as u32);
89        }
90
91        Ok(Self { devices, indices })
92    }
93
94    /// Open specific GPU adapters by index
95    #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
96    pub fn with_indices(adapter_indices: &[u32]) -> Result<Self, String> {
97        use super::runtime;
98        runtime::block_on(Self::with_indices_async(adapter_indices))
99    }
100
101    /// Open specific GPU adapters by index (async)
102    pub async fn with_indices_async(adapter_indices: &[u32]) -> Result<Self, String> {
103        if adapter_indices.is_empty() {
104            return Err("No adapter indices specified".to_string());
105        }
106
107        let mut devices = Vec::with_capacity(adapter_indices.len());
108        let mut indices = Vec::with_capacity(adapter_indices.len());
109
110        for &idx in adapter_indices {
111            let device = GpuDevice::new_with_adapter_index_async(idx).await?;
112            devices.push(device);
113            indices.push(idx);
114        }
115
116        Ok(Self { devices, indices })
117    }
118
119    /// Number of devices in the pool
120    #[must_use]
121    pub fn len(&self) -> usize {
122        self.devices.len()
123    }
124
125    /// Whether the pool is empty
126    #[must_use]
127    pub fn is_empty(&self) -> bool {
128        self.devices.is_empty()
129    }
130
131    /// Get a device by pool position (0-based within this pool)
132    #[must_use]
133    pub fn get(&self, pool_index: usize) -> Option<&GpuDevice> {
134        self.devices.get(pool_index)
135    }
136
137    /// Get the adapter index for a pool position
138    #[must_use]
139    pub fn adapter_index(&self, pool_index: usize) -> Option<u32> {
140        self.indices.get(pool_index).copied()
141    }
142
143    /// Iterate over (adapter_index, device) pairs
144    pub fn iter(&self) -> impl Iterator<Item = (u32, &GpuDevice)> {
145        self.indices.iter().copied().zip(self.devices.iter())
146    }
147
148    /// Consume the pool and return the devices
149    pub fn into_devices(self) -> Vec<GpuDevice> {
150        self.devices
151    }
152}
153
154#[cfg(all(test, feature = "gpu", not(target_arch = "wasm32")))]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_pool_len_matches_devices() {
160        if !GpuDevice::is_available() {
161            eprintln!("GPU not available, skipping pool test");
162            return;
163        }
164
165        let pool = GpuDevicePool::all();
166        match pool {
167            Ok(p) => {
168                assert!(!p.is_empty());
169                assert!(p.len() > 0);
170                assert!(p.get(0).is_some());
171                assert!(p.adapter_index(0).is_some());
172            }
173            Err(e) => {
174                eprintln!("Pool creation failed (expected on CPU-only): {}", e);
175            }
176        }
177    }
178}