1pub mod zero_optimizer;
13pub mod zero_stage1;
14pub mod zero_stage2;
15pub mod zero_stage3;
16pub mod zero_stage3_overlap;
17pub mod zero_utils;
18
19pub use zero_optimizer::{ZeROConfig, ZeROOptimizer, ZeROStage};
20pub use zero_stage1::ZeROStage1;
21pub use zero_stage2::ZeROStage2;
22pub use zero_stage3::ZeROStage3;
23pub use zero_utils::{
24 all_gather_gradients, gather_parameters, partition_gradients, partition_parameters,
25 reduce_scatter_gradients, GradientBuffer, ParameterGroup, ParameterPartition, ZeROState,
26};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ZeROImplementationStage {
31 Stage1,
33 Stage2,
35 Stage3,
37}
38
39#[derive(Debug, Clone)]
41pub struct ZeROMemoryStats {
42 pub optimizer_memory_saved: usize,
44 pub gradient_memory_saved: usize,
46 pub parameter_memory_saved: usize,
48 pub total_memory_saved: usize,
50 pub communication_overhead: usize,
52}
53
54impl Default for ZeROMemoryStats {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl ZeROMemoryStats {
61 pub fn new() -> Self {
62 Self {
63 optimizer_memory_saved: 0,
64 gradient_memory_saved: 0,
65 parameter_memory_saved: 0,
66 total_memory_saved: 0,
67 communication_overhead: 0,
68 }
69 }
70
71 pub fn update_totals(&mut self) {
72 self.total_memory_saved =
73 self.optimizer_memory_saved + self.gradient_memory_saved + self.parameter_memory_saved;
74 }
75}
76
77pub fn partition_optimizer_state(state: &[Vec<f32>], world_size: usize) -> Vec<Vec<Vec<f32>>> {
85 assert!(world_size > 0, "world_size must be > 0");
86 let mut result: Vec<Vec<Vec<f32>>> = vec![Vec::new(); world_size];
87 for param_state in state {
88 let total = param_state.len();
89 let chunk_size = total.div_ceil(world_size);
90 for rank in 0..world_size {
91 let start = rank * chunk_size;
92 let end = (start + chunk_size).min(total);
93 let shard = if start < total { param_state[start..end].to_vec() } else { Vec::new() };
94 result[rank].push(shard);
95 }
96 }
97 result
98}
99
100pub fn partition_gradients_flat(grads: &[Vec<f32>], world_size: usize) -> Vec<Vec<Vec<f32>>> {
104 assert!(world_size > 0, "world_size must be > 0");
105 let mut result: Vec<Vec<Vec<f32>>> = vec![Vec::new(); world_size];
106 for grad in grads {
107 let total = grad.len();
108 let chunk_size = total.div_ceil(world_size);
109 for rank in 0..world_size {
110 let start = rank * chunk_size;
111 let end = (start + chunk_size).min(total);
112 let shard = if start < total { grad[start..end].to_vec() } else { Vec::new() };
113 result[rank].push(shard);
114 }
115 }
116 result
117}
118
119pub fn partition_parameters_flat(params: &[Vec<f32>], world_size: usize) -> Vec<Vec<Vec<f32>>> {
123 assert!(world_size > 0, "world_size must be > 0");
124 let mut result: Vec<Vec<Vec<f32>>> = vec![Vec::new(); world_size];
125 for param in params {
126 let total = param.len();
127 let chunk_size = total.div_ceil(world_size);
128 for rank in 0..world_size {
129 let start = rank * chunk_size;
130 let end = (start + chunk_size).min(total);
131 let shard = if start < total { param[start..end].to_vec() } else { Vec::new() };
132 result[rank].push(shard);
133 }
134 }
135 result
136}
137
138pub fn gather_parameters_flat(partitioned: &[Vec<Vec<f32>>]) -> Vec<Vec<f32>> {
143 if partitioned.is_empty() {
144 return Vec::new();
145 }
146 let num_params = partitioned[0].len();
147 let mut result: Vec<Vec<f32>> = vec![Vec::new(); num_params];
148 for rank_data in partitioned {
149 for (param_idx, shard) in rank_data.iter().enumerate() {
150 if param_idx < result.len() {
151 result[param_idx].extend_from_slice(shard);
152 }
153 }
154 }
155 result
156}
157
158pub fn zero_stage_memory_reduction(
165 stage: u8,
166 world_size: usize,
167 param_bytes: usize,
168 grad_bytes: usize,
169 opt_bytes: usize,
170) -> f32 {
171 if world_size <= 1 {
172 return 0.0;
173 }
174 let total_bytes = (param_bytes + grad_bytes + opt_bytes) as f32;
175 if total_bytes == 0.0 {
176 return 0.0;
177 }
178 let ws = world_size as f32;
179 let save_fraction = (ws - 1.0) / ws;
180 let saved_bytes = match stage {
181 1 => opt_bytes as f32 * save_fraction,
182 2 => (opt_bytes + grad_bytes) as f32 * save_fraction,
183 3 => (param_bytes + grad_bytes + opt_bytes) as f32 * save_fraction,
184 _ => 0.0,
185 };
186 saved_bytes / total_bytes
187}
188
189#[derive(Debug, Clone)]
193pub struct ZeroConfig {
194 pub stage: u8,
196 pub world_size: usize,
198 pub overlap_comm: bool,
200 pub reduce_bucket_size: usize,
202}
203
204impl Default for ZeroConfig {
205 fn default() -> Self {
206 Self {
207 stage: 1,
208 world_size: 1,
209 overlap_comm: true,
210 reduce_bucket_size: 500_000_000,
211 }
212 }
213}
214
215impl ZeroConfig {
216 pub fn validate(&self) -> Result<(), String> {
222 if self.stage == 0 || self.stage > 3 {
223 return Err(format!("ZeRO stage must be 1, 2, or 3; got {}", self.stage));
224 }
225 if self.world_size == 0 {
226 return Err("world_size must be >= 1".to_string());
227 }
228 Ok(())
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn make_params(n: usize) -> Vec<f32> {
239 (0..n).map(|i| i as f32).collect()
240 }
241
242 #[test]
245 fn test_partition_optimizer_state_basic() {
246 let state = vec![make_params(8)];
248 let partitioned = partition_optimizer_state(&state, 4);
249 assert_eq!(partitioned.len(), 4);
250 for rank in 0..4 {
251 assert_eq!(
252 partitioned[rank][0].len(),
253 2,
254 "rank {rank} should have 2 elements"
255 );
256 }
257 assert_eq!(partitioned[0][0], vec![0.0, 1.0]);
259 assert_eq!(partitioned[1][0], vec![2.0, 3.0]);
260 assert_eq!(partitioned[2][0], vec![4.0, 5.0]);
261 assert_eq!(partitioned[3][0], vec![6.0, 7.0]);
262 }
263
264 #[test]
265 fn test_partition_optimizer_state_uneven() {
266 let state = vec![make_params(7)];
268 let partitioned = partition_optimizer_state(&state, 3);
269 assert_eq!(partitioned.len(), 3);
270 assert_eq!(partitioned[0][0].len(), 3);
271 assert_eq!(partitioned[1][0].len(), 3);
272 assert_eq!(partitioned[2][0].len(), 1);
273 let total: usize = partitioned.iter().map(|r| r[0].len()).sum();
275 assert_eq!(total, 7);
276 }
277
278 #[test]
279 fn test_partition_optimizer_state_multiple_states() {
280 let state = vec![make_params(4), make_params(6), make_params(2)];
282 let partitioned = partition_optimizer_state(&state, 2);
283 assert_eq!(partitioned.len(), 2);
284 for rank_data in &partitioned {
285 assert_eq!(rank_data.len(), 3, "each rank should have 3 param states");
286 }
287 }
288
289 #[test]
290 fn test_partition_optimizer_state_rank_sizes_sum_to_original() {
291 let state = vec![make_params(10), make_params(7)];
292 let partitioned = partition_optimizer_state(&state, 4);
293 for param_idx in 0..2 {
294 let total: usize = partitioned.iter().map(|r| r[param_idx].len()).sum();
295 assert_eq!(total, state[param_idx].len());
296 }
297 }
298
299 #[test]
302 fn test_partition_gradients_basic() {
303 let grads = vec![make_params(16)];
304 let partitioned = partition_gradients_flat(&grads, 4);
305 assert_eq!(partitioned.len(), 4);
306 for rank in 0..4 {
307 assert_eq!(partitioned[rank][0].len(), 4);
308 }
309 }
310
311 #[test]
312 fn test_partition_gradients_multi() {
313 let grads = vec![make_params(8), make_params(4)];
314 let partitioned = partition_gradients_flat(&grads, 2);
315 assert_eq!(partitioned[0][0].len(), 4);
317 assert_eq!(partitioned[0][1].len(), 2);
318 }
319
320 #[test]
321 fn test_partition_gradients_size_check() {
322 let grads = vec![make_params(9), make_params(5)];
323 let partitioned = partition_gradients_flat(&grads, 3);
324 for (param_idx, original) in grads.iter().enumerate() {
325 let total: usize = partitioned.iter().map(|r| r[param_idx].len()).sum();
326 assert_eq!(total, original.len());
327 }
328 }
329
330 #[test]
333 fn test_partition_parameters_basic() {
334 let params = vec![make_params(12)];
335 let partitioned = partition_parameters_flat(¶ms, 4);
336 assert_eq!(partitioned.len(), 4);
337 for rank in 0..4 {
338 assert_eq!(partitioned[rank][0].len(), 3);
339 }
340 }
341
342 #[test]
343 fn test_partition_parameters_no_duplicate() {
344 let params = vec![make_params(20)];
346 let partitioned = partition_parameters_flat(¶ms, 4);
347 let total: usize = partitioned.iter().map(|r| r[0].len()).sum();
348 assert_eq!(total, 20);
349 }
350
351 #[test]
352 fn test_partition_parameters_world_size_1() {
353 let params = vec![make_params(10)];
354 let partitioned = partition_parameters_flat(¶ms, 1);
355 assert_eq!(partitioned.len(), 1);
356 assert_eq!(partitioned[0][0], make_params(10));
357 }
358
359 #[test]
362 fn test_gather_is_inverse_of_partition() {
363 let original = vec![make_params(12), make_params(8)];
364 let partitioned = partition_parameters_flat(&original, 4);
365 let gathered = gather_parameters_flat(&partitioned);
366 assert_eq!(gathered.len(), original.len());
367 for (idx, orig) in original.iter().enumerate() {
368 assert_eq!(&gathered[idx], orig, "param {idx} mismatch after gather");
369 }
370 }
371
372 #[test]
373 fn test_gather_inverse_uneven() {
374 let original = vec![make_params(7), make_params(11)];
375 let partitioned = partition_parameters_flat(&original, 3);
376 let gathered = gather_parameters_flat(&partitioned);
377 for (idx, orig) in original.iter().enumerate() {
378 assert_eq!(&gathered[idx], orig);
379 }
380 }
381
382 #[test]
383 fn test_gather_empty() {
384 let gathered = gather_parameters_flat(&[]);
385 assert!(gathered.is_empty());
386 }
387
388 #[test]
391 fn test_stage1_memory_reduction() {
392 let ratio = zero_stage_memory_reduction(1, 4, 1000, 1000, 1000);
395 let expected = (1000.0f32 * 0.75) / 3000.0;
397 assert!(
398 (ratio - expected).abs() < 1e-5,
399 "got {ratio}, expected {expected}"
400 );
401 }
402
403 #[test]
404 fn test_stage2_memory_reduction() {
405 let ratio = zero_stage_memory_reduction(2, 4, 1000, 1000, 1000);
406 let expected = (2000.0f32 * 0.75) / 3000.0;
408 assert!(
409 (ratio - expected).abs() < 1e-5,
410 "got {ratio}, expected {expected}"
411 );
412 }
413
414 #[test]
415 fn test_stage3_memory_reduction() {
416 let ratio = zero_stage_memory_reduction(3, 4, 1000, 1000, 1000);
417 let expected = 3000.0f32 * 0.75 / 3000.0;
419 assert!(
420 (ratio - expected).abs() < 1e-5,
421 "got {ratio}, expected {expected}"
422 );
423 }
424
425 #[test]
426 fn test_memory_reduction_world_size_1() {
427 let ratio = zero_stage_memory_reduction(3, 1, 1000, 1000, 1000);
428 assert_eq!(ratio, 0.0);
429 }
430
431 #[test]
432 fn test_memory_reduction_stage3_is_greater_than_stage1() {
433 let r1 = zero_stage_memory_reduction(1, 4, 1000, 1000, 1000);
434 let r3 = zero_stage_memory_reduction(3, 4, 1000, 1000, 1000);
435 assert!(r3 > r1, "stage3 should save more than stage1");
436 }
437
438 #[test]
441 fn test_zero_config_valid() {
442 let cfg = ZeroConfig {
443 stage: 2,
444 world_size: 4,
445 ..Default::default()
446 };
447 assert!(cfg.validate().is_ok());
448 }
449
450 #[test]
451 fn test_zero_config_invalid_stage_zero() {
452 let cfg = ZeroConfig {
453 stage: 0,
454 world_size: 4,
455 ..Default::default()
456 };
457 assert!(cfg.validate().is_err());
458 }
459
460 #[test]
461 fn test_zero_config_invalid_stage_four() {
462 let cfg = ZeroConfig {
463 stage: 4,
464 world_size: 4,
465 ..Default::default()
466 };
467 assert!(cfg.validate().is_err());
468 }
469
470 #[test]
471 fn test_zero_config_invalid_world_size() {
472 let cfg = ZeroConfig {
473 stage: 1,
474 world_size: 0,
475 ..Default::default()
476 };
477 assert!(cfg.validate().is_err());
478 }
479
480 #[test]
481 fn test_zero_config_all_stages_valid() {
482 for stage in 1u8..=3 {
483 let cfg = ZeroConfig {
484 stage,
485 world_size: 8,
486 ..Default::default()
487 };
488 assert!(cfg.validate().is_ok(), "stage {stage} should be valid");
489 }
490 }
491
492 #[test]
495 fn test_zero_memory_stats_new() {
496 let stats = ZeROMemoryStats::new();
497 assert_eq!(stats.optimizer_memory_saved, 0);
498 assert_eq!(stats.gradient_memory_saved, 0);
499 assert_eq!(stats.parameter_memory_saved, 0);
500 assert_eq!(stats.total_memory_saved, 0);
501 assert_eq!(stats.communication_overhead, 0);
502 }
503
504 #[test]
505 fn test_zero_memory_stats_update_totals() {
506 let mut stats = ZeROMemoryStats::new();
507 stats.optimizer_memory_saved = 100;
508 stats.gradient_memory_saved = 200;
509 stats.parameter_memory_saved = 300;
510 stats.update_totals();
511 assert_eq!(stats.total_memory_saved, 600);
512 }
513
514 #[test]
515 fn test_partition_large_vectors() {
516 let params: Vec<Vec<f32>> =
517 (0..5).map(|p| (0..1000).map(|i| (p * 1000 + i) as f32).collect()).collect();
518 let partitioned = partition_parameters_flat(¶ms, 8);
519 assert_eq!(partitioned.len(), 8);
520 assert_eq!(partitioned[0][0].len(), 125);
522 let gathered = gather_parameters_flat(&partitioned);
524 for (idx, orig) in params.iter().enumerate() {
525 assert_eq!(&gathered[idx], orig, "param {idx} mismatch");
526 }
527 }
528}