Skip to main content

trustformers_optim/zero/
zero_utils.rs

1//! Utility functions and data structures for ZeRO optimization
2
3use std::collections::HashMap;
4use trustformers_core::errors::Result;
5use trustformers_core::parallel::ModelParallelContext;
6use trustformers_core::tensor::Tensor;
7
8/// ZeRO optimizer state management
9#[derive(Debug, Clone)]
10pub struct ZeROState {
11    /// Current step number
12    pub step: usize,
13    /// Partitioned optimizer states per parameter group
14    pub optimizer_states: HashMap<String, HashMap<String, Tensor>>,
15    /// Partitioned gradients (for Stage 2+)
16    pub gradient_partitions: HashMap<String, GradientBuffer>,
17    /// Partitioned parameters (for Stage 3)
18    pub parameter_partitions: HashMap<String, ParameterPartition>,
19    /// Communication buffers for all-gather operations
20    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    /// Reset gradients for next iteration
41    pub fn zero_grad(&mut self) {
42        for buffer in self.gradient_partitions.values_mut() {
43            buffer.zero();
44        }
45    }
46
47    /// Increment step counter
48    pub fn step(&mut self) {
49        self.step += 1;
50    }
51
52    /// Get memory usage statistics
53    pub fn memory_usage(&self) -> HashMap<String, usize> {
54        let mut stats = HashMap::new();
55
56        // Calculate optimizer state memory
57        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        // Calculate gradient memory
66        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        // Calculate parameter memory
73        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        // Calculate communication buffer memory
80        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/// Parameter group for ZeRO optimization
91#[derive(Debug, Clone)]
92pub struct ParameterGroup {
93    /// Group name/identifier
94    pub name: String,
95    /// Parameter names in this group
96    pub parameter_names: Vec<String>,
97    /// Local partition of parameters
98    pub local_parameters: HashMap<String, Tensor>,
99    /// Metadata for parameter partitioning
100    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    /// Add a parameter to this group
114    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    /// Get total memory usage of this group
122    pub fn memory_usage(&self) -> usize {
123        self.local_parameters.values().map(|t| t.memory_usage()).sum()
124    }
125}
126
127/// Partition information for distributed parameters
128#[derive(Debug, Clone)]
129pub struct PartitionInfo {
130    /// Rank of this partition
131    pub rank: usize,
132    /// Total number of partitions
133    pub world_size: usize,
134    /// Start index in global parameter
135    pub start_idx: usize,
136    /// End index in global parameter
137    pub end_idx: usize,
138    /// Global shape of full parameter
139    pub global_shape: Vec<usize>,
140    /// Local shape of this partition
141    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/// Parameter partition for ZeRO Stage 3
158#[derive(Debug, Clone)]
159pub struct ParameterPartition {
160    /// Parameter name
161    pub name: String,
162    /// Local shard of the parameter
163    pub local_shard: Tensor,
164    /// Partition metadata
165    pub partition_info: PartitionInfo,
166    /// Whether this parameter is currently gathered
167    pub is_gathered: bool,
168    /// Full parameter (only valid when is_gathered = true)
169    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    /// Get memory usage of this partition
184    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    /// Gather full parameter from all partitions
193    pub fn gather(&mut self, mp_context: &ModelParallelContext) -> Result<()> {
194        if self.is_gathered {
195            return Ok(());
196        }
197
198        // Use model parallel context to gather the parameter
199        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, // Assume partitioning along first dimension
205                    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    /// Release gathered parameter to save memory
219    pub fn release(&mut self) {
220        self.full_parameter = None;
221        self.is_gathered = false;
222    }
223}
224
225/// Gradient buffer for ZeRO Stage 2+
226#[derive(Debug, Clone)]
227pub struct GradientBuffer {
228    /// Buffer name
229    pub name: String,
230    /// Local gradient shard
231    pub local_gradient: Tensor,
232    /// Accumulated gradients
233    pub accumulated_gradient: Option<Tensor>,
234    /// Number of accumulated steps
235    pub accumulation_steps: usize,
236    /// Partition metadata
237    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    /// Zero the gradient buffer
252    pub fn zero(&mut self) {
253        // The shape is taken from an existing valid tensor, so `zeros` cannot
254        // fail in practice; on the theoretical error path we leave the buffer
255        // unchanged rather than panicking.
256        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    /// Accumulate gradient
264    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    /// Get the accumulated gradient (averaged if needed)
275    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    /// Get memory usage of this buffer
288    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
297/// Partition parameters across devices for ZeRO Stage 3
298pub 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 total_elements = shape.iter().product::<usize>();
308
309        // Calculate partition size
310        let elements_per_rank = total_elements.div_ceil(world_size);
311        let start_idx = rank * elements_per_rank;
312        let end_idx = ((rank + 1) * elements_per_rank).min(total_elements);
313
314        // Create local shard using simplified slicing approach
315        // In a full implementation, this would use proper distributed tensor slicing
316        // For now, we create a scaled-down version to simulate partitioning
317        let local_shard = if world_size == 1 || total_elements <= elements_per_rank {
318            // If single device or small parameter, each rank gets a copy
319            param.clone()
320        } else {
321            // For demonstration, create a smaller tensor that represents the local shard
322            // This simulates the effect of partitioning without complex slicing logic
323            let scale_factor = 1.0 / (world_size as f32);
324
325            param.mul_scalar(scale_factor)?
326        };
327
328        let partition_info = PartitionInfo {
329            rank,
330            world_size,
331            start_idx,
332            end_idx,
333            global_shape: shape.to_vec(),
334            local_shape: local_shard.shape().to_vec(),
335        };
336
337        let partition = ParameterPartition::new(name.clone(), local_shard, partition_info);
338        partitions.insert(name.clone(), partition);
339    }
340
341    Ok(partitions)
342}
343
344/// Gather parameters from all devices
345pub fn gather_parameters(
346    partitions: &mut HashMap<String, ParameterPartition>,
347    mp_context: &ModelParallelContext,
348) -> Result<HashMap<String, Tensor>> {
349    let mut gathered = HashMap::new();
350
351    for (name, partition) in partitions.iter_mut() {
352        partition.gather(mp_context)?;
353        if let Some(full_param) = &partition.full_parameter {
354            gathered.insert(name.clone(), full_param.clone());
355        }
356    }
357
358    Ok(gathered)
359}
360
361/// Partition gradients across devices for ZeRO Stage 2+
362pub fn partition_gradients(
363    gradients: &HashMap<String, Tensor>,
364    world_size: usize,
365    rank: usize,
366) -> Result<HashMap<String, GradientBuffer>> {
367    let mut buffers = HashMap::new();
368
369    for (name, grad) in gradients {
370        let shape = grad.shape();
371        let total_elements = shape.iter().product::<usize>();
372
373        // Calculate partition size
374        let elements_per_rank = total_elements.div_ceil(world_size);
375        let start_idx = rank * elements_per_rank;
376        let end_idx = ((rank + 1) * elements_per_rank).min(total_elements);
377
378        // Create local gradient shard using simplified approach
379        // In a full implementation, this would use proper distributed gradient slicing
380        let local_gradient = if world_size == 1 || total_elements <= elements_per_rank {
381            // If single device or small gradient, each rank gets a copy
382            grad.clone()
383        } else {
384            // For demonstration, create a scaled version to simulate partitioning
385            let scale_factor = 1.0 / (world_size as f32);
386
387            grad.mul_scalar(scale_factor)?
388        };
389
390        let partition_info = PartitionInfo {
391            rank,
392            world_size,
393            start_idx,
394            end_idx,
395            global_shape: shape.to_vec(),
396            local_shape: local_gradient.shape().to_vec(),
397        };
398
399        let buffer = GradientBuffer::new(name.clone(), local_gradient, partition_info);
400        buffers.insert(name.clone(), buffer);
401    }
402
403    Ok(buffers)
404}
405
406/// All-gather gradients from all devices
407pub fn all_gather_gradients(
408    buffers: &HashMap<String, GradientBuffer>,
409    mp_context: &ModelParallelContext,
410) -> Result<HashMap<String, Tensor>> {
411    let mut gathered = HashMap::new();
412
413    for (name, buffer) in buffers {
414        let distributed_tensor = trustformers_core::parallel::DistributedTensor::new(
415            buffer.local_gradient.clone(),
416            buffer.partition_info.global_shape.clone(),
417            trustformers_core::parallel::TensorPartition {
418                split_dim: 0,
419                start_idx: buffer.partition_info.start_idx,
420                end_idx: buffer.partition_info.end_idx,
421                num_partitions: buffer.partition_info.world_size,
422                partition_rank: buffer.partition_info.rank,
423            },
424            buffer.partition_info.rank,
425        );
426
427        let full_gradient = mp_context.all_gather(&distributed_tensor)?;
428        gathered.insert(name.clone(), full_gradient);
429    }
430
431    Ok(gathered)
432}
433
434/// Reduce-scatter gradients across devices
435pub fn reduce_scatter_gradients(
436    gradients: &HashMap<String, Tensor>,
437    mp_context: &ModelParallelContext,
438) -> Result<HashMap<String, Tensor>> {
439    let mut scattered = HashMap::new();
440
441    for (name, grad) in gradients {
442        let scattered_grad = mp_context.reduce_scatter(grad, 0)?;
443        scattered.insert(name.clone(), scattered_grad);
444    }
445
446    Ok(scattered)
447}
448
449/// Calculate optimal bucket size for gradient communication
450pub fn calculate_bucket_size(
451    parameter_sizes: &[usize],
452    target_bucket_size: usize,
453) -> Vec<Vec<usize>> {
454    let mut buckets = Vec::new();
455    let mut current_bucket = Vec::new();
456    let mut current_size = 0;
457
458    for (i, &size) in parameter_sizes.iter().enumerate() {
459        if current_size + size > target_bucket_size && !current_bucket.is_empty() {
460            buckets.push(current_bucket);
461            current_bucket = Vec::new();
462            current_size = 0;
463        }
464
465        current_bucket.push(i);
466        current_size += size;
467    }
468
469    if !current_bucket.is_empty() {
470        buckets.push(current_bucket);
471    }
472
473    buckets
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    #[test]
481    fn test_zero_state_creation() {
482        let state = ZeROState::new();
483        assert_eq!(state.step, 0);
484        assert!(state.optimizer_states.is_empty());
485        assert!(state.gradient_partitions.is_empty());
486        assert!(state.parameter_partitions.is_empty());
487    }
488
489    #[test]
490    fn test_parameter_group() {
491        let mut group = ParameterGroup::new("test_group".to_string(), vec!["param1".to_string()]);
492        let tensor = Tensor::ones(&[2, 2]).expect("Failed to create tensor");
493        group.add_parameter("param1".to_string(), tensor);
494
495        assert_eq!(group.parameter_names.len(), 1);
496        assert_eq!(group.local_parameters.len(), 1);
497        assert!(group.memory_usage() > 0);
498    }
499
500    #[test]
501    fn test_gradient_buffer() {
502        let tensor = Tensor::ones(&[2, 2]).expect("Failed to create tensor");
503        let partition_info = PartitionInfo::default();
504        let mut buffer = GradientBuffer::new("test_grad".to_string(), tensor, partition_info);
505
506        let grad = Tensor::ones(&[2, 2]).expect("Failed to create tensor");
507        buffer.accumulate(&grad).expect("Operation failed in test");
508
509        assert_eq!(buffer.accumulation_steps, 1);
510        assert!(buffer.get_accumulated().is_some());
511    }
512
513    #[test]
514    fn test_partition_parameters() {
515        let mut params = HashMap::new();
516        params.insert(
517            "param1".to_string(),
518            Tensor::ones(&[4, 4]).expect("Failed to create tensor"),
519        );
520        params.insert(
521            "param2".to_string(),
522            Tensor::ones(&[2, 2]).expect("Failed to create tensor"),
523        );
524
525        let partitions = partition_parameters(&params, 2, 0).expect("Operation failed in test");
526        assert_eq!(partitions.len(), 2);
527
528        for partition in partitions.values() {
529            assert_eq!(partition.partition_info.world_size, 2);
530            assert_eq!(partition.partition_info.rank, 0);
531        }
532    }
533
534    #[test]
535    fn test_calculate_bucket_size() {
536        let sizes = vec![100, 200, 150, 300, 50];
537        let buckets = calculate_bucket_size(&sizes, 400);
538
539        assert!(!buckets.is_empty());
540
541        // Check that no bucket exceeds the target size
542        for bucket in &buckets {
543            let bucket_size: usize = bucket.iter().map(|&i| sizes[i]).sum();
544            assert!(bucket_size <= 400 || bucket.len() == 1); // Single large item allowed
545        }
546    }
547}