1use serde::{Deserialize, Serialize};
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum StageOp {
44 Forward,
46 Backward,
48 Idle,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
54pub struct ScheduleStep {
55 pub stage: usize,
57 pub time: usize,
59 pub op: StageOp,
61 pub microbatch: usize,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub enum ScheduleKind {
68 GPipe,
70 OneForwardOneBackward,
72}
73
74#[derive(Debug, Clone)]
76pub struct Schedule {
77 num_stages: usize,
78 num_microbatches: usize,
79 per_stage: Vec<Vec<ScheduleStep>>,
81}
82
83impl Schedule {
84 pub fn num_stages(&self) -> usize {
86 self.num_stages
87 }
88
89 pub fn num_microbatches(&self) -> usize {
91 self.num_microbatches
92 }
93
94 pub fn steps(&self, stage: usize) -> &[ScheduleStep] {
96 &self.per_stage[stage]
97 }
98
99 pub fn total_steps(&self) -> usize {
101 self.per_stage.first().map_or(0, |s| s.len())
102 }
103
104 pub fn bubble_steps(&self) -> usize {
106 self.per_stage
107 .iter()
108 .flat_map(|s| s.iter())
109 .filter(|s| s.op == StageOp::Idle)
110 .count()
111 }
112
113 pub fn bubble_ratio(&self) -> f64 {
115 let total = self.per_stage.iter().map(|s| s.len()).sum::<usize>();
116 if total == 0 {
117 return 0.0;
118 }
119 self.bubble_steps() as f64 / total as f64
120 }
121
122 pub fn at_time(&self, time: usize) -> Vec<&ScheduleStep> {
124 self.per_stage.iter().filter_map(|s| s.get(time)).collect()
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct PipelineConfig {
131 num_stages: usize,
133 num_microbatches: usize,
135}
136
137impl PipelineConfig {
138 pub fn new(num_stages: usize, num_microbatches: usize) -> Self {
144 assert!(num_stages > 0, "num_stages must be > 0");
145 assert!(num_microbatches > 0, "num_microbatches must be > 0");
146 Self {
147 num_stages,
148 num_microbatches,
149 }
150 }
151
152 pub fn num_stages(&self) -> usize {
154 self.num_stages
155 }
156
157 pub fn num_microbatches(&self) -> usize {
159 self.num_microbatches
160 }
161
162 pub fn build_schedule(&self, kind: ScheduleKind) -> Schedule {
164 match kind {
165 ScheduleKind::GPipe => self.build_gpipe(),
166 ScheduleKind::OneForwardOneBackward => self.build_1f1b(),
167 }
168 }
169
170 fn build_gpipe(&self) -> Schedule {
175 let s = self.num_stages;
176 let m = self.num_microbatches;
177 let phase_len = s + m - 1;
178 let total = 2 * phase_len;
179
180 let mut per_stage: Vec<Vec<ScheduleStep>> = Vec::with_capacity(s);
181 for stage in 0..s {
182 let mut steps = Vec::with_capacity(total);
183 for t in 0..total {
184 let step = if t < phase_len {
185 let mb = t as isize - stage as isize;
187 if mb >= 0 && (mb as usize) < m {
188 ScheduleStep {
189 stage,
190 time: t,
191 op: StageOp::Forward,
192 microbatch: mb as usize,
193 }
194 } else {
195 ScheduleStep {
196 stage,
197 time: t,
198 op: StageOp::Idle,
199 microbatch: 0,
200 }
201 }
202 } else {
203 let bt = t - phase_len;
205 let mb = bt as isize - (s - 1 - stage) as isize;
206 if mb >= 0 && (mb as usize) < m {
207 let actual_mb = m - 1 - mb as usize;
208 ScheduleStep {
209 stage,
210 time: t,
211 op: StageOp::Backward,
212 microbatch: actual_mb,
213 }
214 } else {
215 ScheduleStep {
216 stage,
217 time: t,
218 op: StageOp::Idle,
219 microbatch: 0,
220 }
221 }
222 };
223 steps.push(step);
224 }
225 per_stage.push(steps);
226 }
227
228 Schedule {
229 num_stages: s,
230 num_microbatches: m,
231 per_stage,
232 }
233 }
234
235 fn build_1f1b(&self) -> Schedule {
238 let s = self.num_stages;
239 let m = self.num_microbatches;
240
241 let mut per_stage: Vec<Vec<ScheduleStep>> = vec![Vec::new(); s];
246
247 for (stage, steps) in per_stage.iter_mut().enumerate() {
250 let warmup = s - 1 - stage;
251 let mut fwd_idx: usize = 0;
252 let mut bwd_idx: usize = 0;
253 let mut time: usize = stage; fn pad_idle(steps: &mut Vec<ScheduleStep>, stage: usize, time: usize) {
257 while steps.len() < time {
258 let t = steps.len();
259 steps.push(ScheduleStep {
260 stage,
261 time: t,
262 op: StageOp::Idle,
263 microbatch: 0,
264 });
265 }
266 }
267
268 for _ in 0..warmup.min(m) {
270 pad_idle(steps, stage, time);
271 steps.push(ScheduleStep {
272 stage,
273 time,
274 op: StageOp::Forward,
275 microbatch: fwd_idx,
276 });
277 fwd_idx += 1;
278 time += 1;
279 }
280
281 while fwd_idx < m {
283 pad_idle(steps, stage, time);
285 steps.push(ScheduleStep {
286 stage,
287 time,
288 op: StageOp::Forward,
289 microbatch: fwd_idx,
290 });
291 fwd_idx += 1;
292 time += 1;
293
294 if bwd_idx < m {
296 pad_idle(steps, stage, time);
297 steps.push(ScheduleStep {
298 stage,
299 time,
300 op: StageOp::Backward,
301 microbatch: bwd_idx,
302 });
303 bwd_idx += 1;
304 time += 1;
305 }
306 }
307
308 while bwd_idx < m {
310 pad_idle(steps, stage, time);
311 steps.push(ScheduleStep {
312 stage,
313 time,
314 op: StageOp::Backward,
315 microbatch: bwd_idx,
316 });
317 bwd_idx += 1;
318 time += 1;
319 }
320 }
321
322 let max_len = per_stage.iter().map(|s| s.len()).max().unwrap_or(0);
324 for (stage, steps) in per_stage.iter_mut().enumerate() {
325 while steps.len() < max_len {
326 let t = steps.len();
327 steps.push(ScheduleStep {
328 stage,
329 time: t,
330 op: StageOp::Idle,
331 microbatch: 0,
332 });
333 }
334 }
335
336 Schedule {
337 num_stages: s,
338 num_microbatches: m,
339 per_stage,
340 }
341 }
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct StageAssignment {
347 pub assignments: Vec<usize>,
349 pub num_stages: usize,
351}
352
353impl StageAssignment {
354 pub fn uniform(num_layers: usize, num_stages: usize) -> Self {
356 assert!(
357 num_stages > 0 && num_layers >= num_stages,
358 "need at least as many layers as stages"
359 );
360 let per_stage = num_layers / num_stages;
361 let remainder = num_layers % num_stages;
362 let mut assignments = Vec::with_capacity(num_layers);
363 let mut layer = 0;
364 for stage in 0..num_stages {
365 let count = per_stage + if stage < remainder { 1 } else { 0 };
366 for _ in 0..count {
367 assignments.push(stage);
368 layer += 1;
369 }
370 }
371 let _ = layer;
372 Self {
373 assignments,
374 num_stages,
375 }
376 }
377
378 pub fn stage_of(&self, layer: usize) -> Option<usize> {
380 self.assignments.get(layer).copied()
381 }
382
383 pub fn layers_in_stage(&self, stage: usize) -> Vec<usize> {
385 self.assignments
386 .iter()
387 .enumerate()
388 .filter(|(_, s)| **s == stage)
389 .map(|(i, _)| i)
390 .collect()
391 }
392
393 pub fn layers_per_stage(&self) -> Vec<usize> {
395 let mut counts = vec![0usize; self.num_stages];
396 for &s in &self.assignments {
397 counts[s] += 1;
398 }
399 counts
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406
407 #[test]
408 fn gpipe_schedule_basic() {
409 let config = PipelineConfig::new(2, 4);
410 let sched = config.build_schedule(ScheduleKind::GPipe);
411
412 assert_eq!(sched.num_stages(), 2);
413 assert_eq!(sched.num_microbatches(), 4);
414
415 let first = &sched.steps(0)[0];
417 assert_eq!(first.op, StageOp::Forward);
418 assert_eq!(first.microbatch, 0);
419 }
420
421 #[test]
422 fn gpipe_all_microbatches_processed() {
423 let config = PipelineConfig::new(3, 5);
424 let sched = config.build_schedule(ScheduleKind::GPipe);
425
426 for stage in 0..3 {
427 let fwd_mbs: Vec<usize> = sched
428 .steps(stage)
429 .iter()
430 .filter(|s| s.op == StageOp::Forward)
431 .map(|s| s.microbatch)
432 .collect();
433 assert_eq!(
434 fwd_mbs.len(),
435 5,
436 "stage {stage} should have 5 forward steps"
437 );
438 let mut sorted = fwd_mbs.clone();
440 sorted.sort();
441 assert_eq!(sorted, vec![0, 1, 2, 3, 4]);
442 }
443 }
444
445 #[test]
446 fn one_f1b_schedule_basic() {
447 let config = PipelineConfig::new(4, 8);
448 let sched = config.build_schedule(ScheduleKind::OneForwardOneBackward);
449
450 assert_eq!(sched.num_stages(), 4);
451 assert_eq!(sched.num_microbatches(), 8);
452
453 let first_non_idle = sched
455 .steps(0)
456 .iter()
457 .find(|s| s.op != StageOp::Idle)
458 .unwrap();
459 assert_eq!(first_non_idle.op, StageOp::Forward);
460 assert_eq!(first_non_idle.microbatch, 0);
461 }
462
463 #[test]
464 fn one_f1b_all_microbatches_processed() {
465 let config = PipelineConfig::new(3, 6);
466 let sched = config.build_schedule(ScheduleKind::OneForwardOneBackward);
467
468 for stage in 0..3 {
469 let fwd_mbs: Vec<usize> = sched
470 .steps(stage)
471 .iter()
472 .filter(|s| s.op == StageOp::Forward)
473 .map(|s| s.microbatch)
474 .collect();
475 let bwd_mbs: Vec<usize> = sched
476 .steps(stage)
477 .iter()
478 .filter(|s| s.op == StageOp::Backward)
479 .map(|s| s.microbatch)
480 .collect();
481 assert_eq!(fwd_mbs.len(), 6, "stage {stage} forward count");
482 assert_eq!(bwd_mbs.len(), 6, "stage {stage} backward count");
483 }
484 }
485
486 #[test]
487 fn one_f1b_less_bubble_than_gpipe() {
488 let config = PipelineConfig::new(4, 16);
489 let gpipe = config.build_schedule(ScheduleKind::GPipe);
490 let f1b = config.build_schedule(ScheduleKind::OneForwardOneBackward);
491
492 assert!(
494 f1b.bubble_ratio() <= gpipe.bubble_ratio(),
495 "1F1B bubble={:.2}% should be ≤ GPipe bubble={:.2}%",
496 f1b.bubble_ratio() * 100.0,
497 gpipe.bubble_ratio() * 100.0
498 );
499 }
500
501 #[test]
502 fn bubble_ratio_range() {
503 let config = PipelineConfig::new(2, 4);
504 let sched = config.build_schedule(ScheduleKind::GPipe);
505 let ratio = sched.bubble_ratio();
506 assert!((0.0..=1.0).contains(&ratio));
507 }
508
509 #[test]
510 fn at_time_query() {
511 let config = PipelineConfig::new(2, 3);
512 let sched = config.build_schedule(ScheduleKind::GPipe);
513 let steps = sched.at_time(0);
514 assert_eq!(steps.len(), 2); }
516
517 #[test]
518 fn uniform_stage_assignment() {
519 let assign = StageAssignment::uniform(12, 4);
520 assert_eq!(assign.layers_per_stage(), vec![3, 3, 3, 3]);
521 assert_eq!(assign.stage_of(0), Some(0));
522 assert_eq!(assign.stage_of(3), Some(1));
523 assert_eq!(assign.stage_of(11), Some(3));
524 }
525
526 #[test]
527 fn uniform_stage_with_remainder() {
528 let assign = StageAssignment::uniform(10, 3);
529 assert_eq!(assign.layers_per_stage(), vec![4, 3, 3]);
531 }
532
533 #[test]
534 fn layers_in_stage() {
535 let assign = StageAssignment::uniform(6, 2);
536 assert_eq!(assign.layers_in_stage(0), vec![0, 1, 2]);
537 assert_eq!(assign.layers_in_stage(1), vec![3, 4, 5]);
538 }
539
540 #[test]
541 #[should_panic]
542 fn zero_stages_panics() {
543 PipelineConfig::new(0, 4);
544 }
545
546 #[test]
547 #[should_panic]
548 fn zero_microbatches_panics() {
549 PipelineConfig::new(4, 0);
550 }
551
552 #[test]
553 fn single_stage_no_bubble() {
554 let config = PipelineConfig::new(1, 4);
555 let sched = config.build_schedule(ScheduleKind::GPipe);
556 let bubble = sched.bubble_steps();
558 assert_eq!(bubble, 0);
559 }
560}