Skip to main content

torsh_optim/
state_dict_ops.rs

1//! Optimized state dict operations for efficient optimizer state management
2//!
3//! This module provides optimized implementations for saving and loading optimizer state,
4//! including compression, serialization formats, and memory-efficient operations.
5
6use crate::{OptimizerState, ParamGroupState};
7use std::collections::HashMap;
8use torsh_core::error::{Result, TorshError};
9use torsh_core::DeviceType;
10use torsh_tensor::Tensor;
11
12/// Compression method for state dict serialization
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum CompressionMethod {
15    None,
16    Gzip,
17    Zstd,
18    Lz4,
19}
20
21/// Serialization format for state dict
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum SerializationFormat {
24    Binary,
25    Json,
26    MessagePack,
27    Protobuf,
28}
29
30/// Configuration for state dict operations
31#[derive(Debug, Clone)]
32pub struct StateDictConfig {
33    pub compression: CompressionMethod,
34    pub format: SerializationFormat,
35    pub compress_threshold: usize,
36    pub use_memory_mapping: bool,
37    pub chunk_size: usize,
38    pub parallel_processing: bool,
39}
40
41impl Default for StateDictConfig {
42    fn default() -> Self {
43        Self {
44            compression: CompressionMethod::Zstd,
45            format: SerializationFormat::Binary,
46            compress_threshold: 1024 * 1024, // 1MB
47            use_memory_mapping: true,
48            chunk_size: 64 * 1024 * 1024, // 64MB
49            parallel_processing: true,
50        }
51    }
52}
53
54/// Optimized state dict manager
55pub struct StateDictManager {
56    config: StateDictConfig,
57    cache: HashMap<String, CachedStateEntry>,
58    compression_stats: CompressionStats,
59}
60
61/// Cached state entry for fast access
62#[derive(Clone)]
63struct CachedStateEntry {
64    data: Vec<u8>,
65    checksum: u64,
66    last_accessed: std::time::Instant,
67    compression_ratio: f64,
68}
69
70/// Compression statistics
71#[derive(Debug, Clone, Default)]
72pub struct CompressionStats {
73    pub total_original_size: u64,
74    pub total_compressed_size: u64,
75    pub compression_time_ms: u64,
76    pub decompression_time_ms: u64,
77    pub cache_hits: u64,
78    pub cache_misses: u64,
79}
80
81impl CompressionStats {
82    /// Calculate overall compression ratio
83    pub fn compression_ratio(&self) -> f64 {
84        if self.total_original_size == 0 {
85            1.0
86        } else {
87            self.total_compressed_size as f64 / self.total_original_size as f64
88        }
89    }
90
91    /// Calculate cache hit rate
92    pub fn cache_hit_rate(&self) -> f64 {
93        let total_accesses = self.cache_hits + self.cache_misses;
94        if total_accesses == 0 {
95            0.0
96        } else {
97            self.cache_hits as f64 / total_accesses as f64
98        }
99    }
100}
101
102impl StateDictManager {
103    /// Create a new state dict manager with default configuration
104    pub fn new() -> Self {
105        Self::with_config(StateDictConfig::default())
106    }
107
108    /// Create a new state dict manager with custom configuration
109    pub fn with_config(config: StateDictConfig) -> Self {
110        Self {
111            config,
112            cache: HashMap::new(),
113            compression_stats: CompressionStats::default(),
114        }
115    }
116
117    /// Serialize optimizer state to bytes with optimization
118    pub fn serialize_state(&mut self, state: &OptimizerState) -> Result<Vec<u8>> {
119        let start_time = std::time::Instant::now();
120
121        // Convert state to serializable format
122        let serializable_state = self.prepare_for_serialization(state)?;
123
124        // Serialize based on format
125        let mut data = match self.config.format {
126            SerializationFormat::Binary => self.serialize_binary(&serializable_state)?,
127            SerializationFormat::Json => self.serialize_json(&serializable_state)?,
128            SerializationFormat::MessagePack => self.serialize_msgpack(&serializable_state)?,
129            SerializationFormat::Protobuf => self.serialize_protobuf(&serializable_state)?,
130        };
131
132        let original_size = data.len() as u64;
133
134        // Apply compression if data is large enough
135        if data.len() > self.config.compress_threshold {
136            data = self.compress_data(data)?;
137        }
138
139        let compressed_size = data.len() as u64;
140
141        // Update statistics
142        self.compression_stats.total_original_size += original_size;
143        self.compression_stats.total_compressed_size += compressed_size;
144        self.compression_stats.compression_time_ms += start_time.elapsed().as_millis() as u64;
145
146        Ok(data)
147    }
148
149    /// Deserialize optimizer state from bytes with optimization
150    pub fn deserialize_state(&mut self, data: &[u8]) -> Result<OptimizerState> {
151        let start_time = std::time::Instant::now();
152
153        // Decompress data if needed
154        let decompressed_data = if self.is_compressed(data) {
155            self.decompress_data(data)?
156        } else {
157            data.to_vec()
158        };
159
160        // Deserialize based on format
161        let serializable_state = match self.config.format {
162            SerializationFormat::Binary => self.deserialize_binary(&decompressed_data)?,
163            SerializationFormat::Json => self.deserialize_json(&decompressed_data)?,
164            SerializationFormat::MessagePack => self.deserialize_msgpack(&decompressed_data)?,
165            SerializationFormat::Protobuf => self.deserialize_protobuf(&decompressed_data)?,
166        };
167
168        // Convert back to optimizer state
169        let state = self.restore_from_serialization(serializable_state)?;
170
171        self.compression_stats.decompression_time_ms += start_time.elapsed().as_millis() as u64;
172
173        Ok(state)
174    }
175
176    /// Save optimizer state to file with optimizations
177    pub fn save_to_file(&mut self, state: &OptimizerState, path: &str) -> Result<()> {
178        let data = self.serialize_state(state)?;
179
180        if self.config.use_memory_mapping && data.len() > self.config.chunk_size {
181            self.save_with_memory_mapping(&data, path)
182        } else {
183            std::fs::write(path, data)
184                .map_err(|e| TorshError::IoError(format!("Failed to write file: {e}")))?;
185            Ok(())
186        }
187    }
188
189    /// Load optimizer state from file with optimizations
190    pub fn load_from_file(&mut self, path: &str) -> Result<OptimizerState> {
191        let data = if self.config.use_memory_mapping {
192            self.load_with_memory_mapping(path)?
193        } else {
194            std::fs::read(path)
195                .map_err(|e| TorshError::IoError(format!("Failed to read file: {e}")))?
196        };
197
198        self.deserialize_state(&data)
199    }
200
201    /// Get compression statistics
202    pub fn get_stats(&self) -> &CompressionStats {
203        &self.compression_stats
204    }
205
206    /// Clear cache and reset statistics
207    pub fn clear_cache(&mut self) {
208        self.cache.clear();
209        self.compression_stats = CompressionStats::default();
210    }
211
212    /// Prepare state for serialization (convert tensors to raw data)
213    fn prepare_for_serialization(&self, state: &OptimizerState) -> Result<SerializableState> {
214        let mut serializable_tensors = HashMap::new();
215
216        for (param_id, tensor_map) in &state.state {
217            let mut param_tensors = HashMap::new();
218            for (tensor_name, tensor) in tensor_map {
219                // Convert tensor to raw bytes for serialization
220                let tensor_data = TensorData {
221                    data: tensor.to_vec()?,
222                    shape: tensor.shape().dims().to_vec(),
223                    dtype: format!("{:?}", tensor.dtype()),
224                    device: format!("{:?}", tensor.device()),
225                };
226                param_tensors.insert(tensor_name.clone(), tensor_data);
227            }
228            serializable_tensors.insert(param_id.clone(), param_tensors);
229        }
230
231        Ok(SerializableState {
232            optimizer_type: state.optimizer_type.clone(),
233            version: state.version.clone(),
234            param_groups: state.param_groups.clone(),
235            state: serializable_tensors,
236            global_state: state.global_state.clone(),
237        })
238    }
239
240    /// Restore state from serialization
241    fn restore_from_serialization(
242        &self,
243        serializable: SerializableState,
244    ) -> Result<OptimizerState> {
245        let mut state_tensors = HashMap::new();
246
247        for (param_id, tensor_map) in serializable.state {
248            let mut param_tensors = HashMap::new();
249            for (tensor_name, tensor_data) in tensor_map {
250                // Recreate tensor from raw data
251                let tensor = self.recreate_tensor(tensor_data)?;
252                param_tensors.insert(tensor_name, tensor);
253            }
254            state_tensors.insert(param_id, param_tensors);
255        }
256
257        Ok(OptimizerState {
258            optimizer_type: serializable.optimizer_type,
259            version: serializable.version,
260            param_groups: serializable.param_groups,
261            state: state_tensors,
262            global_state: serializable.global_state,
263        })
264    }
265
266    /// Recreate tensor from serialized data
267    fn recreate_tensor(&self, data: TensorData) -> Result<Tensor> {
268        // This is a simplified implementation
269        // In a real implementation, this would properly recreate the tensor
270        // with the correct device, dtype, and shape
271        let device = DeviceType::Cpu; // Simplified
272        Ok(torsh_tensor::creation::from_vec(
273            data.data,
274            &data.shape,
275            device,
276        )?)
277    }
278
279    /// Compress data using the configured method
280    fn compress_data(&self, data: Vec<u8>) -> Result<Vec<u8>> {
281        match self.config.compression {
282            CompressionMethod::None => Ok(data),
283            CompressionMethod::Gzip => {
284                // Simplified - would use actual gzip compression
285                Ok(data)
286            }
287            CompressionMethod::Zstd => {
288                // Simplified - would use actual zstd compression
289                Ok(data)
290            }
291            CompressionMethod::Lz4 => {
292                // Simplified - would use actual lz4 compression
293                Ok(data)
294            }
295        }
296    }
297
298    /// Decompress data
299    fn decompress_data(&self, data: &[u8]) -> Result<Vec<u8>> {
300        // Simplified implementation
301        Ok(data.to_vec())
302    }
303
304    /// Check if data is compressed
305    fn is_compressed(&self, data: &[u8]) -> bool {
306        // Simplified check - would check magic bytes for compression format
307        data.len() > 4 && &data[0..4] == b"COMP"
308    }
309
310    /// Serialize to binary format
311    fn serialize_binary(&self, state: &SerializableState) -> Result<Vec<u8>> {
312        // Simplified binary serialization
313        Ok(format!("{:?}", state).into_bytes())
314    }
315
316    /// Deserialize from binary format
317    fn deserialize_binary(&self, _data: &[u8]) -> Result<SerializableState> {
318        // Simplified binary deserialization
319        Err(TorshError::Other(
320            "Binary deserialization not fully implemented".to_string(),
321        ))
322    }
323
324    /// Serialize to JSON format
325    fn serialize_json(&self, state: &SerializableState) -> Result<Vec<u8>> {
326        // Would use serde_json for actual implementation
327        Ok(format!("{:?}", state).into_bytes())
328    }
329
330    /// Deserialize from JSON format
331    fn deserialize_json(&self, _data: &[u8]) -> Result<SerializableState> {
332        // Would use serde_json for actual implementation
333        Err(TorshError::Other(
334            "JSON deserialization not fully implemented".to_string(),
335        ))
336    }
337
338    /// Serialize to MessagePack format
339    fn serialize_msgpack(&self, state: &SerializableState) -> Result<Vec<u8>> {
340        // Would use rmp-serde for actual implementation
341        Ok(format!("{:?}", state).into_bytes())
342    }
343
344    /// Deserialize from MessagePack format
345    fn deserialize_msgpack(&self, _data: &[u8]) -> Result<SerializableState> {
346        // Would use rmp-serde for actual implementation
347        Err(TorshError::Other(
348            "MessagePack deserialization not fully implemented".to_string(),
349        ))
350    }
351
352    /// Serialize to Protobuf format
353    fn serialize_protobuf(&self, state: &SerializableState) -> Result<Vec<u8>> {
354        // Would use prost for actual implementation
355        Ok(format!("{:?}", state).into_bytes())
356    }
357
358    /// Deserialize from Protobuf format
359    fn deserialize_protobuf(&self, _data: &[u8]) -> Result<SerializableState> {
360        // Would use prost for actual implementation
361        Err(TorshError::Other(
362            "Protobuf deserialization not fully implemented".to_string(),
363        ))
364    }
365
366    /// Save using memory mapping for large files
367    fn save_with_memory_mapping(&self, data: &[u8], path: &str) -> Result<()> {
368        // Simplified implementation - would use memmap2 crate
369        std::fs::write(path, data)
370            .map_err(|e| TorshError::IoError(format!("Failed to write file: {e}")))?;
371        Ok(())
372    }
373
374    /// Load using memory mapping for large files
375    fn load_with_memory_mapping(&self, path: &str) -> Result<Vec<u8>> {
376        // Simplified implementation - would use memmap2 crate
377        std::fs::read(path).map_err(|e| TorshError::IoError(format!("Failed to read file: {e}")))
378    }
379}
380
381impl Default for StateDictManager {
382    fn default() -> Self {
383        Self::new()
384    }
385}
386
387/// Serializable representation of optimizer state
388#[derive(Debug, Clone)]
389struct SerializableState {
390    optimizer_type: String,
391    version: String,
392    param_groups: Vec<ParamGroupState>,
393    state: HashMap<String, HashMap<String, TensorData>>,
394    global_state: HashMap<String, f32>,
395}
396
397/// Serializable tensor data
398#[derive(Debug, Clone)]
399struct TensorData {
400    data: Vec<f32>,
401    shape: Vec<usize>,
402    dtype: String,
403    device: String,
404}
405
406/// Utility functions for state dict operations
407pub mod utils {
408    use super::*;
409
410    /// Calculate state dict size in bytes
411    pub fn calculate_state_size(state: &OptimizerState) -> usize {
412        let mut total_size = 0;
413
414        for (_, tensor_map) in &state.state {
415            for (_, tensor) in tensor_map {
416                total_size += tensor.numel() * std::mem::size_of::<f32>();
417            }
418        }
419
420        total_size
421    }
422
423    /// Estimate memory usage for state dict operations
424    pub fn estimate_memory_usage(
425        state: &OptimizerState,
426        config: &StateDictConfig,
427    ) -> MemoryEstimate {
428        let state_size = calculate_state_size(state);
429        let serialization_overhead = state_size / 4; // Rough estimate
430        let compression_working_memory = if matches!(config.compression, CompressionMethod::None) {
431            0
432        } else {
433            state_size / 2
434        };
435
436        MemoryEstimate {
437            state_size,
438            serialization_overhead,
439            compression_working_memory,
440            total_peak_usage: state_size + serialization_overhead + compression_working_memory,
441        }
442    }
443
444    /// Optimize state dict configuration based on state size
445    pub fn optimize_config_for_size(state_size: usize) -> StateDictConfig {
446        let mut config = StateDictConfig::default();
447
448        if state_size > 1024 * 1024 * 1024 {
449            // > 1GB
450            config.compression = CompressionMethod::Zstd;
451            config.use_memory_mapping = true;
452            config.chunk_size = 128 * 1024 * 1024; // 128MB chunks
453            config.parallel_processing = true;
454        } else if state_size > 100 * 1024 * 1024 {
455            // > 100MB
456            config.compression = CompressionMethod::Lz4; // Faster compression
457            config.use_memory_mapping = false;
458            config.parallel_processing = true;
459        } else {
460            config.compression = CompressionMethod::None;
461            config.use_memory_mapping = false;
462            config.parallel_processing = false;
463        }
464
465        config
466    }
467}
468
469/// Memory usage estimate for state dict operations
470#[derive(Debug, Clone)]
471pub struct MemoryEstimate {
472    pub state_size: usize,
473    pub serialization_overhead: usize,
474    pub compression_working_memory: usize,
475    pub total_peak_usage: usize,
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::OptimizerResult;
482    use torsh_core::device::Device;
483    use torsh_tensor::creation;
484
485    #[test]
486    fn test_state_dict_manager_creation() {
487        let manager = StateDictManager::new();
488        assert_eq!(manager.config.compression, CompressionMethod::Zstd);
489        assert_eq!(manager.config.format, SerializationFormat::Binary);
490    }
491
492    #[test]
493    fn test_compression_stats() {
494        let mut stats = CompressionStats::default();
495        stats.total_original_size = 1000;
496        stats.total_compressed_size = 500;
497        stats.cache_hits = 8;
498        stats.cache_misses = 2;
499
500        assert_eq!(stats.compression_ratio(), 0.5);
501        assert_eq!(stats.cache_hit_rate(), 0.8);
502    }
503
504    #[test]
505    fn test_config_optimization() {
506        let small_config = utils::optimize_config_for_size(1024); // 1KB
507        assert_eq!(small_config.compression, CompressionMethod::None);
508        assert!(!small_config.use_memory_mapping);
509
510        let large_config = utils::optimize_config_for_size(2 * 1024 * 1024 * 1024); // 2GB
511        assert_eq!(large_config.compression, CompressionMethod::Zstd);
512        assert!(large_config.use_memory_mapping);
513    }
514
515    #[test]
516    fn test_memory_estimation() -> OptimizerResult<()> {
517        // Create a simple state for testing
518        let tensor = creation::randn::<f32>(&[100, 100])?;
519
520        let mut state_map = HashMap::new();
521        let mut tensor_map = HashMap::new();
522        tensor_map.insert("test_tensor".to_string(), tensor);
523        state_map.insert("param_1".to_string(), tensor_map);
524
525        let state = OptimizerState {
526            optimizer_type: "test".to_string(),
527            version: "0.1.0".to_string(),
528            param_groups: vec![],
529            state: state_map,
530            global_state: HashMap::new(),
531        };
532
533        let config = StateDictConfig::default();
534        let estimate = utils::estimate_memory_usage(&state, &config);
535
536        assert!(estimate.state_size > 0);
537        assert!(estimate.total_peak_usage >= estimate.state_size);
538        Ok(())
539    }
540
541    #[test]
542    fn test_calculate_state_size() -> OptimizerResult<()> {
543        let tensor1 = creation::randn::<f32>(&[10, 10])?; // 100 elements
544        let tensor2 = creation::randn::<f32>(&[5, 5])?; // 25 elements
545
546        let mut state_map = HashMap::new();
547        let mut tensor_map = HashMap::new();
548        tensor_map.insert("tensor1".to_string(), tensor1);
549        tensor_map.insert("tensor2".to_string(), tensor2);
550        state_map.insert("param_1".to_string(), tensor_map);
551
552        let state = OptimizerState {
553            optimizer_type: "test".to_string(),
554            version: "0.1.0".to_string(),
555            param_groups: vec![],
556            state: state_map,
557            global_state: HashMap::new(),
558        };
559
560        let size = utils::calculate_state_size(&state);
561        // 125 elements * 4 bytes per f32
562        assert_eq!(size, 125 * 4);
563        Ok(())
564    }
565}