1#![allow(unused_variables)] use crate::errors::{tensor_op_error, Result};
10#[cfg(not(feature = "nccl"))]
12use crate::errors::runtime_error;
13use crate::Tensor;
14use std::sync::Arc;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ModelParallelStrategy {
19 Pipeline,
21 Tensor,
23 Hybrid,
25}
26
27#[derive(Debug, Clone)]
29pub struct ModelParallelConfig {
30 pub num_devices: usize,
32 pub strategy: ModelParallelStrategy,
34 pub device_ids: Vec<usize>,
36 pub pipeline_depth: Option<usize>,
38 pub tensor_split_dim: Option<usize>,
40 pub gradient_checkpointing: bool,
42 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#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
62pub enum CommunicationBackend {
63 Nccl,
65 Mpi,
67 Gloo,
69 Custom,
71}
72
73#[derive(Debug, Clone)]
75pub struct DistributedTensor {
76 pub local_shard: Tensor,
78 pub global_shape: Vec<usize>,
80 pub partition: TensorPartition,
82 pub device_id: usize,
84}
85
86#[derive(Debug, Clone)]
88pub struct TensorPartition {
89 pub split_dim: usize,
91 pub start_idx: usize,
93 pub end_idx: usize,
95 pub num_partitions: usize,
97 pub partition_rank: usize,
99}
100
101impl DistributedTensor {
102 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 pub fn local_shape(&self) -> Vec<usize> {
119 self.local_shard.shape()
120 }
121
122 pub fn requires_communication(&self) -> bool {
124 self.partition.num_partitions > 1
125 }
126}
127
128pub 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 pub fn new(config: ModelParallelConfig) -> Result<Self> {
141 let world_size = config.num_devices;
142 let rank = 0; 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 pub fn rank(&self) -> usize {
158 self.rank
159 }
160
161 pub fn world_size(&self) -> usize {
163 self.world_size
164 }
165
166 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 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 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 pub fn reduce_scatter(&self, tensor: &Tensor, split_dim: usize) -> Result<Tensor> {
216 self.communicator.reduce_scatter(tensor, split_dim)
217 }
218
219 pub fn all_reduce(&self, tensor: &mut Tensor) -> Result<()> {
221 self.communicator.all_reduce(tensor)
222 }
223
224 pub fn broadcast(&self, tensor: &mut Tensor, root: usize) -> Result<()> {
226 self.communicator.broadcast(tensor, root)
227 }
228}
229
230#[derive(Debug, Clone)]
232pub struct DeviceMesh {
233 device_ids: Vec<usize>,
235 topology: MeshTopology,
237}
238
239#[derive(Debug, Clone)]
240enum MeshTopology {
241 Linear,
243 Grid2D { rows: usize, cols: usize },
245 #[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 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 MeshTopology::Linear
265 },
266 };
267
268 Ok(Self {
269 device_ids: device_ids.to_vec(),
270 topology,
271 })
272 }
273
274 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
300pub trait Communicator: Send + Sync {
302 fn all_gather(&self, tensor: &Tensor, split_dim: usize) -> Result<Tensor>;
304
305 fn reduce_scatter(&self, tensor: &Tensor, split_dim: usize) -> Result<Tensor>;
307
308 fn all_reduce(&self, tensor: &mut Tensor) -> Result<()>;
310
311 fn send(&self, tensor: &Tensor, dest: usize) -> Result<()>;
313
314 fn recv(&self, shape: &[usize], src: usize) -> Result<Tensor>;
316
317 fn broadcast(&self, tensor: &mut Tensor, root: usize) -> Result<()>;
319}
320
321fn 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 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 Ok(Arc::new(MockCommunicator::new()))
356 },
357 CommunicationBackend::Custom => Ok(Arc::new(MockCommunicator::new())),
358 }
359}
360
361struct 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 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#[derive(Debug, Clone)]
399pub struct PipelineSchedule {
400 pub num_stages: usize,
402 pub num_microbatches: usize,
404 pub schedule_type: PipelineScheduleType,
406}
407
408#[derive(Debug, Clone, Copy)]
409pub enum PipelineScheduleType {
410 Sequential,
412 OneForwardOneBackward,
414 InterleavedOneF1B,
416}
417
418impl PipelineSchedule {
419 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 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 for mb in 0..self.num_microbatches {
446 ops.push(PipelineOp::Forward { microbatch_id: mb });
447 }
448
449 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 for mb in 0..num_warmup.min(self.num_microbatches) {
463 ops.push(PipelineOp::Forward { microbatch_id: mb });
464 }
465
466 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 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 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, ..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 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 let local_shape = distributed.local_shard.shape();
532 assert_eq!(local_shape, vec![32, 512]); }
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 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}