1use std::collections::HashMap;
4use trustformers_core::errors::{Result, TrustformersError};
5use trustformers_core::parallel::ModelParallelContext;
6use trustformers_core::tensor::Tensor;
7
8#[derive(Debug, Clone)]
10pub struct ZeROState {
11 pub step: usize,
13 pub optimizer_states: HashMap<String, HashMap<String, Tensor>>,
15 pub gradient_partitions: HashMap<String, GradientBuffer>,
17 pub parameter_partitions: HashMap<String, ParameterPartition>,
19 pub communication_buffers: HashMap<String, Tensor>,
21}
22
23impl Default for ZeROState {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl ZeROState {
30 pub fn new() -> Self {
31 Self {
32 step: 0,
33 optimizer_states: HashMap::new(),
34 gradient_partitions: HashMap::new(),
35 parameter_partitions: HashMap::new(),
36 communication_buffers: HashMap::new(),
37 }
38 }
39
40 pub fn zero_grad(&mut self) {
42 for buffer in self.gradient_partitions.values_mut() {
43 buffer.zero();
44 }
45 }
46
47 pub fn step(&mut self) {
49 self.step += 1;
50 }
51
52 pub fn memory_usage(&self) -> HashMap<String, usize> {
54 let mut stats = HashMap::new();
55
56 let mut optimizer_memory = 0;
58 for states in self.optimizer_states.values() {
59 for tensor in states.values() {
60 optimizer_memory += tensor.memory_usage();
61 }
62 }
63 stats.insert("optimizer_states".to_string(), optimizer_memory);
64
65 let mut gradient_memory = 0;
67 for buffer in self.gradient_partitions.values() {
68 gradient_memory += buffer.memory_usage();
69 }
70 stats.insert("gradient_partitions".to_string(), gradient_memory);
71
72 let mut parameter_memory = 0;
74 for partition in self.parameter_partitions.values() {
75 parameter_memory += partition.memory_usage();
76 }
77 stats.insert("parameter_partitions".to_string(), parameter_memory);
78
79 let mut comm_memory = 0;
81 for tensor in self.communication_buffers.values() {
82 comm_memory += tensor.memory_usage();
83 }
84 stats.insert("communication_buffers".to_string(), comm_memory);
85
86 stats
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct ParameterGroup {
93 pub name: String,
95 pub parameter_names: Vec<String>,
97 pub local_parameters: HashMap<String, Tensor>,
99 pub partition_info: PartitionInfo,
101}
102
103impl ParameterGroup {
104 pub fn new(name: String, parameter_names: Vec<String>) -> Self {
105 Self {
106 name,
107 parameter_names,
108 local_parameters: HashMap::new(),
109 partition_info: PartitionInfo::default(),
110 }
111 }
112
113 pub fn add_parameter(&mut self, name: String, tensor: Tensor) {
115 self.local_parameters.insert(name.clone(), tensor);
116 if !self.parameter_names.contains(&name) {
117 self.parameter_names.push(name);
118 }
119 }
120
121 pub fn memory_usage(&self) -> usize {
123 self.local_parameters.values().map(|t| t.memory_usage()).sum()
124 }
125}
126
127#[derive(Debug, Clone)]
129pub struct PartitionInfo {
130 pub rank: usize,
132 pub world_size: usize,
134 pub start_idx: usize,
136 pub end_idx: usize,
138 pub global_shape: Vec<usize>,
140 pub local_shape: Vec<usize>,
142}
143
144impl Default for PartitionInfo {
145 fn default() -> Self {
146 Self {
147 rank: 0,
148 world_size: 1,
149 start_idx: 0,
150 end_idx: 0,
151 global_shape: vec![],
152 local_shape: vec![],
153 }
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct ParameterPartition {
160 pub name: String,
162 pub local_shard: Tensor,
164 pub partition_info: PartitionInfo,
166 pub is_gathered: bool,
168 pub full_parameter: Option<Tensor>,
170}
171
172impl ParameterPartition {
173 pub fn new(name: String, local_shard: Tensor, partition_info: PartitionInfo) -> Self {
174 Self {
175 name,
176 local_shard,
177 partition_info,
178 is_gathered: false,
179 full_parameter: None,
180 }
181 }
182
183 pub fn memory_usage(&self) -> usize {
185 let mut usage = self.local_shard.memory_usage();
186 if let Some(full_param) = &self.full_parameter {
187 usage += full_param.memory_usage();
188 }
189 usage
190 }
191
192 pub fn gather(&mut self, mp_context: &ModelParallelContext) -> Result<()> {
194 if self.is_gathered {
195 return Ok(());
196 }
197
198 let full_param =
200 mp_context.all_gather(&trustformers_core::parallel::DistributedTensor::new(
201 self.local_shard.clone(),
202 self.partition_info.global_shape.clone(),
203 trustformers_core::parallel::TensorPartition {
204 split_dim: 0, start_idx: self.partition_info.start_idx,
206 end_idx: self.partition_info.end_idx,
207 num_partitions: self.partition_info.world_size,
208 partition_rank: self.partition_info.rank,
209 },
210 self.partition_info.rank,
211 ))?;
212
213 self.full_parameter = Some(full_param);
214 self.is_gathered = true;
215 Ok(())
216 }
217
218 pub fn release(&mut self) {
220 self.full_parameter = None;
221 self.is_gathered = false;
222 }
223}
224
225#[derive(Debug, Clone)]
227pub struct GradientBuffer {
228 pub name: String,
230 pub local_gradient: Tensor,
232 pub accumulated_gradient: Option<Tensor>,
234 pub accumulation_steps: usize,
236 pub partition_info: PartitionInfo,
238}
239
240impl GradientBuffer {
241 pub fn new(name: String, local_gradient: Tensor, partition_info: PartitionInfo) -> Self {
242 Self {
243 name,
244 local_gradient,
245 accumulated_gradient: None,
246 accumulation_steps: 0,
247 partition_info,
248 }
249 }
250
251 pub fn zero(&mut self) {
253 if let Ok(zeros) = Tensor::zeros(&self.local_gradient.shape()) {
257 self.local_gradient = zeros;
258 }
259 self.accumulated_gradient = None;
260 self.accumulation_steps = 0;
261 }
262
263 pub fn accumulate(&mut self, gradient: &Tensor) -> Result<()> {
265 if let Some(acc_grad) = &mut self.accumulated_gradient {
266 *acc_grad = acc_grad.add(gradient)?;
267 } else {
268 self.accumulated_gradient = Some(gradient.clone());
269 }
270 self.accumulation_steps += 1;
271 Ok(())
272 }
273
274 pub fn get_accumulated(&self) -> Option<Tensor> {
276 if let Some(acc_grad) = &self.accumulated_gradient {
277 if self.accumulation_steps > 1 {
278 acc_grad.scalar_div(self.accumulation_steps as f32).ok()
279 } else {
280 Some(acc_grad.clone())
281 }
282 } else {
283 None
284 }
285 }
286
287 pub fn memory_usage(&self) -> usize {
289 let mut usage = self.local_gradient.memory_usage();
290 if let Some(acc_grad) = &self.accumulated_gradient {
291 usage += acc_grad.memory_usage();
292 }
293 usage
294 }
295}
296
297pub fn partition_parameters(
299 parameters: &HashMap<String, Tensor>,
300 world_size: usize,
301 rank: usize,
302) -> Result<HashMap<String, ParameterPartition>> {
303 let mut partitions = HashMap::new();
304
305 for (name, param) in parameters {
306 let shape = param.shape();
307 let (start_idx, end_idx) = shard_range(shape.iter().product::<usize>(), world_size, rank)?;
308 let local_shard = slice_flat(param, start_idx, end_idx)?;
309
310 let partition_info = PartitionInfo {
311 rank,
312 world_size,
313 start_idx,
314 end_idx,
315 global_shape: shape.to_vec(),
316 local_shape: local_shard.shape().to_vec(),
317 };
318
319 let partition = ParameterPartition::new(name.clone(), local_shard, partition_info);
320 partitions.insert(name.clone(), partition);
321 }
322
323 Ok(partitions)
324}
325
326pub fn shard_range(
337 total_elements: usize,
338 world_size: usize,
339 rank: usize,
340) -> Result<(usize, usize)> {
341 if world_size == 0 {
342 return Err(TrustformersError::invalid_input(
343 "ZeRO partitioning requires world_size > 0".to_string(),
344 ));
345 }
346 if rank >= world_size {
347 return Err(TrustformersError::invalid_input(format!(
348 "rank {rank} is out of range for world_size {world_size}"
349 )));
350 }
351
352 let elements_per_rank = total_elements.div_ceil(world_size);
353 let start_idx = (rank * elements_per_rank).min(total_elements);
354 let end_idx = ((rank + 1) * elements_per_rank).min(total_elements);
355 Ok((start_idx, end_idx))
356}
357
358pub fn slice_flat(tensor: &Tensor, start: usize, end: usize) -> Result<Tensor> {
371 let data = tensor.data_f32()?;
372 if end > data.len() || start > end {
373 return Err(TrustformersError::invalid_input(format!(
374 "shard range {start}..{end} is out of bounds for a tensor of {} elements",
375 data.len()
376 )));
377 }
378 let shard = data[start..end].to_vec();
379 let len = shard.len();
380 Tensor::from_vec(shard, &[len])
381}
382
383pub fn gather_shards(shards: &[Tensor], global_shape: &[usize]) -> Result<Tensor> {
392 let mut values = Vec::new();
393 for shard in shards {
394 values.extend_from_slice(&shard.data_f32()?);
395 }
396
397 let expected: usize = global_shape.iter().product();
398 if values.len() != expected {
399 return Err(TrustformersError::invalid_input(format!(
400 "gathered {} elements but the global shape {global_shape:?} needs {expected}",
401 values.len()
402 )));
403 }
404
405 Tensor::from_vec(values, global_shape)
406}
407
408pub fn gather_parameters(
410 partitions: &mut HashMap<String, ParameterPartition>,
411 mp_context: &ModelParallelContext,
412) -> Result<HashMap<String, Tensor>> {
413 let mut gathered = HashMap::new();
414
415 for (name, partition) in partitions.iter_mut() {
416 partition.gather(mp_context)?;
417 if let Some(full_param) = &partition.full_parameter {
418 gathered.insert(name.clone(), full_param.clone());
419 }
420 }
421
422 Ok(gathered)
423}
424
425pub fn partition_gradients(
427 gradients: &HashMap<String, Tensor>,
428 world_size: usize,
429 rank: usize,
430) -> Result<HashMap<String, GradientBuffer>> {
431 let mut buffers = HashMap::new();
432
433 for (name, grad) in gradients {
434 let shape = grad.shape();
435 let (start_idx, end_idx) = shard_range(shape.iter().product::<usize>(), world_size, rank)?;
436 let local_gradient = slice_flat(grad, start_idx, end_idx)?;
437
438 let partition_info = PartitionInfo {
439 rank,
440 world_size,
441 start_idx,
442 end_idx,
443 global_shape: shape.to_vec(),
444 local_shape: local_gradient.shape().to_vec(),
445 };
446
447 let buffer = GradientBuffer::new(name.clone(), local_gradient, partition_info);
448 buffers.insert(name.clone(), buffer);
449 }
450
451 Ok(buffers)
452}
453
454pub fn all_gather_gradients(
456 buffers: &HashMap<String, GradientBuffer>,
457 mp_context: &ModelParallelContext,
458) -> Result<HashMap<String, Tensor>> {
459 let mut gathered = HashMap::new();
460
461 for (name, buffer) in buffers {
462 let distributed_tensor = trustformers_core::parallel::DistributedTensor::new(
463 buffer.local_gradient.clone(),
464 buffer.partition_info.global_shape.clone(),
465 trustformers_core::parallel::TensorPartition {
466 split_dim: 0,
467 start_idx: buffer.partition_info.start_idx,
468 end_idx: buffer.partition_info.end_idx,
469 num_partitions: buffer.partition_info.world_size,
470 partition_rank: buffer.partition_info.rank,
471 },
472 buffer.partition_info.rank,
473 );
474
475 let full_gradient = mp_context.all_gather(&distributed_tensor)?;
476 gathered.insert(name.clone(), full_gradient);
477 }
478
479 Ok(gathered)
480}
481
482pub fn reduce_scatter_gradients(
484 gradients: &HashMap<String, Tensor>,
485 mp_context: &ModelParallelContext,
486) -> Result<HashMap<String, Tensor>> {
487 let mut scattered = HashMap::new();
488
489 for (name, grad) in gradients {
490 let scattered_grad = mp_context.reduce_scatter(grad, 0)?;
491 scattered.insert(name.clone(), scattered_grad);
492 }
493
494 Ok(scattered)
495}
496
497pub fn calculate_bucket_size(
499 parameter_sizes: &[usize],
500 target_bucket_size: usize,
501) -> Vec<Vec<usize>> {
502 let mut buckets = Vec::new();
503 let mut current_bucket = Vec::new();
504 let mut current_size = 0;
505
506 for (i, &size) in parameter_sizes.iter().enumerate() {
507 if current_size + size > target_bucket_size && !current_bucket.is_empty() {
508 buckets.push(current_bucket);
509 current_bucket = Vec::new();
510 current_size = 0;
511 }
512
513 current_bucket.push(i);
514 current_size += size;
515 }
516
517 if !current_bucket.is_empty() {
518 buckets.push(current_bucket);
519 }
520
521 buckets
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn test_zero_state_creation() {
530 let state = ZeROState::new();
531 assert_eq!(state.step, 0);
532 assert!(state.optimizer_states.is_empty());
533 assert!(state.gradient_partitions.is_empty());
534 assert!(state.parameter_partitions.is_empty());
535 }
536
537 #[test]
538 fn test_parameter_group() {
539 let mut group = ParameterGroup::new("test_group".to_string(), vec!["param1".to_string()]);
540 let tensor = Tensor::ones(&[2, 2]).expect("Failed to create tensor");
541 group.add_parameter("param1".to_string(), tensor);
542
543 assert_eq!(group.parameter_names.len(), 1);
544 assert_eq!(group.local_parameters.len(), 1);
545 assert!(group.memory_usage() > 0);
546 }
547
548 #[test]
549 fn test_gradient_buffer() {
550 let tensor = Tensor::ones(&[2, 2]).expect("Failed to create tensor");
551 let partition_info = PartitionInfo::default();
552 let mut buffer = GradientBuffer::new("test_grad".to_string(), tensor, partition_info);
553
554 let grad = Tensor::ones(&[2, 2]).expect("Failed to create tensor");
555 buffer.accumulate(&grad).expect("Operation failed in test");
556
557 assert_eq!(buffer.accumulation_steps, 1);
558 assert!(buffer.get_accumulated().is_some());
559 }
560
561 #[test]
562 fn test_partition_parameters() {
563 let mut params = HashMap::new();
564 params.insert(
565 "param1".to_string(),
566 Tensor::ones(&[4, 4]).expect("Failed to create tensor"),
567 );
568 params.insert(
569 "param2".to_string(),
570 Tensor::ones(&[2, 2]).expect("Failed to create tensor"),
571 );
572
573 let partitions = partition_parameters(¶ms, 2, 0).expect("Operation failed in test");
574 assert_eq!(partitions.len(), 2);
575
576 for partition in partitions.values() {
577 assert_eq!(partition.partition_info.world_size, 2);
578 assert_eq!(partition.partition_info.rank, 0);
579 }
580 }
581
582 #[test]
583 fn test_calculate_bucket_size() {
584 let sizes = vec![100, 200, 150, 300, 50];
585 let buckets = calculate_bucket_size(&sizes, 400);
586
587 assert!(!buckets.is_empty());
588
589 for bucket in &buckets {
591 let bucket_size: usize = bucket.iter().map(|&i| sizes[i]).sum();
592 assert!(bucket_size <= 400 || bucket.len() == 1); }
594 }
595
596 #[test]
599 fn shards_are_disjoint_and_cover_every_element() {
600 let mut parameters = HashMap::new();
601 parameters.insert(
602 "w".to_string(),
603 Tensor::from_vec((0..10).map(|i| i as f32).collect(), &[10]).expect("tensor"),
604 );
605
606 let world_size = 4;
607 let mut total = 0_usize;
608 let mut seen = Vec::new();
609 for rank in 0..world_size {
610 let partitions =
611 partition_parameters(¶meters, world_size, rank).expect("partition");
612 let shard = &partitions.get("w").expect("shard").local_shard;
613 let values = shard.data_f32().expect("values");
614 total += values.len();
615 seen.extend(values);
616 }
617
618 assert_eq!(total, 10, "shard lengths must sum to the element count");
619 let expected: Vec<f32> = (0..10).map(|i| i as f32).collect();
620 assert_eq!(seen, expected, "rank r must own slice r, in order");
621 }
622
623 #[test]
626 fn each_shard_is_smaller_than_the_global_tensor() {
627 let mut parameters = HashMap::new();
628 parameters.insert(
629 "w".to_string(),
630 Tensor::from_vec(vec![1.0_f32; 16], &[4, 4]).expect("tensor"),
631 );
632
633 let partitions = partition_parameters(¶meters, 4, 1).expect("partition");
634 let partition = partitions.get("w").expect("partition");
635 assert_eq!(partition.local_shard.len(), 4);
636 assert_eq!(partition.partition_info.start_idx, 4);
637 assert_eq!(partition.partition_info.end_idx, 8);
638 assert_eq!(partition.partition_info.global_shape, vec![4, 4]);
639 assert_eq!(partition.partition_info.local_shape, vec![4]);
640 }
641
642 #[test]
644 fn gather_round_trips_the_original_tensor() {
645 let original =
646 Tensor::from_vec((0..12).map(|i| i as f32 * 0.5).collect(), &[3, 4]).expect("tensor");
647 let mut parameters = HashMap::new();
648 parameters.insert("w".to_string(), original.clone());
649
650 let world_size = 5;
651 let shards: Vec<Tensor> = (0..world_size)
652 .map(|rank| {
653 partition_parameters(¶meters, world_size, rank)
654 .expect("partition")
655 .get("w")
656 .expect("shard")
657 .local_shard
658 .clone()
659 })
660 .collect();
661
662 let gathered = gather_shards(&shards, &[3, 4]).expect("gather");
663 assert_eq!(gathered.shape(), original.shape());
664 assert_eq!(
665 gathered.data_f32().expect("values"),
666 original.data_f32().expect("values")
667 );
668 }
669
670 #[test]
672 fn gradients_are_sharded_not_rescaled() {
673 let mut gradients = HashMap::new();
674 gradients.insert(
675 "g".to_string(),
676 Tensor::from_vec(vec![2.0_f32; 8], &[8]).expect("tensor"),
677 );
678
679 let buffers = partition_gradients(&gradients, 2, 0).expect("partition");
680 let local = &buffers.get("g").expect("buffer").local_gradient;
681 assert_eq!(local.len(), 4, "the shard must be half the gradient");
682 assert!(
683 local.data_f32().expect("values").iter().all(|v| (*v - 2.0).abs() < 1e-6),
684 "values must be copied verbatim, not scaled by 1/world_size"
685 );
686 }
687
688 #[test]
690 fn trailing_ranks_may_own_nothing() {
691 assert_eq!(shard_range(3, 4, 3).expect("range"), (3, 3));
692 assert_eq!(shard_range(3, 4, 0).expect("range"), (0, 1));
693 assert!(shard_range(3, 0, 0).is_err());
694 assert!(shard_range(3, 2, 2).is_err());
695 }
696}