Skip to main content

ferrum_engine/parallel/
executor.rs

1//! Parallel Executor
2//!
3//! Provides execution coordination for multi-GPU inference.
4
5use async_trait::async_trait;
6use ferrum_types::{Device, Result};
7use std::sync::Arc;
8use tracing::{debug, info};
9
10use super::config::{ParallelConfig, ParallelismType};
11use super::device::DeviceManager;
12
13/// Parallel executor trait
14#[async_trait]
15pub trait ParallelExecutor: Send + Sync {
16    /// Get the parallelism configuration
17    fn config(&self) -> &ParallelConfig;
18
19    /// Get local rank
20    fn rank(&self) -> usize;
21
22    /// Get world size
23    fn world_size(&self) -> usize;
24
25    /// Check if this is the master rank
26    fn is_master(&self) -> bool {
27        self.rank() == 0
28    }
29
30    /// Get the local device
31    fn device(&self) -> Device;
32
33    /// Barrier synchronization across all ranks
34    async fn barrier(&self) -> Result<()>;
35
36    /// All-reduce operation (sum by default)
37    async fn all_reduce(&self, data: &mut [f32]) -> Result<()>;
38
39    /// All-gather operation
40    async fn all_gather(&self, local_data: &[f32]) -> Result<Vec<f32>>;
41
42    /// Broadcast from master to all ranks
43    async fn broadcast(&self, data: &mut [f32]) -> Result<()>;
44}
45
46/// Single-GPU executor (no parallelism)
47pub struct SingleGpuExecutor {
48    config: ParallelConfig,
49    device: Device,
50}
51
52impl SingleGpuExecutor {
53    pub fn new(device: Device) -> Self {
54        Self {
55            config: ParallelConfig::single_gpu(device.clone()),
56            device,
57        }
58    }
59}
60
61#[async_trait]
62impl ParallelExecutor for SingleGpuExecutor {
63    fn config(&self) -> &ParallelConfig {
64        &self.config
65    }
66
67    fn rank(&self) -> usize {
68        0
69    }
70
71    fn world_size(&self) -> usize {
72        1
73    }
74
75    fn device(&self) -> Device {
76        self.device.clone()
77    }
78
79    async fn barrier(&self) -> Result<()> {
80        // No-op for single GPU
81        Ok(())
82    }
83
84    async fn all_reduce(&self, _data: &mut [f32]) -> Result<()> {
85        // No-op for single GPU
86        Ok(())
87    }
88
89    async fn all_gather(&self, local_data: &[f32]) -> Result<Vec<f32>> {
90        // Just return copy of local data
91        Ok(local_data.to_vec())
92    }
93
94    async fn broadcast(&self, _data: &mut [f32]) -> Result<()> {
95        // No-op for single GPU
96        Ok(())
97    }
98}
99
100/// Simulated multi-GPU executor for development/testing
101///
102/// Uses threading to simulate multiple GPUs within a single process.
103/// Useful for testing parallel algorithms without actual multi-GPU hardware.
104pub struct SimulatedParallelExecutor {
105    config: ParallelConfig,
106    rank: usize,
107    /// Shared buffers for simulated communication
108    shared_buffers: Arc<parking_lot::RwLock<Vec<Vec<f32>>>>,
109    /// Barrier for synchronization
110    barrier: Arc<std::sync::Barrier>,
111}
112
113impl SimulatedParallelExecutor {
114    /// Create a simulated parallel executor
115    pub fn new(
116        config: ParallelConfig,
117        rank: usize,
118        shared_buffers: Arc<parking_lot::RwLock<Vec<Vec<f32>>>>,
119        barrier: Arc<std::sync::Barrier>,
120    ) -> Self {
121        Self {
122            config,
123            rank,
124            shared_buffers,
125            barrier,
126        }
127    }
128}
129
130#[async_trait]
131impl ParallelExecutor for SimulatedParallelExecutor {
132    fn config(&self) -> &ParallelConfig {
133        &self.config
134    }
135
136    fn rank(&self) -> usize {
137        self.rank
138    }
139
140    fn world_size(&self) -> usize {
141        self.config.world_size()
142    }
143
144    fn device(&self) -> Device {
145        self.config
146            .devices
147            .get(self.rank)
148            .cloned()
149            .unwrap_or(Device::CPU)
150    }
151
152    async fn barrier(&self) -> Result<()> {
153        self.barrier.wait();
154        Ok(())
155    }
156
157    async fn all_reduce(&self, data: &mut [f32]) -> Result<()> {
158        // Store local data
159        {
160            let mut buffers = self.shared_buffers.write();
161            buffers[self.rank] = data.to_vec();
162        }
163
164        // Wait for all ranks
165        self.barrier.wait();
166
167        // Compute sum
168        {
169            let buffers = self.shared_buffers.read();
170            for i in 0..data.len() {
171                let mut sum = 0.0f32;
172                for rank_data in buffers.iter() {
173                    if i < rank_data.len() {
174                        sum += rank_data[i];
175                    }
176                }
177                data[i] = sum;
178            }
179        }
180
181        // Sync after reduction
182        self.barrier.wait();
183        Ok(())
184    }
185
186    async fn all_gather(&self, local_data: &[f32]) -> Result<Vec<f32>> {
187        // Store local data
188        {
189            let mut buffers = self.shared_buffers.write();
190            buffers[self.rank] = local_data.to_vec();
191        }
192
193        // Wait for all ranks
194        self.barrier.wait();
195
196        // Gather all data
197        let result = {
198            let buffers = self.shared_buffers.read();
199            buffers.iter().flatten().copied().collect()
200        };
201
202        // Sync after gather
203        self.barrier.wait();
204        Ok(result)
205    }
206
207    async fn broadcast(&self, data: &mut [f32]) -> Result<()> {
208        if self.rank == 0 {
209            // Master stores data
210            let mut buffers = self.shared_buffers.write();
211            buffers[0] = data.to_vec();
212        }
213
214        // Wait for master
215        self.barrier.wait();
216
217        // Non-master ranks copy data
218        if self.rank != 0 {
219            let buffers = self.shared_buffers.read();
220            data.copy_from_slice(&buffers[0]);
221        }
222
223        // Sync after broadcast
224        self.barrier.wait();
225        Ok(())
226    }
227}
228
229/// Factory for creating parallel executors
230pub struct ParallelExecutorFactory;
231
232impl ParallelExecutorFactory {
233    /// Create executor based on configuration
234    pub fn create(config: &ParallelConfig, rank: usize) -> Result<Box<dyn ParallelExecutor>> {
235        match config.parallelism_type {
236            ParallelismType::None => {
237                let device = config.devices.first().cloned().unwrap_or(Device::CPU);
238                Ok(Box::new(SingleGpuExecutor::new(device)))
239            }
240            ParallelismType::Tensor
241            | ParallelismType::Pipeline
242            | ParallelismType::Data
243            | ParallelismType::Hybrid => {
244                // For now, return a simulated executor
245                // In production, this would create actual distributed executors
246                info!("Creating simulated parallel executor (rank {})", rank);
247                let world_size = config.world_size();
248                let shared_buffers =
249                    Arc::new(parking_lot::RwLock::new(vec![Vec::new(); world_size]));
250                let barrier = Arc::new(std::sync::Barrier::new(world_size));
251
252                Ok(Box::new(SimulatedParallelExecutor::new(
253                    config.clone(),
254                    rank,
255                    shared_buffers,
256                    barrier,
257                )))
258            }
259        }
260    }
261
262    /// Create executors for all ranks
263    pub fn create_all(config: &ParallelConfig) -> Result<Vec<Box<dyn ParallelExecutor>>> {
264        let world_size = config.world_size();
265        let mut executors = Vec::with_capacity(world_size);
266
267        // Create shared state for simulated executors
268        let shared_buffers = Arc::new(parking_lot::RwLock::new(vec![Vec::new(); world_size]));
269        let barrier = Arc::new(std::sync::Barrier::new(world_size));
270
271        for rank in 0..world_size {
272            let executor: Box<dyn ParallelExecutor> = match config.parallelism_type {
273                ParallelismType::None => {
274                    let device = config.devices.first().cloned().unwrap_or(Device::CPU);
275                    Box::new(SingleGpuExecutor::new(device))
276                }
277                _ => Box::new(SimulatedParallelExecutor::new(
278                    config.clone(),
279                    rank,
280                    Arc::clone(&shared_buffers),
281                    Arc::clone(&barrier),
282                )),
283            };
284            executors.push(executor);
285        }
286
287        Ok(executors)
288    }
289}
290
291/// Helper to select parallelism strategy based on model and hardware
292pub struct ParallelStrategySelector;
293
294impl ParallelStrategySelector {
295    /// Select optimal parallelism strategy
296    pub fn select(
297        model_size_bytes: usize,
298        _num_layers: usize,
299        device_manager: &DeviceManager,
300    ) -> ParallelConfig {
301        let gpu_devices = device_manager.get_gpu_devices();
302
303        if gpu_devices.is_empty() {
304            debug!("No GPUs available, using CPU");
305            return ParallelConfig::single_gpu(Device::CPU);
306        }
307
308        // Check if model fits on a single GPU
309        let first_gpu = &gpu_devices[0];
310        if first_gpu.capability.can_fit_model(model_size_bytes) {
311            debug!("Model fits on single GPU");
312            return ParallelConfig::single_gpu(first_gpu.device.clone());
313        }
314
315        // Need multi-GPU parallelism
316        let devices: Vec<_> = gpu_devices.iter().map(|d| d.device.clone()).collect();
317        let num_gpus = devices.len();
318
319        // Calculate total GPU memory
320        let total_memory: usize = gpu_devices.iter().map(|d| d.capability.total_memory).sum();
321
322        if total_memory >= model_size_bytes {
323            // Tensor parallelism if model can be sharded across available GPUs
324            if (2..=8).contains(&num_gpus) {
325                debug!("Using tensor parallelism with {} GPUs", num_gpus);
326                return ParallelConfig::tensor_parallel(devices);
327            }
328        }
329
330        // Pipeline parallelism for many GPUs or very large models
331        if num_gpus >= 2 {
332            debug!("Using pipeline parallelism with {} stages", num_gpus);
333            return ParallelConfig::pipeline_parallel(devices);
334        }
335
336        // Fallback to single GPU with memory optimizations
337        ParallelConfig::single_gpu(first_gpu.device.clone())
338    }
339}
340
341// ============================================================================
342// Tests
343// ============================================================================
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    #[tokio::test]
350    async fn test_single_gpu_executor() {
351        let executor = SingleGpuExecutor::new(Device::CPU);
352
353        assert_eq!(executor.rank(), 0);
354        assert_eq!(executor.world_size(), 1);
355        assert!(executor.is_master());
356
357        // Operations should be no-ops
358        executor.barrier().await.unwrap();
359
360        let mut data = vec![1.0, 2.0, 3.0];
361        executor.all_reduce(&mut data).await.unwrap();
362        assert_eq!(data, vec![1.0, 2.0, 3.0]); // Unchanged
363
364        let gathered = executor.all_gather(&[1.0, 2.0]).await.unwrap();
365        assert_eq!(gathered, vec![1.0, 2.0]);
366    }
367
368    #[test]
369    fn test_parallel_executor_factory() {
370        let config = ParallelConfig::single_gpu(Device::CPU);
371        let executor = ParallelExecutorFactory::create(&config, 0).unwrap();
372        assert_eq!(executor.world_size(), 1);
373    }
374
375    #[test]
376    fn test_strategy_selector_single_gpu() {
377        let manager = DeviceManager::new();
378        manager.discover_devices().unwrap();
379
380        // Small model should use single GPU
381        let config = ParallelStrategySelector::select(
382            1024 * 1024 * 100, // 100MB model
383            32,
384            &manager,
385        );
386
387        // On a machine with GPUs, might get GPU; otherwise CPU
388        assert_eq!(config.world_size(), 1);
389    }
390}