Skip to main content

trueno_db/gpu/
multigpu.rs

1//! Multi-GPU data partitioning and distribution
2//!
3//! Toyota Way Principles:
4//! - Heijunka (Load Leveling): Distribute work evenly across GPUs
5//! - Muda elimination: Parallel execution reduces total wall-clock time
6//!
7//! Architecture:
8//! - Detect all available GPU devices
9//! - Partition data by range (contiguous chunks) or hash (random distribution)
10//! - Execute operations in parallel across all GPUs
11//! - Reduce results from all GPUs to final answer
12//!
13//! References:
14//! - Leis et al. (2014): Morsel-driven parallelism for NUMA systems
15//! - `MapD` (2017): Multi-GPU query execution patterns
16
17use crate::{Error, Result};
18use arrow::array::Int32Array;
19use wgpu;
20
21/// Information about a single GPU device
22#[derive(Debug, Clone)]
23pub struct GpuDeviceInfo {
24    /// Device name (e.g., "NVIDIA RTX 4090", "AMD Radeon RX 7900 XTX")
25    pub name: String,
26    /// Device type (`DiscreteGpu`, `IntegratedGpu`, `VirtualGpu`, Cpu, Other)
27    pub device_type: wgpu::DeviceType,
28    /// Backend (Vulkan, Metal, DX12, DX11, GL, `BrowserWebGPU`)
29    pub backend: wgpu::Backend,
30}
31
32/// Multi-GPU device manager
33pub struct MultiGpuManager {
34    /// All available GPU devices
35    devices: Vec<GpuDeviceInfo>,
36}
37
38impl MultiGpuManager {
39    /// Detect all available GPU devices
40    ///
41    /// # Errors
42    /// Returns error if GPU enumeration fails
43    pub fn new() -> Result<Self> {
44        // PMAT-927: constrain to a non-GLES backend mask (see `super::gpu_backends`)
45        // so the broken GLES/EGL adapter (SIGABRT-in-Drop on Linux/AMD-RADV) is
46        // never registered, at BOTH the instance and the enumerate_adapters site.
47        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
48            backends: super::gpu_backends(),
49            ..Default::default()
50        });
51
52        // Enumerate all adapters (non-GLES mask).
53        let adapters = instance.enumerate_adapters(super::gpu_backends());
54
55        // Convert adapters to device info
56        let devices: Vec<GpuDeviceInfo> = adapters
57            .iter()
58            .map(|adapter| {
59                let info = adapter.get_info();
60                GpuDeviceInfo {
61                    name: info.name,
62                    device_type: info.device_type,
63                    backend: info.backend,
64                }
65            })
66            .collect();
67
68        Ok(Self { devices })
69    }
70
71    /// Get number of available GPUs
72    #[must_use]
73    pub fn device_count(&self) -> usize {
74        self.devices.len()
75    }
76
77    /// Get information about all devices
78    #[must_use]
79    pub fn devices(&self) -> &[GpuDeviceInfo] {
80        &self.devices
81    }
82}
83
84/// Data partitioning strategy
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum PartitionStrategy {
87    /// Range partitioning: divide data into contiguous chunks
88    /// Example: GPU0: [0..N/2], GPU1: [N/2..N]
89    Range,
90    /// Hash partitioning: distribute rows based on `hash(row_id)` % `num_gpus`
91    /// Better load balancing for skewed data
92    Hash,
93}
94
95/// Data partition for a single GPU
96#[derive(Debug)]
97pub struct DataPartition {
98    /// GPU device index
99    pub device_id: usize,
100    /// Data chunk for this GPU
101    pub data: Int32Array,
102}
103
104/// Partition data across multiple GPUs
105///
106/// # Arguments
107/// * `data` - Input array to partition
108/// * `num_partitions` - Number of partitions (typically number of GPUs)
109/// * `strategy` - Partitioning strategy (Range or Hash)
110///
111/// # Returns
112/// Vector of partitions, one per GPU
113///
114/// # Errors
115/// Returns error if partitioning fails
116pub fn partition_data(
117    data: &Int32Array,
118    num_partitions: usize,
119    strategy: PartitionStrategy,
120) -> Result<Vec<DataPartition>> {
121    if num_partitions == 0 {
122        return Err(Error::InvalidInput("num_partitions must be > 0".to_string()));
123    }
124
125    let partitions = match strategy {
126        PartitionStrategy::Range => partition_range(data, num_partitions),
127        PartitionStrategy::Hash => partition_hash(data, num_partitions),
128    };
129    Ok(partitions)
130}
131
132/// Partition data using range partitioning (contiguous chunks)
133fn partition_range(data: &Int32Array, num_partitions: usize) -> Vec<DataPartition> {
134    let len = data.len();
135    let mut partitions = Vec::with_capacity(num_partitions);
136
137    // Calculate chunk size (handle uneven division)
138    let base_size = len / num_partitions;
139    let remainder = len % num_partitions;
140
141    let mut offset = 0;
142    for device_id in 0..num_partitions {
143        // First 'remainder' partitions get an extra element
144        let size = if device_id < remainder { base_size + 1 } else { base_size };
145
146        // Extract slice
147        let values: Vec<i32> = (offset..offset + size).map(|i| data.value(i)).collect();
148
149        partitions.push(DataPartition { device_id, data: Int32Array::from(values) });
150
151        offset += size;
152    }
153
154    partitions
155}
156
157/// Partition data using hash partitioning (random distribution)
158fn partition_hash(data: &Int32Array, num_partitions: usize) -> Vec<DataPartition> {
159    use std::collections::hash_map::DefaultHasher;
160    use std::hash::{Hash, Hasher};
161
162    // Initialize empty vectors for each partition
163    let mut buckets: Vec<Vec<i32>> = (0..num_partitions).map(|_| Vec::new()).collect();
164
165    // Distribute elements by hash
166    for i in 0..data.len() {
167        let value = data.value(i);
168
169        // Hash the row index (not the value) for deterministic distribution
170        let mut hasher = DefaultHasher::new();
171        i.hash(&mut hasher);
172        let hash = hasher.finish();
173
174        #[allow(clippy::cast_possible_truncation)]
175        let partition_id = (hash % num_partitions as u64) as usize;
176        buckets[partition_id].push(value);
177    }
178
179    // Convert buckets to DataPartition
180    let partitions: Vec<DataPartition> = buckets
181        .into_iter()
182        .enumerate()
183        .map(|(device_id, values)| DataPartition { device_id, data: Int32Array::from(values) })
184        .collect();
185
186    partitions
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_multigpu_device_detection() {
195        // RED: This test should fail because MultiGpuManager::new() is not implemented
196        let manager = MultiGpuManager::new();
197
198        // If no GPUs available, should return Ok with 0 devices
199        // If GPUs available, should return Ok with device info
200        match manager {
201            Ok(mgr) => {
202                // Should detect at least 0 devices (graceful degradation)
203                let count = mgr.device_count();
204                println!("Detected {count} GPU device(s)");
205
206                // If devices found, validate their info
207                if count > 0 {
208                    for (i, device) in mgr.devices().iter().enumerate() {
209                        println!("GPU {i}: {device:?}");
210                        assert!(!device.name.is_empty(), "Device name should not be empty");
211                    }
212                }
213            }
214            Err(e) => {
215                panic!("MultiGpuManager::new() failed: {e}");
216            }
217        }
218    }
219
220    #[test]
221    fn test_multigpu_device_count_zero_when_no_gpu() {
222        // RED: Should fail because not implemented
223        // When no GPU available, should return 0 devices (not an error)
224        let manager = MultiGpuManager::new();
225
226        if let Ok(mgr) = manager {
227            // Valid result: 0 devices (no GPU) or N devices (GPUs found)
228            // device_count is usize, so always >= 0
229            let _count = mgr.device_count();
230        } else {
231            // Also acceptable: return error if GPU enumeration fails
232            // But prefer returning 0 devices for graceful degradation
233        }
234    }
235
236    #[test]
237    fn test_partition_range_even_split() {
238        // RED: Should fail because partition_data() is not implemented
239        let data = Int32Array::from(vec![1, 2, 3, 4, 5, 6, 7, 8]);
240        let partitions = partition_data(&data, 2, PartitionStrategy::Range).unwrap();
241
242        assert_eq!(partitions.len(), 2);
243
244        // GPU 0: [1, 2, 3, 4]
245        assert_eq!(partitions[0].device_id, 0);
246        assert_eq!(partitions[0].data.len(), 4);
247        assert_eq!(partitions[0].data.value(0), 1);
248        assert_eq!(partitions[0].data.value(3), 4);
249
250        // GPU 1: [5, 6, 7, 8]
251        assert_eq!(partitions[1].device_id, 1);
252        assert_eq!(partitions[1].data.len(), 4);
253        assert_eq!(partitions[1].data.value(0), 5);
254        assert_eq!(partitions[1].data.value(3), 8);
255    }
256
257    #[test]
258    fn test_partition_range_uneven_split() {
259        // RED: Should fail because not implemented
260        // With 10 elements and 3 GPUs: [4, 3, 3] or [3, 3, 4] distribution
261        let data = Int32Array::from(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
262        let partitions = partition_data(&data, 3, PartitionStrategy::Range).unwrap();
263
264        assert_eq!(partitions.len(), 3);
265
266        // Verify all data is partitioned (no data loss)
267        let total_len: usize = partitions.iter().map(|p| p.data.len()).sum();
268        assert_eq!(total_len, 10);
269
270        // Verify partitions are contiguous
271        assert_eq!(partitions[0].data.value(0), 1); // First partition starts at 1
272        let last_partition = &partitions[2];
273        assert_eq!(last_partition.data.value(last_partition.data.len() - 1), 10);
274        // Last partition ends at 10
275    }
276
277    #[test]
278    fn test_partition_hash_distribution() {
279        // RED: Should fail because not implemented
280        let data = Int32Array::from(vec![1, 2, 3, 4, 5, 6, 7, 8]);
281        let partitions = partition_data(&data, 2, PartitionStrategy::Hash).unwrap();
282
283        assert_eq!(partitions.len(), 2);
284
285        // Verify all data is partitioned (no data loss)
286        let total_len: usize = partitions.iter().map(|p| p.data.len()).sum();
287        assert_eq!(total_len, 8);
288
289        // Hash partitioning: elements may be in different order
290        // Just verify device IDs are correct
291        assert_eq!(partitions[0].device_id, 0);
292        assert_eq!(partitions[1].device_id, 1);
293    }
294
295    #[test]
296    fn test_partition_single_gpu() {
297        // RED: Should fail because not implemented
298        // With 1 GPU, all data goes to partition 0
299        let data = Int32Array::from(vec![1, 2, 3, 4]);
300        let partitions = partition_data(&data, 1, PartitionStrategy::Range).unwrap();
301
302        assert_eq!(partitions.len(), 1);
303        assert_eq!(partitions[0].device_id, 0);
304        assert_eq!(partitions[0].data.len(), 4);
305    }
306
307    #[test]
308    fn test_partition_empty_data() {
309        // RED: Should fail because not implemented
310        let data = Int32Array::from(vec![] as Vec<i32>);
311        let partitions = partition_data(&data, 2, PartitionStrategy::Range).unwrap();
312
313        assert_eq!(partitions.len(), 2);
314        // Both partitions should be empty
315        assert_eq!(partitions[0].data.len(), 0);
316        assert_eq!(partitions[1].data.len(), 0);
317    }
318
319    #[test]
320    fn test_partition_zero_partitions_error() {
321        // RED: Should fail because not implemented
322        // num_partitions = 0 should return error
323        let data = Int32Array::from(vec![1, 2, 3]);
324        let result = partition_data(&data, 0, PartitionStrategy::Range);
325
326        assert!(result.is_err());
327        assert!(result.unwrap_err().to_string().contains("num_partitions must be > 0"));
328    }
329}