optirs_core/memory_efficient_optimizer.rs
1//! Memory-efficient optimizer operations
2//!
3//! This module provides memory-efficient optimization for very large models
4//! through gradient accumulation, chunked processing, and memory usage estimation.
5//!
6//! # Features
7//!
8//! - Gradient accumulation to reduce memory pressure
9//! - Chunked parameter processing for large models
10//! - Memory usage estimation and recommendations
11//! - Streaming gradient computation
12//!
13//! # Performance
14//!
15//! Enables optimization of models with billions of parameters through efficient memory management.
16
17use scirs2_core::ndarray::{s, Array1, ArrayView1, Ix1, ScalarOperand};
18use scirs2_core::numeric::{Float, Zero};
19use std::fmt::Debug;
20
21use crate::error::Result;
22use crate::optimizers::Optimizer;
23use crate::utils::try_scalar;
24
25/// Gradient accumulator for memory-efficient training
26///
27/// Accumulates gradients over multiple micro-batches before applying updates,
28/// reducing memory requirements for large batch training.
29///
30/// # Examples
31///
32/// ```
33/// use scirs2_core::ndarray::Array1;
34/// use optirs_core::memory_efficient_optimizer::GradientAccumulator;
35///
36/// let mut accumulator = GradientAccumulator::<f32>::new(1000);
37///
38/// // Accumulate gradients from 4 micro-batches
39/// for _ in 0..4 {
40/// let micro_batch_grads = Array1::from_elem(1000, 0.1);
41/// accumulator.accumulate(µ_batch_grads.view()).expect("shapes match the accumulator");
42/// }
43///
44/// // Get averaged gradients
45/// let avg_grads = accumulator.average().expect("at least one micro-batch was accumulated");
46/// ```
47pub struct GradientAccumulator<A: Float> {
48 accumulated: Array1<A>,
49 count: usize,
50}
51
52impl<A: Float + ScalarOperand + Debug + Zero> GradientAccumulator<A> {
53 /// Creates a new gradient accumulator
54 ///
55 /// # Arguments
56 ///
57 /// * `size` - Size of gradient vectors
58 pub fn new(size: usize) -> Self {
59 Self {
60 accumulated: Array1::zeros(size),
61 count: 0,
62 }
63 }
64
65 /// Accumulate a gradient vector
66 ///
67 /// # Arguments
68 ///
69 /// * `gradients` - Gradients to accumulate
70 pub fn accumulate(&mut self, gradients: &ArrayView1<A>) -> Result<()> {
71 if gradients.len() != self.accumulated.len() {
72 return Err(crate::error::OptimError::DimensionMismatch(format!(
73 "Gradient size ({}) doesn't match accumulator size ({})",
74 gradients.len(),
75 self.accumulated.len()
76 )));
77 }
78
79 self.accumulated = &self.accumulated + gradients;
80 self.count += 1;
81
82 Ok(())
83 }
84
85 /// Get the number of accumulated gradients
86 pub fn count(&self) -> usize {
87 self.count
88 }
89
90 /// Compute the average of accumulated gradients
91 ///
92 /// Returns the averaged gradients and resets the accumulator.
93 pub fn average(&mut self) -> Result<Array1<A>> {
94 if self.count == 0 {
95 return Err(crate::error::OptimError::InvalidConfig(
96 "No gradients accumulated".to_string(),
97 ));
98 }
99
100 let scale = try_scalar::<A, _>(self.count)?;
101 let averaged = &self.accumulated / scale;
102
103 // Reset accumulator
104 self.reset();
105
106 Ok(averaged)
107 }
108
109 /// Reset the accumulator
110 pub fn reset(&mut self) {
111 self.accumulated.fill(A::zero());
112 self.count = 0;
113 }
114
115 /// Check if accumulator has reached target count
116 ///
117 /// # Arguments
118 ///
119 /// * `target` - Target number of accumulations
120 pub fn is_ready(&self, target: usize) -> bool {
121 self.count >= target
122 }
123}
124
125/// Chunked optimizer for processing large parameter arrays in chunks
126///
127/// Enables optimization of very large models by processing parameters
128/// in manageable chunks, reducing peak memory usage.
129pub struct ChunkedOptimizer<O, A>
130where
131 O: Optimizer<A, Ix1> + Clone,
132 A: Float + ScalarOperand + Debug,
133{
134 base_optimizer: O,
135 chunk_size: usize,
136 _phantom: std::marker::PhantomData<A>,
137}
138
139impl<O, A> ChunkedOptimizer<O, A>
140where
141 O: Optimizer<A, Ix1> + Clone,
142 A: Float + ScalarOperand + Debug,
143{
144 /// Creates a new chunked optimizer
145 ///
146 /// # Arguments
147 ///
148 /// * `base_optimizer` - Base optimizer to use for each chunk
149 /// * `chunk_size` - Size of each chunk (default: 1M elements)
150 pub fn new(base_optimizer: O, chunk_size: Option<usize>) -> Self {
151 let chunk_size = chunk_size.unwrap_or(1_000_000);
152
153 Self {
154 base_optimizer,
155 chunk_size,
156 _phantom: std::marker::PhantomData,
157 }
158 }
159
160 /// Process parameters in chunks
161 ///
162 /// # Arguments
163 ///
164 /// * `params` - Full parameter array
165 /// * `gradients` - Full gradient array
166 ///
167 /// # Returns
168 ///
169 /// Updated parameters
170 pub fn step_chunked(&mut self, params: &Array1<A>, gradients: &Array1<A>) -> Result<Array1<A>> {
171 if params.len() != gradients.len() {
172 return Err(crate::error::OptimError::DimensionMismatch(format!(
173 "Parameters ({}) and gradients ({}) must have same size",
174 params.len(),
175 gradients.len()
176 )));
177 }
178
179 let total_size = params.len();
180 let mut updated = Array1::zeros(total_size);
181
182 // Process in chunks
183 let num_chunks = total_size.div_ceil(self.chunk_size);
184
185 for chunk_idx in 0..num_chunks {
186 let start = chunk_idx * self.chunk_size;
187 let end = (start + self.chunk_size).min(total_size);
188
189 // Extract chunk views
190 let params_chunk = params.slice(s![start..end]).to_owned();
191 let grads_chunk = gradients.slice(s![start..end]).to_owned();
192
193 // Update chunk
194 let updated_chunk = self.base_optimizer.step(¶ms_chunk, &grads_chunk)?;
195
196 // Copy back to result
197 updated.slice_mut(s![start..end]).assign(&updated_chunk);
198 }
199
200 Ok(updated)
201 }
202
203 /// Get the chunk size
204 pub fn chunk_size(&self) -> usize {
205 self.chunk_size
206 }
207
208 /// Calculate number of chunks for given size
209 pub fn num_chunks(&self, total_size: usize) -> usize {
210 total_size.div_ceil(self.chunk_size)
211 }
212}
213
214/// Memory usage estimator for optimizers
215///
216/// Provides utilities for estimating memory requirements and recommending
217/// optimal configurations for different optimizer types.
218pub struct MemoryUsageEstimator;
219
220impl MemoryUsageEstimator {
221 /// Estimate memory usage for SGD without momentum
222 ///
223 /// # Arguments
224 ///
225 /// * `num_params` - Number of parameters
226 /// * `dtype_size` - Size of data type in bytes (4 for f32, 8 for f64)
227 ///
228 /// # Returns
229 ///
230 /// Estimated memory usage in bytes
231 pub fn sgd(num_params: usize, dtype_size: usize) -> usize {
232 // Parameters + gradients
233 num_params * dtype_size * 2
234 }
235
236 /// Estimate memory usage for SGD with momentum
237 ///
238 /// # Arguments
239 ///
240 /// * `num_params` - Number of parameters
241 /// * `dtype_size` - Size of data type in bytes (4 for f32, 8 for f64)
242 ///
243 /// # Returns
244 ///
245 /// Estimated memory usage in bytes
246 pub fn sgd_with_momentum(num_params: usize, dtype_size: usize) -> usize {
247 // Parameters + gradients + velocity
248 num_params * dtype_size * 3
249 }
250
251 /// Estimate memory usage for Adam optimizer
252 ///
253 /// # Arguments
254 ///
255 /// * `num_params` - Number of parameters
256 /// * `dtype_size` - Size of data type in bytes (4 for f32, 8 for f64)
257 ///
258 /// # Returns
259 ///
260 /// Estimated memory usage in bytes
261 pub fn adam(num_params: usize, dtype_size: usize) -> usize {
262 // Parameters + gradients + first moment + second moment
263 num_params * dtype_size * 4
264 }
265
266 /// Recommend chunk size based on available memory
267 ///
268 /// # Arguments
269 ///
270 /// * `total_params` - Total number of parameters
271 /// * `available_memory_bytes` - Available memory in bytes
272 /// * `dtype_size` - Size of data type in bytes (4 for f32, 8 for f64)
273 /// * `optimizer_state_multiplier` - Memory multiplier for optimizer state
274 ///
275 /// # Returns
276 ///
277 /// Recommended chunk size
278 pub fn recommend_chunk_size(
279 total_params: usize,
280 available_memory_bytes: usize,
281 dtype_size: usize,
282 optimizer_state_multiplier: usize,
283 ) -> usize {
284 let memory_per_param = dtype_size * optimizer_state_multiplier;
285 let max_params = available_memory_bytes / memory_per_param;
286
287 // Use 80% of available memory to leave headroom
288 let safe_params = (max_params * 80) / 100;
289
290 safe_params.min(total_params).max(1024)
291 }
292
293 /// Get recommended accumulation steps for given batch size
294 ///
295 /// # Arguments
296 ///
297 /// * `target_batch_size` - Desired effective batch size
298 /// * `max_micro_batch_size` - Maximum micro-batch that fits in memory
299 ///
300 /// # Returns
301 ///
302 /// Number of gradient accumulation steps
303 pub fn recommend_accumulation_steps(
304 target_batch_size: usize,
305 max_micro_batch_size: usize,
306 ) -> usize {
307 target_batch_size.div_ceil(max_micro_batch_size)
308 }
309
310 /// Estimate peak memory usage during training
311 ///
312 /// # Arguments
313 ///
314 /// * `num_params` - Number of parameters
315 /// * `batch_size` - Batch size
316 /// * `sequence_length` - Sequence length (for transformers, 1 otherwise)
317 /// * `dtype_size` - Size of data type in bytes
318 /// * `optimizer_type` - Type of optimizer ("sgd", "adam", etc.)
319 ///
320 /// # Returns
321 ///
322 /// Estimated peak memory in bytes
323 pub fn estimate_peak_memory(
324 num_params: usize,
325 batch_size: usize,
326 sequence_length: usize,
327 dtype_size: usize,
328 optimizer_type: &str,
329 ) -> usize {
330 // Model parameters
331 let param_memory = num_params * dtype_size;
332
333 // Gradients
334 let grad_memory = num_params * dtype_size;
335
336 // Optimizer state
337 let optimizer_memory = match optimizer_type {
338 "sgd" => num_params * dtype_size,
339 "adam" | "adamw" => num_params * dtype_size * 2,
340 _ => num_params * dtype_size,
341 };
342
343 // Activations (rough estimate: batch_size * sequence_length * hidden_dim)
344 let hidden_dim = (num_params as f64).sqrt() as usize;
345 let activation_memory = batch_size * sequence_length * hidden_dim * dtype_size;
346
347 param_memory + grad_memory + optimizer_memory + activation_memory
348 }
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::optimizers::SGD;
355 use approx::assert_relative_eq;
356
357 #[test]
358 fn test_gradient_accumulator() {
359 let mut accumulator = GradientAccumulator::<f32>::new(100);
360
361 // Accumulate some gradients
362 let grad1 = Array1::from_elem(100, 1.0);
363 let grad2 = Array1::from_elem(100, 2.0);
364
365 accumulator
366 .accumulate(&grad1.view())
367 .expect("unwrap failed");
368 accumulator
369 .accumulate(&grad2.view())
370 .expect("unwrap failed");
371
372 assert_eq!(accumulator.count(), 2);
373 assert!(accumulator.is_ready(2));
374
375 // Get average
376 let avg = accumulator.average().expect("unwrap failed");
377 assert_relative_eq!(avg[0], 1.5, epsilon = 1e-6);
378
379 // After average, accumulator should be reset
380 assert_eq!(accumulator.count(), 0);
381 }
382
383 #[test]
384 fn test_chunked_optimizer() {
385 let optimizer = SGD::new(0.01);
386 let mut chunked_opt = ChunkedOptimizer::new(optimizer, Some(10));
387
388 let params = Array1::from_vec((0..25).map(|i| i as f32).collect());
389 let gradients = Array1::from_elem(25, 0.1);
390
391 let updated = chunked_opt
392 .step_chunked(¶ms, &gradients)
393 .expect("unwrap failed");
394
395 // Verify updates
396 assert_eq!(updated.len(), 25);
397 assert_relative_eq!(updated[0], 0.0 - 0.01 * 0.1, epsilon = 1e-6);
398
399 // Check number of chunks
400 assert_eq!(chunked_opt.num_chunks(25), 3);
401 }
402
403 #[test]
404 fn test_memory_estimator_sgd() {
405 // SGD for 1M parameters (f32)
406 let mem = MemoryUsageEstimator::sgd(1_000_000, 4);
407 assert_eq!(mem, 8_000_000); // 8 MB
408
409 // SGD with momentum
410 let mem = MemoryUsageEstimator::sgd_with_momentum(1_000_000, 4);
411 assert_eq!(mem, 12_000_000); // 12 MB
412 }
413
414 #[test]
415 fn test_memory_estimator_adam() {
416 // Adam for 1M parameters (f32)
417 let mem = MemoryUsageEstimator::adam(1_000_000, 4);
418 assert_eq!(mem, 16_000_000); // 16 MB
419 }
420
421 #[test]
422 fn test_recommend_chunk_size() {
423 // 1GB available, f32, Adam optimizer
424 let chunk_size = MemoryUsageEstimator::recommend_chunk_size(
425 100_000_000, // 100M total params
426 1_000_000_000, // 1GB available
427 4, // f32
428 4, // Adam state multiplier
429 );
430
431 // Should be around 50M params (80% of 62.5M that fits in 1GB)
432 assert!(chunk_size > 40_000_000);
433 assert!(chunk_size < 60_000_000);
434 }
435
436 #[test]
437 fn test_recommend_accumulation_steps() {
438 let steps = MemoryUsageEstimator::recommend_accumulation_steps(128, 32);
439 assert_eq!(steps, 4);
440
441 let steps = MemoryUsageEstimator::recommend_accumulation_steps(100, 32);
442 assert_eq!(steps, 4); // Rounds up
443 }
444
445 #[test]
446 fn test_estimate_peak_memory() {
447 let peak = MemoryUsageEstimator::estimate_peak_memory(
448 10_000_000, // 10M params
449 32, // batch size
450 512, // sequence length
451 4, // f32
452 "adam",
453 );
454
455 // Should be substantial (model + optimizer + activations)
456 assert!(peak > 100_000_000); // > 100MB
457 }
458}