ferrum_engine/parallel/
executor.rs1use 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#[async_trait]
15pub trait ParallelExecutor: Send + Sync {
16 fn config(&self) -> &ParallelConfig;
18
19 fn rank(&self) -> usize;
21
22 fn world_size(&self) -> usize;
24
25 fn is_master(&self) -> bool {
27 self.rank() == 0
28 }
29
30 fn device(&self) -> Device;
32
33 async fn barrier(&self) -> Result<()>;
35
36 async fn all_reduce(&self, data: &mut [f32]) -> Result<()>;
38
39 async fn all_gather(&self, local_data: &[f32]) -> Result<Vec<f32>>;
41
42 async fn broadcast(&self, data: &mut [f32]) -> Result<()>;
44}
45
46pub 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 Ok(())
82 }
83
84 async fn all_reduce(&self, _data: &mut [f32]) -> Result<()> {
85 Ok(())
87 }
88
89 async fn all_gather(&self, local_data: &[f32]) -> Result<Vec<f32>> {
90 Ok(local_data.to_vec())
92 }
93
94 async fn broadcast(&self, _data: &mut [f32]) -> Result<()> {
95 Ok(())
97 }
98}
99
100pub struct SimulatedParallelExecutor {
105 config: ParallelConfig,
106 rank: usize,
107 shared_buffers: Arc<parking_lot::RwLock<Vec<Vec<f32>>>>,
109 barrier: Arc<std::sync::Barrier>,
111}
112
113impl SimulatedParallelExecutor {
114 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 {
160 let mut buffers = self.shared_buffers.write();
161 buffers[self.rank] = data.to_vec();
162 }
163
164 self.barrier.wait();
166
167 {
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 self.barrier.wait();
183 Ok(())
184 }
185
186 async fn all_gather(&self, local_data: &[f32]) -> Result<Vec<f32>> {
187 {
189 let mut buffers = self.shared_buffers.write();
190 buffers[self.rank] = local_data.to_vec();
191 }
192
193 self.barrier.wait();
195
196 let result = {
198 let buffers = self.shared_buffers.read();
199 buffers.iter().flatten().copied().collect()
200 };
201
202 self.barrier.wait();
204 Ok(result)
205 }
206
207 async fn broadcast(&self, data: &mut [f32]) -> Result<()> {
208 if self.rank == 0 {
209 let mut buffers = self.shared_buffers.write();
211 buffers[0] = data.to_vec();
212 }
213
214 self.barrier.wait();
216
217 if self.rank != 0 {
219 let buffers = self.shared_buffers.read();
220 data.copy_from_slice(&buffers[0]);
221 }
222
223 self.barrier.wait();
225 Ok(())
226 }
227}
228
229pub struct ParallelExecutorFactory;
231
232impl ParallelExecutorFactory {
233 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 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 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 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
291pub struct ParallelStrategySelector;
293
294impl ParallelStrategySelector {
295 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 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 let devices: Vec<_> = gpu_devices.iter().map(|d| d.device.clone()).collect();
317 let num_gpus = devices.len();
318
319 let total_memory: usize = gpu_devices.iter().map(|d| d.capability.total_memory).sum();
321
322 if total_memory >= model_size_bytes {
323 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 if num_gpus >= 2 {
332 debug!("Using pipeline parallelism with {} stages", num_gpus);
333 return ParallelConfig::pipeline_parallel(devices);
334 }
335
336 ParallelConfig::single_gpu(first_gpu.device.clone())
338 }
339}
340
341#[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 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]); 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 let config = ParallelStrategySelector::select(
382 1024 * 1024 * 100, 32,
384 &manager,
385 );
386
387 assert_eq!(config.world_size(), 1);
389 }
390}