1#![allow(dead_code)]
21
22use crate::StatefulOptimizer;
23use serde::{Deserialize, Serialize};
24use std::collections::HashMap;
25use trustformers_core::errors::Result;
26use trustformers_core::tensor::Tensor;
27use trustformers_core::traits::Optimizer;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CPUOffloadConfig {
32 pub offload_optimizer_states: bool,
34 pub offload_gradients: bool,
36 pub offload_parameters: bool,
38 pub overlap_transfers: bool,
40 pub pin_memory: bool,
42 pub offload_threshold: usize,
44}
45
46impl Default for CPUOffloadConfig {
47 fn default() -> Self {
48 Self {
49 offload_optimizer_states: true,
50 offload_gradients: false,
51 offload_parameters: false,
52 overlap_transfers: true,
53 pin_memory: true,
54 offload_threshold: 1024 * 1024, }
56 }
57}
58
59pub struct CPUOffloadedOptimizer<T: Optimizer> {
61 base_optimizer: T,
62 config: CPUOffloadConfig,
63 cpu_states: HashMap<String, Tensor>,
64 gpu_states: HashMap<String, Tensor>,
65 transfer_stream: Option<usize>, memory_stats: CPUOffloadStats,
67}
68
69#[derive(Debug, Default)]
70pub struct CPUOffloadStats {
71 pub total_cpu_memory_bytes: usize,
72 pub total_gpu_memory_bytes: usize,
73 pub transfers_to_cpu: usize,
74 pub transfers_to_gpu: usize,
75 pub transfer_time_ms: f64,
76}
77
78impl<T: Optimizer + StatefulOptimizer> CPUOffloadedOptimizer<T> {
79 pub fn new(base_optimizer: T, config: CPUOffloadConfig) -> Self {
81 Self {
82 base_optimizer,
83 config,
84 cpu_states: HashMap::new(),
85 gpu_states: HashMap::new(),
86 transfer_stream: None,
87 memory_stats: CPUOffloadStats::default(),
88 }
89 }
90
91 pub fn with_default_config(base_optimizer: T) -> Self {
93 Self::new(base_optimizer, CPUOffloadConfig::default())
94 }
95
96 pub fn get_memory_stats(&self) -> &CPUOffloadStats {
98 &self.memory_stats
99 }
100
101 pub fn get_memory_savings_bytes(&self) -> usize {
103 self.memory_stats.total_cpu_memory_bytes
104 }
105
106 pub fn get_memory_savings_percent(&self) -> f32 {
108 let total_memory =
109 self.memory_stats.total_cpu_memory_bytes + self.memory_stats.total_gpu_memory_bytes;
110 if total_memory == 0 {
111 0.0
112 } else {
113 (self.memory_stats.total_cpu_memory_bytes as f32 / total_memory as f32) * 100.0
114 }
115 }
116
117 fn offload_to_cpu(&mut self, key: &str, tensor: Tensor) -> Result<()> {
119 if tensor.size_bytes() >= self.config.offload_threshold {
120 let start_time = std::time::Instant::now();
121
122 let cpu_tensor = tensor.to_device("cpu")?;
124 self.cpu_states.insert(key.to_string(), cpu_tensor);
125
126 self.memory_stats.total_cpu_memory_bytes += tensor.size_bytes();
128 self.memory_stats.transfers_to_cpu += 1;
129 self.memory_stats.transfer_time_ms += start_time.elapsed().as_secs_f64() * 1000.0;
130
131 if let Some(gpu_tensor) = self.gpu_states.remove(key) {
133 self.memory_stats.total_gpu_memory_bytes -= gpu_tensor.size_bytes();
134 }
135 } else {
136 self.memory_stats.total_gpu_memory_bytes += tensor.size_bytes();
138 self.gpu_states.insert(key.to_string(), tensor);
139 }
140
141 Ok(())
142 }
143
144 fn retrieve_from_cpu(&mut self, key: &str, target_device: &str) -> Result<Option<Tensor>> {
146 if let Some(cpu_tensor) = self.cpu_states.get(key) {
147 let start_time = std::time::Instant::now();
148
149 let gpu_tensor = cpu_tensor.to_device(target_device)?;
151 let tensor_size = gpu_tensor.size_bytes();
152
153 self.gpu_states.insert(key.to_string(), gpu_tensor.clone());
155
156 self.memory_stats.total_gpu_memory_bytes += tensor_size;
158 self.memory_stats.transfers_to_gpu += 1;
159 self.memory_stats.transfer_time_ms += start_time.elapsed().as_secs_f64() * 1000.0;
160
161 Ok(Some(gpu_tensor))
162 } else {
163 Ok(self.gpu_states.get(key).cloned())
165 }
166 }
167
168 pub fn prefetch_states(&mut self, keys: &[String], device: &str) -> Result<()> {
170 if !self.config.overlap_transfers {
171 return Ok(());
172 }
173
174 for key in keys {
175 if self.cpu_states.contains_key(key) && !self.gpu_states.contains_key(key) {
176 self.retrieve_from_cpu(key, device)?;
178 }
179 }
180
181 Ok(())
182 }
183
184 pub fn evict_unused_states(&mut self, keep_keys: &[String]) -> Result<()> {
186 let mut to_remove = Vec::new();
187
188 for key in self.gpu_states.keys() {
189 if !keep_keys.contains(&key.to_string()) && self.cpu_states.contains_key(key) {
190 to_remove.push(key.clone());
191 }
192 }
193
194 for key in to_remove {
195 if let Some(tensor) = self.gpu_states.remove(&key) {
196 self.memory_stats.total_gpu_memory_bytes -= tensor.size_bytes();
197 }
198 }
199
200 Ok(())
201 }
202
203 pub fn get_config(&self) -> &CPUOffloadConfig {
205 &self.config
206 }
207
208 pub fn set_config(&mut self, config: CPUOffloadConfig) {
210 self.config = config;
211 }
212}
213
214impl<T: Optimizer> Optimizer for CPUOffloadedOptimizer<T> {
215 fn update(&mut self, parameter: &mut Tensor, grad: &Tensor) -> Result<()> {
216 self.base_optimizer.update(parameter, grad)
217 }
218
219 fn zero_grad(&mut self) {
220 self.base_optimizer.zero_grad()
221 }
222
223 fn step(&mut self) {
224 self.base_optimizer.step()
225 }
226
227 fn get_lr(&self) -> f32 {
228 self.base_optimizer.get_lr()
229 }
230
231 fn set_lr(&mut self, lr: f32) {
232 self.base_optimizer.set_lr(lr)
233 }
234}
235
236impl<T: Optimizer + StatefulOptimizer> CPUOffloadedOptimizer<T> {
237 fn state_dict(&self) -> Result<HashMap<String, Tensor>> {
238 let mut state = self.base_optimizer.state_dict()?;
240
241 for (key, tensor) in &self.cpu_states {
243 state.insert(format!("cpu_{}", key), tensor.clone());
244 }
245
246 Ok(state)
247 }
248
249 fn load_state_dict(&mut self, state: HashMap<String, Tensor>) -> Result<()> {
250 let mut base_state = HashMap::new();
251 let mut cpu_state = HashMap::new();
252
253 for (key, tensor) in state {
255 if let Some(cpu_key) = key.strip_prefix("cpu_") {
256 cpu_state.insert(cpu_key.to_string(), tensor);
257 } else {
258 base_state.insert(key, tensor);
259 }
260 }
261
262 self.base_optimizer.load_state_dict(base_state)?;
264
265 self.cpu_states = cpu_state;
267
268 Ok(())
269 }
270}
271
272impl<T: Optimizer + StatefulOptimizer> CPUOffloadedOptimizer<T> {
273 fn offload_states_after_step(&mut self, param_names: &[String]) -> Result<()> {
276 if !self.config.offload_optimizer_states {
277 return Ok(());
278 }
279
280 let current_states = self.base_optimizer.state_dict()?;
282
283 for param_name in param_names {
285 for (state_key, state_tensor) in ¤t_states {
287 if state_key.starts_with(param_name) || state_key.contains(param_name) {
290 if state_tensor.size_bytes() >= self.config.offload_threshold {
292 let device = state_tensor.device();
293
294 if device.starts_with("cuda") || device.starts_with("gpu") {
296 self.offload_to_cpu(state_key, state_tensor.clone())?;
297
298 }
300 }
301 }
302 }
303
304 let keys_to_offload: Vec<String> = self
306 .gpu_states
307 .keys()
308 .filter(|key| key.starts_with(param_name) || key.contains(param_name))
309 .cloned()
310 .collect();
311
312 for key in keys_to_offload {
313 if let Some(gpu_tensor) = self.gpu_states.get(&key).cloned() {
314 self.offload_to_cpu(&key, gpu_tensor)?;
315 }
316 }
317 }
318
319 Ok(())
320 }
321}
322
323pub fn create_cpu_offloaded_adam(
325 learning_rate: f32,
326 beta1: f32,
327 beta2: f32,
328 epsilon: f32,
329 weight_decay: f32,
330 config: Option<CPUOffloadConfig>,
331) -> CPUOffloadedOptimizer<crate::adam::Adam> {
332 let adam = crate::adam::Adam::new(learning_rate, (beta1, beta2), epsilon, weight_decay);
333 CPUOffloadedOptimizer::new(adam, config.unwrap_or_default())
334}
335
336pub fn create_cpu_offloaded_adamw(
338 learning_rate: f32,
339 beta1: f32,
340 beta2: f32,
341 epsilon: f32,
342 weight_decay: f32,
343 config: Option<CPUOffloadConfig>,
344) -> CPUOffloadedOptimizer<crate::adam::AdamW> {
345 let adamw = crate::adam::AdamW::new(learning_rate, (beta1, beta2), epsilon, weight_decay);
346 CPUOffloadedOptimizer::new(adamw, config.unwrap_or_default())
347}
348
349pub fn create_cpu_offloaded_sgd(
351 learning_rate: f32,
352 momentum: f32,
353 _dampening: f32,
354 weight_decay: f32,
355 nesterov: bool,
356 config: Option<CPUOffloadConfig>,
357) -> CPUOffloadedOptimizer<crate::sgd::SGD> {
358 let sgd = crate::sgd::SGD::new(learning_rate, momentum, weight_decay, nesterov);
359 CPUOffloadedOptimizer::new(sgd, config.unwrap_or_default())
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_cpu_offload_config_default() {
368 let config = CPUOffloadConfig::default();
369 assert!(config.offload_optimizer_states);
370 assert!(!config.offload_gradients);
371 assert!(!config.offload_parameters);
372 assert!(config.overlap_transfers);
373 assert!(config.pin_memory);
374 assert_eq!(config.offload_threshold, 1024 * 1024);
375 }
376
377 #[test]
378 fn test_memory_stats() {
379 let adam = crate::adam::Adam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
380 let optimizer = CPUOffloadedOptimizer::new(adam, CPUOffloadConfig::default());
381
382 let stats = optimizer.get_memory_stats();
383 assert_eq!(stats.total_cpu_memory_bytes, 0);
384 assert_eq!(stats.total_gpu_memory_bytes, 0);
385 assert_eq!(stats.transfers_to_cpu, 0);
386 assert_eq!(stats.transfers_to_gpu, 0);
387 }
388
389 #[test]
390 fn test_memory_savings_calculation() {
391 let adam = crate::adam::Adam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
392 let optimizer = CPUOffloadedOptimizer::new(adam, CPUOffloadConfig::default());
393
394 assert_eq!(optimizer.get_memory_savings_percent(), 0.0);
396 assert_eq!(optimizer.get_memory_savings_bytes(), 0);
397 }
398
399 #[test]
400 fn test_convenience_functions() {
401 let _adam_offload = create_cpu_offloaded_adam(1e-3, 0.9, 0.999, 1e-8, 0.01, None);
402 let _adamw_offload = create_cpu_offloaded_adamw(1e-3, 0.9, 0.999, 1e-8, 0.01, None);
403 let _sgd_offload = create_cpu_offloaded_sgd(1e-2, 0.9, 0.0, 1e-4, false, None);
404
405 }
407
408 #[test]
409 fn test_config_update() {
410 let adam = crate::adam::Adam::new(1e-3, (0.9, 0.999), 1e-8, 0.01);
411 let mut optimizer = CPUOffloadedOptimizer::new(adam, CPUOffloadConfig::default());
412
413 let new_config = CPUOffloadConfig {
414 offload_gradients: true,
415 offload_threshold: 2048,
416 ..CPUOffloadConfig::default()
417 };
418
419 optimizer.set_config(new_config.clone());
420
421 assert!(optimizer.get_config().offload_gradients);
422 assert_eq!(optimizer.get_config().offload_threshold, 2048);
423 }
424}