1use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq)]
13pub enum WrappingPolicy {
14 WholeProgramWrap,
16 TransformerLayerWrap {
18 min_params: usize,
20 },
21 SizeBasedWrap {
23 min_num_params: usize,
25 },
26}
27
28#[derive(Debug, Clone)]
30pub struct FsdpConfig {
31 pub world_size: usize,
33 pub local_rank: usize,
35 pub wrapping_policy: WrappingPolicy,
37 pub cpu_offload: bool,
39 pub mixed_precision: bool,
41 pub backward_prefetch: bool,
43 pub forward_prefetch: bool,
45}
46
47impl Default for FsdpConfig {
48 fn default() -> Self {
49 Self {
50 world_size: 1,
51 local_rank: 0,
52 wrapping_policy: WrappingPolicy::WholeProgramWrap,
53 cpu_offload: false,
54 mixed_precision: false,
55 backward_prefetch: true,
56 forward_prefetch: false,
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub enum ShardingStrategy {
64 FullShard,
66 ShardGradOp,
68 NoShard,
70 HybridShard { num_model_replicas: usize },
72}
73
74pub struct FsdpMemoryAnalyzer;
78
79impl FsdpMemoryAnalyzer {
80 pub fn peak_memory(config: &FsdpConfig, total_params: usize) -> usize {
86 let param_bytes = total_params * 4; let grad_bytes = total_params * 4;
88 let opt_bytes = total_params * 8; let ws = config.world_size.max(1);
90 if ws == 1 {
91 param_bytes + grad_bytes + opt_bytes
92 } else {
93 param_bytes / ws + grad_bytes / ws + opt_bytes / ws
95 }
96 }
97
98 pub fn memory_vs_ddp_ratio(world_size: usize) -> f32 {
103 if world_size <= 1 {
104 1.0
105 } else {
106 1.0 / world_size as f32
107 }
108 }
109}
110
111#[derive(Debug, Clone)]
113pub struct FsdpUnit {
114 pub unit_id: usize,
116 pub param_names: Vec<String>,
118 pub total_params: usize,
120 pub shard_size: usize,
122 pub is_offloaded: bool,
124}
125
126impl FsdpUnit {
127 pub fn new(
129 unit_id: usize,
130 param_names: Vec<String>,
131 total_params: usize,
132 world_size: usize,
133 ) -> Self {
134 let shard_size =
135 if world_size == 0 { total_params } else { total_params.div_ceil(world_size) };
136
137 Self {
138 unit_id,
139 param_names,
140 total_params,
141 shard_size,
142 is_offloaded: false,
143 }
144 }
145
146 pub fn memory_bytes_per_rank(&self) -> usize {
150 self.shard_size * std::mem::size_of::<f64>()
151 }
152
153 pub fn local_params(&self) -> usize {
155 self.shard_size
156 }
157
158 pub fn all_gather_buffer_size(&self) -> usize {
160 self.total_params
161 }
162
163 pub fn reduce_scatter_buffer_size(&self) -> usize {
165 self.total_params
166 }
167}
168
169pub struct FsdpState {
171 config: FsdpConfig,
172 units: Vec<FsdpUnit>,
173 param_registry: HashMap<String, usize>,
175 gather_cache: HashMap<usize, Vec<f64>>,
177 shard_storage: HashMap<usize, Vec<f64>>,
179}
180
181impl FsdpState {
182 pub fn new(config: FsdpConfig) -> Self {
184 Self {
185 config,
186 units: Vec::new(),
187 param_registry: HashMap::new(),
188 gather_cache: HashMap::new(),
189 shard_storage: HashMap::new(),
190 }
191 }
192
193 pub fn wrap_unit(
197 &mut self,
198 param_names: Vec<String>,
199 param_values: HashMap<String, Vec<f64>>,
200 ) -> Result<usize, FsdpError> {
201 if param_names.is_empty() {
202 return Err(FsdpError::EmptyUnit);
203 }
204
205 for name in ¶m_names {
207 if self.param_registry.contains_key(name) {
208 return Err(FsdpError::AlreadyRegistered(name.clone()));
209 }
210 }
211
212 let unit_id = self.units.len();
213 let total_params: usize =
214 param_names.iter().filter_map(|n| param_values.get(n)).map(|v| v.len()).sum();
215
216 let unit = FsdpUnit::new(
217 unit_id,
218 param_names.clone(),
219 total_params,
220 self.config.world_size,
221 );
222
223 let mut flat_params: Vec<f64> = Vec::with_capacity(total_params);
225 for name in ¶m_names {
226 if let Some(vals) = param_values.get(name) {
227 flat_params.extend_from_slice(vals);
228 }
229 }
230
231 let shard_start = self.config.local_rank * unit.shard_size;
233 let shard_end = (shard_start + unit.shard_size).min(flat_params.len());
234 let shard: Vec<f64> = if shard_start < flat_params.len() {
235 flat_params[shard_start..shard_end].to_vec()
236 } else {
237 Vec::new()
238 };
239
240 self.shard_storage.insert(unit_id, shard);
241
242 for name in ¶m_names {
244 self.param_registry.insert(name.clone(), unit_id);
245 }
246
247 self.units.push(unit);
248 Ok(unit_id)
249 }
250
251 pub fn allgather_unit(&mut self, unit_id: usize) -> Result<Vec<f64>, FsdpError> {
256 let unit = self.units.get(unit_id).ok_or(FsdpError::UnitNotFound(unit_id))?;
257
258 let total_params = unit.total_params;
259 let world_size = self.config.world_size;
260
261 let shard = self.shard_storage.get(&unit_id).cloned().unwrap_or_default();
262
263 let mut gathered: Vec<f64> = Vec::with_capacity(total_params);
265 if world_size == 0 || shard.is_empty() {
266 } else {
268 for _ in 0..world_size {
269 gathered.extend_from_slice(&shard);
270 if gathered.len() >= total_params {
271 break;
272 }
273 }
274 gathered.truncate(total_params);
275 }
276
277 self.gather_cache.insert(unit_id, gathered.clone());
278 Ok(gathered)
279 }
280
281 pub fn discard_unit_params(&mut self, unit_id: usize) -> Result<(), FsdpError> {
283 if unit_id >= self.units.len() {
284 return Err(FsdpError::UnitNotFound(unit_id));
285 }
286 self.gather_cache.remove(&unit_id);
287 Ok(())
288 }
289
290 pub fn reduce_scatter_grads(
294 &mut self,
295 unit_id: usize,
296 grads: Vec<f64>,
297 ) -> Result<Vec<f64>, FsdpError> {
298 let unit = self.units.get(unit_id).ok_or(FsdpError::UnitNotFound(unit_id))?;
299
300 let shard_size = unit.shard_size;
301 let world_size = self.config.world_size.max(1) as f64;
302
303 let averaged: Vec<f64> = grads.iter().map(|&g| g / world_size).collect();
305
306 let rank = self.config.local_rank;
308 let start = rank * shard_size;
309 let end = (start + shard_size).min(averaged.len());
310
311 let local_grads: Vec<f64> =
312 if start < averaged.len() { averaged[start..end].to_vec() } else { Vec::new() };
313
314 Ok(local_grads)
315 }
316
317 pub fn memory_saving_ratio(&self) -> f64 {
321 self.config.world_size as f64
322 }
323
324 pub fn per_rank_memory_bytes(&self) -> usize {
326 self.units.iter().map(|u| u.memory_bytes_per_rank()).sum()
327 }
328
329 pub fn unit_count(&self) -> usize {
331 self.units.len()
332 }
333
334 pub fn total_params(&self) -> usize {
336 self.units.iter().map(|u| u.total_params).sum()
337 }
338}
339
340#[derive(Debug, thiserror::Error)]
342pub enum FsdpError {
343 #[error("Unit not found: {0}")]
344 UnitNotFound(usize),
345 #[error("Param already registered: {0}")]
346 AlreadyRegistered(String),
347 #[error("Empty unit")]
348 EmptyUnit,
349 #[error("Not gathered: unit {0}")]
350 NotGathered(usize),
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 fn make_param_values(names: &[&str], size: usize) -> HashMap<String, Vec<f64>> {
358 names
359 .iter()
360 .enumerate()
361 .map(|(i, &name)| {
362 let vals: Vec<f64> = (0..size).map(|j| (i * size + j) as f64).collect();
363 (name.to_string(), vals)
364 })
365 .collect()
366 }
367
368 #[test]
370 fn test_wrap_unit_basic() {
371 let config = FsdpConfig {
372 world_size: 4,
373 local_rank: 0,
374 ..Default::default()
375 };
376 let mut state = FsdpState::new(config);
377 let names = vec!["w1".to_string(), "b1".to_string()];
378 let values = make_param_values(&["w1", "b1"], 16);
379 let unit_id = state.wrap_unit(names, values).expect("wrap failed");
380 assert_eq!(unit_id, 0);
381 assert_eq!(state.unit_count(), 1);
382 }
383
384 #[test]
386 fn test_unit_shard_size() {
387 let world_size = 4;
388 let config = FsdpConfig {
389 world_size,
390 local_rank: 0,
391 ..Default::default()
392 };
393 let mut state = FsdpState::new(config);
394 let names = vec!["weight".to_string()];
395 let total = 100usize;
396 let values: HashMap<String, Vec<f64>> = {
397 let mut m = HashMap::new();
398 m.insert("weight".to_string(), vec![1.0f64; total]);
399 m
400 };
401 let unit_id = state.wrap_unit(names, values).expect("wrap failed");
402 let unit = &state.units[unit_id];
403 let expected_shard = total.div_ceil(world_size); assert_eq!(unit.shard_size, expected_shard);
405 assert_eq!(unit.total_params, total);
406 }
407
408 #[test]
410 fn test_allgather_unit_reconstruction() {
411 let config = FsdpConfig {
412 world_size: 2,
413 local_rank: 0,
414 ..Default::default()
415 };
416 let mut state = FsdpState::new(config);
417 let names = vec!["p".to_string()];
418 let values: HashMap<String, Vec<f64>> = {
419 let mut m = HashMap::new();
420 m.insert("p".to_string(), vec![1.0, 2.0, 3.0, 4.0]);
421 m
422 };
423 let unit_id = state.wrap_unit(names, values).expect("wrap failed");
424 let gathered = state.allgather_unit(unit_id).expect("gather failed");
425 assert_eq!(gathered.len(), 4);
427 }
428
429 #[test]
431 fn test_discard_clears_cache() {
432 let config = FsdpConfig {
433 world_size: 2,
434 local_rank: 0,
435 ..Default::default()
436 };
437 let mut state = FsdpState::new(config);
438 let names = vec!["q".to_string()];
439 let values: HashMap<String, Vec<f64>> = {
440 let mut m = HashMap::new();
441 m.insert("q".to_string(), vec![1.0, 2.0, 3.0, 4.0]);
442 m
443 };
444 let unit_id = state.wrap_unit(names, values).expect("wrap failed");
445 state.allgather_unit(unit_id).expect("gather failed");
446 assert!(state.gather_cache.contains_key(&unit_id));
447 state.discard_unit_params(unit_id).expect("discard failed");
448 assert!(!state.gather_cache.contains_key(&unit_id));
449 }
450
451 #[test]
453 fn test_reduce_scatter_averages() {
454 let world_size = 4;
455 let config = FsdpConfig {
456 world_size,
457 local_rank: 0,
458 ..Default::default()
459 };
460 let mut state = FsdpState::new(config);
461 let names = vec!["r".to_string()];
462 let total = 8usize;
463 let values: HashMap<String, Vec<f64>> = {
464 let mut m = HashMap::new();
465 m.insert("r".to_string(), vec![1.0f64; total]);
466 m
467 };
468 let unit_id = state.wrap_unit(names, values).expect("wrap failed");
469 let grads = vec![4.0f64; total];
471 let local_grads = state.reduce_scatter_grads(unit_id, grads).expect("scatter failed");
472 for &g in &local_grads {
474 assert!((g - 1.0).abs() < 1e-9, "Expected 1.0, got {}", g);
475 }
476 }
477
478 #[test]
480 fn test_memory_saving_ratio() {
481 let world_size = 8;
482 let config = FsdpConfig {
483 world_size,
484 local_rank: 2,
485 ..Default::default()
486 };
487 let state = FsdpState::new(config);
488 let ratio = state.memory_saving_ratio();
489 assert!((ratio - world_size as f64).abs() < 1e-9);
490 }
491
492 #[test]
494 fn test_per_rank_memory_bytes() {
495 let world_size = 4;
496 let config = FsdpConfig {
497 world_size,
498 local_rank: 0,
499 ..Default::default()
500 };
501 let mut state = FsdpState::new(config);
502 let names = vec!["w".to_string()];
503 let total = 100usize;
504 let values: HashMap<String, Vec<f64>> = {
505 let mut m = HashMap::new();
506 m.insert("w".to_string(), vec![1.0f64; total]);
507 m
508 };
509 let unit_id = state.wrap_unit(names, values).expect("wrap failed");
510 let unit = &state.units[unit_id];
511 let expected_bytes = unit.shard_size * 8; assert_eq!(state.per_rank_memory_bytes(), expected_bytes);
513 }
514
515 #[test]
517 fn test_unit_count_and_total_params() {
518 let config = FsdpConfig {
519 world_size: 2,
520 local_rank: 0,
521 ..Default::default()
522 };
523 let mut state = FsdpState::new(config);
524 assert_eq!(state.unit_count(), 0);
525 assert_eq!(state.total_params(), 0);
526
527 let names = vec!["a".to_string()];
528 let values: HashMap<String, Vec<f64>> = {
529 let mut m = HashMap::new();
530 m.insert("a".to_string(), vec![0.0f64; 50]);
531 m
532 };
533 state.wrap_unit(names, values).expect("wrap failed");
534 assert_eq!(state.unit_count(), 1);
535 assert_eq!(state.total_params(), 50);
536 }
537
538 #[test]
540 fn test_transformer_layer_wrap_policy() {
541 let policy = WrappingPolicy::TransformerLayerWrap { min_params: 1024 };
542 let config = FsdpConfig {
543 world_size: 4,
544 local_rank: 1,
545 wrapping_policy: policy.clone(),
546 ..Default::default()
547 };
548 let state = FsdpState::new(config);
549 assert_eq!(
550 state.config.wrapping_policy,
551 WrappingPolicy::TransformerLayerWrap { min_params: 1024 }
552 );
553 }
554
555 #[test]
557 fn test_fsdp_unit_memory_bytes_per_rank() {
558 let unit = FsdpUnit::new(0, vec!["w".to_string()], 1000, 4);
559 assert_eq!(unit.shard_size, 250);
561 assert_eq!(unit.memory_bytes_per_rank(), 250 * 8);
563 }
564
565 #[test]
567 fn test_double_register_error() {
568 let config = FsdpConfig {
569 world_size: 2,
570 local_rank: 0,
571 ..Default::default()
572 };
573 let mut state = FsdpState::new(config);
574
575 let names1 = vec!["shared_param".to_string()];
576 let values1: HashMap<String, Vec<f64>> = {
577 let mut m = HashMap::new();
578 m.insert("shared_param".to_string(), vec![1.0f64; 10]);
579 m
580 };
581 state.wrap_unit(names1, values1).expect("first wrap should succeed");
582
583 let names2 = vec!["shared_param".to_string()];
584 let values2: HashMap<String, Vec<f64>> = {
585 let mut m = HashMap::new();
586 m.insert("shared_param".to_string(), vec![2.0f64; 10]);
587 m
588 };
589 let result = state.wrap_unit(names2, values2);
590 assert!(matches!(result, Err(FsdpError::AlreadyRegistered(_))));
591 }
592
593 #[test]
595 fn test_unit_not_found_error() {
596 let config = FsdpConfig {
597 world_size: 2,
598 local_rank: 0,
599 ..Default::default()
600 };
601 let mut state = FsdpState::new(config);
602 let result = state.allgather_unit(99);
603 assert!(matches!(result, Err(FsdpError::UnitNotFound(99))));
604 }
605
606 #[test]
608 fn test_multi_unit_model() {
609 let config = FsdpConfig {
610 world_size: 4,
611 local_rank: 0,
612 ..Default::default()
613 };
614 let mut state = FsdpState::new(config);
615
616 let layers = [
617 (vec!["l0.w".to_string(), "l0.b".to_string()], 128, 4),
618 (vec!["l1.w".to_string(), "l1.b".to_string()], 256, 8),
619 (vec!["l2.w".to_string(), "l2.b".to_string()], 512, 16),
620 ];
621
622 let mut total_expected = 0usize;
623 for (names, w_size, b_size) in &layers {
624 let mut values: HashMap<String, Vec<f64>> = HashMap::new();
625 values.insert(names[0].clone(), vec![1.0f64; *w_size]);
626 values.insert(names[1].clone(), vec![0.0f64; *b_size]);
627 total_expected += w_size + b_size;
628 state.wrap_unit(names.clone(), values).expect("wrap failed");
629 }
630
631 assert_eq!(state.unit_count(), 3);
632 assert_eq!(state.total_params(), total_expected);
633 }
634
635 #[test]
637 fn test_cpu_offload_flag() {
638 let config = FsdpConfig {
639 world_size: 4,
640 local_rank: 1,
641 cpu_offload: true,
642 mixed_precision: true,
643 ..Default::default()
644 };
645 assert!(config.cpu_offload);
646 assert!(config.mixed_precision);
647
648 let state = FsdpState::new(config);
649 assert!(state.config.cpu_offload);
650 assert!(state.config.mixed_precision);
651
652 let default_config = FsdpConfig::default();
654 assert!(!default_config.cpu_offload);
655 }
656
657 #[test]
661 fn test_sharding_strategy_full_shard() {
662 let s = ShardingStrategy::FullShard;
663 assert_eq!(s, ShardingStrategy::FullShard);
664 }
665
666 #[test]
668 fn test_sharding_strategy_no_shard() {
669 assert_ne!(ShardingStrategy::NoShard, ShardingStrategy::FullShard);
670 }
671
672 #[test]
674 fn test_sharding_strategy_shard_grad_op() {
675 let s = ShardingStrategy::ShardGradOp;
676 assert_eq!(s, ShardingStrategy::ShardGradOp);
677 assert_ne!(s, ShardingStrategy::FullShard);
678 }
679
680 #[test]
682 fn test_hybrid_shard_replicas() {
683 let s = ShardingStrategy::HybridShard {
684 num_model_replicas: 4,
685 };
686 assert_eq!(
687 s,
688 ShardingStrategy::HybridShard {
689 num_model_replicas: 4
690 }
691 );
692 assert_ne!(
693 s,
694 ShardingStrategy::HybridShard {
695 num_model_replicas: 2
696 }
697 );
698 }
699
700 #[test]
704 fn test_fsdp_unit_local_params() {
705 let unit = FsdpUnit::new(0, vec!["w".to_string()], 100, 4);
706 assert_eq!(unit.local_params(), unit.shard_size);
707 }
708
709 #[test]
711 fn test_fsdp_unit_all_gather_buffer() {
712 let unit = FsdpUnit::new(0, vec!["w".to_string()], 100, 4);
713 assert_eq!(unit.all_gather_buffer_size(), 100);
714 }
715
716 #[test]
718 fn test_fsdp_unit_reduce_scatter_buffer() {
719 let unit = FsdpUnit::new(0, vec!["w".to_string()], 64, 8);
720 assert_eq!(unit.reduce_scatter_buffer_size(), 64);
721 }
722
723 #[test]
727 fn test_memory_analyzer_peak_memory_world_size_1() {
728 let config = FsdpConfig {
729 world_size: 1,
730 local_rank: 0,
731 ..Default::default()
732 };
733 let peak = FsdpMemoryAnalyzer::peak_memory(&config, 1000);
734 assert_eq!(peak, 16_000);
736 }
737
738 #[test]
740 fn test_memory_analyzer_peak_memory_world_size_4() {
741 let config = FsdpConfig {
742 world_size: 4,
743 local_rank: 0,
744 ..Default::default()
745 };
746 let peak = FsdpMemoryAnalyzer::peak_memory(&config, 1000);
747 assert_eq!(peak, 4_000);
751 }
752
753 #[test]
755 fn test_memory_vs_ddp_ratio_world_size_1() {
756 let ratio = FsdpMemoryAnalyzer::memory_vs_ddp_ratio(1);
757 assert!((ratio - 1.0).abs() < 1e-6);
758 }
759
760 #[test]
762 fn test_memory_vs_ddp_ratio_world_size_8() {
763 let ratio = FsdpMemoryAnalyzer::memory_vs_ddp_ratio(8);
764 assert!((ratio - 0.125).abs() < 1e-6);
765 }
766
767 #[test]
769 fn test_memory_vs_ddp_ratio_world_size_0() {
770 let ratio = FsdpMemoryAnalyzer::memory_vs_ddp_ratio(0);
771 assert!((ratio - 1.0).abs() < 1e-6);
772 }
773
774 #[test]
778 fn test_fsdp_state_rank_gets_correct_shard() {
779 let config = FsdpConfig {
780 world_size: 4,
781 local_rank: 2,
782 ..Default::default()
783 };
784 let mut state = FsdpState::new(config);
785 let names = vec!["params".to_string()];
786 let vals: Vec<f64> = (0..100).map(|i| i as f64).collect();
787 let mut values = HashMap::new();
788 values.insert("params".to_string(), vals.clone());
789 let unit_id = state.wrap_unit(names, values).expect("wrap failed");
790
791 let unit = &state.units[unit_id];
792 assert_eq!(unit.shard_size, 25);
793
794 let shard = state.shard_storage.get(&unit_id).expect("shard missing");
796 assert_eq!(shard.len(), 25);
797 assert!(
798 (shard[0] - 50.0).abs() < 1e-9,
799 "first element should be 50.0, got {}",
800 shard[0]
801 );
802 }
803
804 #[test]
806 fn test_forward_prefetch_flag() {
807 let config = FsdpConfig {
808 world_size: 4,
809 local_rank: 0,
810 forward_prefetch: true,
811 ..Default::default()
812 };
813 assert!(config.forward_prefetch);
814 let state = FsdpState::new(config);
815 assert!(state.config.forward_prefetch);
816 assert!(!FsdpConfig::default().forward_prefetch);
817 }
818
819 #[test]
821 fn test_reduce_scatter_rank1_of_4() {
822 let config = FsdpConfig {
823 world_size: 4,
824 local_rank: 1,
825 ..Default::default()
826 };
827 let mut state = FsdpState::new(config);
828 let names = vec!["r".to_string()];
829 let mut values = HashMap::new();
830 values.insert("r".to_string(), vec![1.0f64; 8]);
831 let unit_id = state.wrap_unit(names, values).expect("wrap");
832 let grads = vec![4.0f64; 8]; let local = state.reduce_scatter_grads(unit_id, grads).expect("scatter");
835 assert_eq!(local.len(), 2, "rank 1 should hold 2 elements");
836 for &g in &local {
837 assert!((g - 1.0).abs() < 1e-9, "Expected 1.0, got {g}");
838 }
839 }
840
841 #[test]
843 fn test_wrap_empty_unit_error() {
844 let config = FsdpConfig {
845 world_size: 2,
846 local_rank: 0,
847 ..Default::default()
848 };
849 let mut state = FsdpState::new(config);
850 let result = state.wrap_unit(vec![], HashMap::new());
851 assert!(matches!(result, Err(FsdpError::EmptyUnit)));
852 }
853}