Skip to main content

trustformers_core/parallel/
model_parallel.rs

1//! Model Parallel Support for Large Models
2//!
3//! This module provides infrastructure for distributing model layers and tensors
4//! across multiple devices (GPUs) to enable training and inference of models
5//! that are too large to fit on a single device.
6
7#![allow(unused_variables)] // Model parallelism implementation
8
9use crate::errors::{tensor_op_error, Result};
10// Only used by the non-nccl fallback path in `create_communicator`
11#[cfg(not(feature = "nccl"))]
12use crate::errors::runtime_error;
13use crate::Tensor;
14use std::sync::Arc;
15
16/// Model parallelism strategy
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ModelParallelStrategy {
19    /// Pipeline parallelism - split model by layers
20    Pipeline,
21    /// Tensor parallelism - split individual layers
22    Tensor,
23    /// Hybrid approach combining both
24    Hybrid,
25}
26
27/// Configuration for model parallel execution
28#[derive(Debug, Clone)]
29pub struct ModelParallelConfig {
30    /// Number of devices to use
31    pub num_devices: usize,
32    /// Parallelism strategy
33    pub strategy: ModelParallelStrategy,
34    /// Device IDs to use (e.g., [0, 1, 2, 3] for 4 GPUs)
35    pub device_ids: Vec<usize>,
36    /// Pipeline depth for pipeline parallelism
37    pub pipeline_depth: Option<usize>,
38    /// Tensor split dimension for tensor parallelism
39    pub tensor_split_dim: Option<usize>,
40    /// Enable gradient checkpointing to save memory
41    pub gradient_checkpointing: bool,
42    /// Communication backend
43    pub comm_backend: CommunicationBackend,
44}
45
46impl Default for ModelParallelConfig {
47    fn default() -> Self {
48        Self {
49            num_devices: 1,
50            strategy: ModelParallelStrategy::Pipeline,
51            device_ids: vec![0],
52            pipeline_depth: None,
53            tensor_split_dim: None,
54            gradient_checkpointing: false,
55            comm_backend: CommunicationBackend::Nccl,
56        }
57    }
58}
59
60/// Communication backend for model parallel
61#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
62pub enum CommunicationBackend {
63    /// NVIDIA Collective Communication Library
64    Nccl,
65    /// Message Passing Interface
66    Mpi,
67    /// Gloo (CPU communication)
68    Gloo,
69    /// Custom implementation
70    Custom,
71}
72
73/// Distributed tensor that can be split across devices
74#[derive(Debug, Clone)]
75pub struct DistributedTensor {
76    /// Local shard of the tensor
77    pub local_shard: Tensor,
78    /// Global shape of the full tensor
79    pub global_shape: Vec<usize>,
80    /// Partition info
81    pub partition: TensorPartition,
82    /// Device ID where this shard resides
83    pub device_id: usize,
84}
85
86/// Information about how a tensor is partitioned
87#[derive(Debug, Clone)]
88pub struct TensorPartition {
89    /// Dimension along which tensor is split
90    pub split_dim: usize,
91    /// Start index in the global tensor
92    pub start_idx: usize,
93    /// End index in the global tensor
94    pub end_idx: usize,
95    /// Total number of partitions
96    pub num_partitions: usize,
97    /// This partition's rank
98    pub partition_rank: usize,
99}
100
101impl DistributedTensor {
102    /// Create a new distributed tensor from a local shard
103    pub fn new(
104        local_shard: Tensor,
105        global_shape: Vec<usize>,
106        partition: TensorPartition,
107        device_id: usize,
108    ) -> Self {
109        Self {
110            local_shard,
111            global_shape,
112            partition,
113            device_id,
114        }
115    }
116
117    /// Get the local shape of this shard
118    pub fn local_shape(&self) -> Vec<usize> {
119        self.local_shard.shape()
120    }
121
122    /// Check if this tensor needs communication for operations
123    pub fn requires_communication(&self) -> bool {
124        self.partition.num_partitions > 1
125    }
126}
127
128/// Model parallel context managing distributed execution
129pub struct ModelParallelContext {
130    config: ModelParallelConfig,
131    rank: usize,
132    world_size: usize,
133    pub(crate) communicator: Arc<dyn Communicator>,
134    #[allow(dead_code)]
135    device_mesh: DeviceMesh,
136}
137
138impl ModelParallelContext {
139    /// Initialize model parallel context
140    pub fn new(config: ModelParallelConfig) -> Result<Self> {
141        let world_size = config.num_devices;
142        let rank = 0; // Will be set by init process
143
144        let communicator = create_communicator(&config.comm_backend)?;
145        let device_mesh = DeviceMesh::new(&config.device_ids, config.strategy)?;
146
147        Ok(Self {
148            config,
149            rank,
150            world_size,
151            communicator,
152            device_mesh,
153        })
154    }
155
156    /// Get current process rank
157    pub fn rank(&self) -> usize {
158        self.rank
159    }
160
161    /// Get total world size
162    pub fn world_size(&self) -> usize {
163        self.world_size
164    }
165
166    /// Partition a tensor across devices
167    pub fn partition_tensor(&self, tensor: &Tensor, split_dim: usize) -> Result<DistributedTensor> {
168        let shape = tensor.shape();
169        if split_dim >= shape.len() {
170            return Err(tensor_op_error(
171                "split_tensor",
172                format!(
173                    "Split dimension {} out of bounds for tensor with {} dimensions",
174                    split_dim,
175                    shape.len()
176                ),
177            ));
178        }
179
180        let dim_size = shape[split_dim];
181        let chunk_size = dim_size.div_ceil(self.world_size);
182        let start_idx = self.rank * chunk_size;
183        let end_idx = ((self.rank + 1) * chunk_size).min(dim_size);
184
185        // Extract local shard by slicing the tensor along the split dimension
186        let local_shard = tensor.slice(split_dim, start_idx, end_idx)?;
187
188        let partition = TensorPartition {
189            split_dim,
190            start_idx,
191            end_idx,
192            num_partitions: self.world_size,
193            partition_rank: self.rank,
194        };
195
196        Ok(DistributedTensor::new(
197            local_shard,
198            shape.to_vec(),
199            partition,
200            self.config.device_ids[self.rank],
201        ))
202    }
203
204    /// Gather distributed tensor to full tensor
205    pub fn all_gather(&self, distributed: &DistributedTensor) -> Result<Tensor> {
206        if !distributed.requires_communication() {
207            return Ok(distributed.local_shard.clone());
208        }
209
210        self.communicator
211            .all_gather(&distributed.local_shard, distributed.partition.split_dim)
212    }
213
214    /// Reduce scattered tensor across devices
215    pub fn reduce_scatter(&self, tensor: &Tensor, split_dim: usize) -> Result<Tensor> {
216        self.communicator.reduce_scatter(tensor, split_dim)
217    }
218
219    /// All-reduce operation for gradient synchronization
220    pub fn all_reduce(&self, tensor: &mut Tensor) -> Result<()> {
221        self.communicator.all_reduce(tensor)
222    }
223
224    /// Broadcast tensor from root rank to all other ranks
225    pub fn broadcast(&self, tensor: &mut Tensor, root: usize) -> Result<()> {
226        self.communicator.broadcast(tensor, root)
227    }
228}
229
230/// Device mesh for organizing devices in model parallel
231#[derive(Debug, Clone)]
232pub struct DeviceMesh {
233    /// Device IDs in the mesh
234    device_ids: Vec<usize>,
235    /// Topology of the mesh
236    topology: MeshTopology,
237}
238
239#[derive(Debug, Clone)]
240enum MeshTopology {
241    /// Linear arrangement (for pipeline parallel)
242    Linear,
243    /// 2D mesh (for tensor parallel)
244    Grid2D { rows: usize, cols: usize },
245    /// 3D mesh (for hybrid parallel)
246    #[allow(dead_code)]
247    Grid3D { x: usize, y: usize, z: usize },
248}
249
250impl DeviceMesh {
251    fn new(device_ids: &[usize], strategy: ModelParallelStrategy) -> Result<Self> {
252        let topology = match strategy {
253            ModelParallelStrategy::Pipeline => MeshTopology::Linear,
254            ModelParallelStrategy::Tensor => {
255                // For tensor parallel, try to create a balanced 2D grid
256                let n = device_ids.len();
257                let rows = (n as f64).sqrt().ceil() as usize;
258                let cols = n.div_ceil(rows);
259                MeshTopology::Grid2D { rows, cols }
260            },
261            ModelParallelStrategy::Hybrid => {
262                // For hybrid, create a 3D mesh if possible
263                // This is a simplified version
264                MeshTopology::Linear
265            },
266        };
267
268        Ok(Self {
269            device_ids: device_ids.to_vec(),
270            topology,
271        })
272    }
273
274    /// Get device ID at a given coordinate
275    pub fn device_at(&self, coord: &[usize]) -> Option<usize> {
276        match &self.topology {
277            MeshTopology::Linear => {
278                coord.first().and_then(|&idx| self.device_ids.get(idx).copied())
279            },
280            MeshTopology::Grid2D { rows, cols } => {
281                if coord.len() >= 2 {
282                    let idx = coord[0] * cols + coord[1];
283                    self.device_ids.get(idx).copied()
284                } else {
285                    None
286                }
287            },
288            MeshTopology::Grid3D { x, y, z } => {
289                if coord.len() >= 3 {
290                    let idx = coord[0] * y * z + coord[1] * z + coord[2];
291                    self.device_ids.get(idx).copied()
292                } else {
293                    None
294                }
295            },
296        }
297    }
298}
299
300/// Communication interface for model parallel operations
301pub trait Communicator: Send + Sync {
302    /// All-gather operation
303    fn all_gather(&self, tensor: &Tensor, split_dim: usize) -> Result<Tensor>;
304
305    /// Reduce-scatter operation
306    fn reduce_scatter(&self, tensor: &Tensor, split_dim: usize) -> Result<Tensor>;
307
308    /// All-reduce operation
309    fn all_reduce(&self, tensor: &mut Tensor) -> Result<()>;
310
311    /// Point-to-point send
312    fn send(&self, tensor: &Tensor, dest: usize) -> Result<()>;
313
314    /// Point-to-point receive
315    fn recv(&self, shape: &[usize], src: usize) -> Result<Tensor>;
316
317    /// Broadcast from root
318    fn broadcast(&self, tensor: &mut Tensor, root: usize) -> Result<()>;
319}
320
321/// Create appropriate communicator based on backend
322fn create_communicator(backend: &CommunicationBackend) -> Result<Arc<dyn Communicator>> {
323    match backend {
324        CommunicationBackend::Nccl => {
325            #[cfg(feature = "nccl")]
326            {
327                use super::nccl_communicator::create_nccl_communicator;
328                // Default to rank 0, world_size 1, device 0 for single-process case
329                // In a real distributed setup, these would come from environment or config
330                let rank =
331                    std::env::var("RANK").unwrap_or_else(|_| "0".to_string()).parse().unwrap_or(0);
332                let world_size = std::env::var("WORLD_SIZE")
333                    .unwrap_or_else(|_| "1".to_string())
334                    .parse()
335                    .unwrap_or(1);
336                let device_id = std::env::var("LOCAL_RANK")
337                    .unwrap_or_else(|_| "0".to_string())
338                    .parse()
339                    .unwrap_or(0);
340
341                create_nccl_communicator(rank, world_size, device_id)
342            }
343
344            #[cfg(not(feature = "nccl"))]
345            return Err(runtime_error(
346                "NCCL backend requested but not compiled with nccl feature",
347            ));
348        },
349        CommunicationBackend::Mpi => {
350            use super::mpi_communicator::MpiCommunicatorImpl;
351            Ok(Arc::new(MpiCommunicatorImpl::new()?))
352        },
353        CommunicationBackend::Gloo => {
354            // Fallback to mock for now
355            Ok(Arc::new(MockCommunicator::new()))
356        },
357        CommunicationBackend::Custom => Ok(Arc::new(MockCommunicator::new())),
358    }
359}
360
361/// Mock communicator for testing
362struct MockCommunicator;
363
364impl MockCommunicator {
365    fn new() -> Self {
366        Self
367    }
368}
369
370impl Communicator for MockCommunicator {
371    fn all_gather(&self, tensor: &Tensor, _split_dim: usize) -> Result<Tensor> {
372        // In mock mode, just return the tensor as-is
373        Ok(tensor.clone())
374    }
375
376    fn reduce_scatter(&self, tensor: &Tensor, _split_dim: usize) -> Result<Tensor> {
377        Ok(tensor.clone())
378    }
379
380    fn all_reduce(&self, _tensor: &mut Tensor) -> Result<()> {
381        Ok(())
382    }
383
384    fn send(&self, _tensor: &Tensor, _dest: usize) -> Result<()> {
385        Ok(())
386    }
387
388    fn recv(&self, shape: &[usize], _src: usize) -> Result<Tensor> {
389        Tensor::zeros(shape)
390    }
391
392    fn broadcast(&self, _tensor: &mut Tensor, _root: usize) -> Result<()> {
393        Ok(())
394    }
395}
396
397/// Pipeline parallel schedule for forward/backward passes
398#[derive(Debug, Clone)]
399pub struct PipelineSchedule {
400    /// Number of pipeline stages
401    pub num_stages: usize,
402    /// Number of microbatches
403    pub num_microbatches: usize,
404    /// Schedule type
405    pub schedule_type: PipelineScheduleType,
406}
407
408#[derive(Debug, Clone, Copy)]
409pub enum PipelineScheduleType {
410    /// Forward then backward (simple but inefficient)
411    Sequential,
412    /// 1F1B schedule (one forward, one backward)
413    OneForwardOneBackward,
414    /// Interleaved 1F1B for better efficiency
415    InterleavedOneF1B,
416}
417
418impl PipelineSchedule {
419    /// Create a new pipeline schedule
420    pub fn new(
421        num_stages: usize,
422        num_microbatches: usize,
423        schedule_type: PipelineScheduleType,
424    ) -> Self {
425        Self {
426            num_stages,
427            num_microbatches,
428            schedule_type,
429        }
430    }
431
432    /// Get the schedule for a specific stage
433    pub fn get_stage_schedule(&self, stage_id: usize) -> Vec<PipelineOp> {
434        match self.schedule_type {
435            PipelineScheduleType::Sequential => self.sequential_schedule(stage_id),
436            PipelineScheduleType::OneForwardOneBackward => self.one_f1b_schedule(stage_id),
437            PipelineScheduleType::InterleavedOneF1B => self.interleaved_1f1b_schedule(stage_id),
438        }
439    }
440
441    fn sequential_schedule(&self, stage_id: usize) -> Vec<PipelineOp> {
442        let mut ops = Vec::new();
443
444        // All forwards first
445        for mb in 0..self.num_microbatches {
446            ops.push(PipelineOp::Forward { microbatch_id: mb });
447        }
448
449        // Then all backwards
450        for mb in (0..self.num_microbatches).rev() {
451            ops.push(PipelineOp::Backward { microbatch_id: mb });
452        }
453
454        ops
455    }
456
457    fn one_f1b_schedule(&self, stage_id: usize) -> Vec<PipelineOp> {
458        let mut ops = Vec::new();
459        let num_warmup = self.num_stages - stage_id - 1;
460
461        // Warmup phase - only forward
462        for mb in 0..num_warmup.min(self.num_microbatches) {
463            ops.push(PipelineOp::Forward { microbatch_id: mb });
464        }
465
466        // Steady state - 1F1B
467        let steady_state_mbs = self.num_microbatches.saturating_sub(num_warmup);
468        for i in 0..steady_state_mbs {
469            let forward_mb = num_warmup + i;
470            let backward_mb = i;
471
472            if forward_mb < self.num_microbatches {
473                ops.push(PipelineOp::Forward {
474                    microbatch_id: forward_mb,
475                });
476            }
477            ops.push(PipelineOp::Backward {
478                microbatch_id: backward_mb,
479            });
480        }
481
482        // Cooldown phase - only backward
483        for mb in steady_state_mbs..self.num_microbatches {
484            ops.push(PipelineOp::Backward { microbatch_id: mb });
485        }
486
487        ops
488    }
489
490    fn interleaved_1f1b_schedule(&self, _stage_id: usize) -> Vec<PipelineOp> {
491        // Simplified version - can be optimized further
492        self.one_f1b_schedule(_stage_id)
493    }
494}
495
496#[derive(Debug, Clone)]
497pub enum PipelineOp {
498    Forward { microbatch_id: usize },
499    Backward { microbatch_id: usize },
500    SendActivation { to_stage: usize },
501    RecvActivation { from_stage: usize },
502    SendGradient { to_stage: usize },
503    RecvGradient { from_stage: usize },
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn test_tensor_partition() {
512        let ctx = ModelParallelContext::new(ModelParallelConfig {
513            num_devices: 4,
514            device_ids: vec![0, 1, 2, 3],
515            comm_backend: CommunicationBackend::Custom, // Use mock backend for tests
516            ..Default::default()
517        })
518        .expect("operation failed in test");
519
520        let tensor = Tensor::zeros(&[128, 512]).expect("Failed to create zero tensor");
521        let distributed = ctx.partition_tensor(&tensor, 0).expect("tensor operation failed");
522
523        // Verify partition metadata is correct
524        assert_eq!(distributed.global_shape, vec![128, 512]);
525        assert_eq!(distributed.partition.split_dim, 0);
526        assert_eq!(distributed.partition.start_idx, 0);
527        assert_eq!(distributed.partition.end_idx, 32);
528        assert_eq!(distributed.partition.num_partitions, 4);
529
530        // Check local tensor shape after slicing
531        let local_shape = distributed.local_shard.shape();
532        assert_eq!(local_shape, vec![32, 512]); // First dimension should be sliced to 32
533    }
534
535    #[test]
536    fn test_device_mesh() {
537        let mesh = DeviceMesh::new(&[0, 1, 2, 3], ModelParallelStrategy::Tensor)
538            .expect("tensor operation failed");
539
540        assert_eq!(mesh.device_at(&[0, 0]), Some(0));
541        assert_eq!(mesh.device_at(&[0, 1]), Some(1));
542        assert_eq!(mesh.device_at(&[1, 0]), Some(2));
543        assert_eq!(mesh.device_at(&[1, 1]), Some(3));
544    }
545
546    #[test]
547    fn test_pipeline_schedule() {
548        let schedule = PipelineSchedule::new(4, 8, PipelineScheduleType::OneForwardOneBackward);
549        let stage0_ops = schedule.get_stage_schedule(0);
550
551        // Stage 0 should have 3 warmup forwards
552        let forward_ops: Vec<_> = stage0_ops
553            .iter()
554            .filter(|op| matches!(op, PipelineOp::Forward { .. }))
555            .collect();
556        assert!(!forward_ops.is_empty());
557    }
558}