1use crate::{Optimizer, OptimizerResult};
8use std::time::{Duration, Instant};
9use torsh_core::device::DeviceType;
10use torsh_tensor::{creation, Tensor};
11
12#[derive(Debug, Clone)]
14#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
15pub struct StatisticalAnalysis {
16 pub mean_time: Duration,
18 pub median_time: Duration,
20 pub std_dev: Duration,
22 pub confidence_interval: (Duration, Duration),
24 pub effect_size: Option<f64>,
26 pub p_value: Option<f64>,
28}
29
30#[derive(Debug, Clone)]
35pub struct BenchmarkConfig {
36 pub num_iterations: usize,
38 pub warmup_iterations: usize,
40 pub max_time_seconds: f32,
42 pub device: DeviceType,
44 pub profile_memory: bool,
46}
47
48impl Default for BenchmarkConfig {
49 fn default() -> Self {
50 Self {
51 num_iterations: 1000,
52 warmup_iterations: 100,
53 max_time_seconds: 60.0,
54 device: DeviceType::Cpu,
55 profile_memory: false,
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
65#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
66pub struct BenchmarkResult {
67 pub name: String,
69 pub iterations_completed: usize,
71 pub total_time: Duration,
73 pub avg_time_per_iteration: Duration,
75 pub min_time_per_iteration: Duration,
77 pub max_time_per_iteration: Duration,
79 pub time_std_dev: Duration,
81 pub final_loss: Option<f32>,
83 pub memory_stats: Option<MemoryStats>,
85 pub convergence_rate: Option<f32>,
87}
88
89#[derive(Debug, Clone)]
91#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
92pub struct MemoryStats {
93 pub peak_memory_bytes: usize,
95 pub initial_memory_bytes: usize,
97 pub final_memory_bytes: usize,
99 pub avg_memory_bytes: usize,
101}
102
103pub struct OptimizerBenchmarks {
108 config: BenchmarkConfig,
109}
110
111impl OptimizerBenchmarks {
112 pub fn new() -> Self {
114 Self {
115 config: BenchmarkConfig::default(),
116 }
117 }
118
119 pub fn with_config(config: BenchmarkConfig) -> Self {
121 Self { config }
122 }
123
124 pub fn config(&self) -> &BenchmarkConfig {
126 &self.config
127 }
128
129 pub fn set_config(&mut self, config: BenchmarkConfig) {
131 self.config = config;
132 }
133
134 fn estimate_memory_usage<O: Optimizer>(params: &[Tensor], optimizer: &O) -> usize {
136 let mut total_bytes = 0;
137
138 for param in params {
140 let shape = param.shape();
141 let element_count = shape.dims().iter().product::<usize>();
142 total_bytes += element_count * 4;
144
145 if param.has_grad() {
147 total_bytes += element_count * 4;
148 }
149 }
150
151 let state_multiplier = match optimizer.get_lr().len() {
154 1 => 1.2,
156 _ => 3.0,
158 };
159
160 let optimizer_state_bytes = (total_bytes as f64 * state_multiplier) as usize;
161 total_bytes + optimizer_state_bytes
162 }
163
164 pub fn benchmark_step_performance<O: Optimizer>(
178 &self,
179 mut optimizer: O,
180 problem_size: usize,
181 ) -> OptimizerResult<BenchmarkResult> {
182 let mut params = creation::randn::<f32>(&[problem_size])?;
183 let mut iteration_times = Vec::new();
184
185 for _ in 0..self.config.warmup_iterations {
187 let grads = creation::randn::<f32>(&[problem_size])?;
188 params.set_grad(Some(grads));
189 optimizer.step()?;
190 }
191
192 let start_time = Instant::now();
193 let mut iterations_completed = 0;
194
195 for _i in 0..self.config.num_iterations {
197 if start_time.elapsed().as_secs_f32() > self.config.max_time_seconds {
199 break;
200 }
201
202 let grads = creation::randn::<f32>(&[problem_size])?;
203 params.set_grad(Some(grads));
204
205 let iter_start = Instant::now();
206 optimizer.step()?;
207 let iter_time = iter_start.elapsed();
208
209 iteration_times.push(iter_time);
210 iterations_completed += 1;
211 }
212
213 let total_time = start_time.elapsed();
214
215 let avg_time = total_time / iterations_completed as u32;
217 let min_time = iteration_times
218 .iter()
219 .min()
220 .copied()
221 .unwrap_or(Duration::ZERO);
222 let max_time = iteration_times
223 .iter()
224 .max()
225 .copied()
226 .unwrap_or(Duration::ZERO);
227
228 let mean_nanos = avg_time.as_nanos() as f64;
230 let variance = iteration_times
231 .iter()
232 .map(|t| (t.as_nanos() as f64 - mean_nanos).powi(2))
233 .sum::<f64>()
234 / iterations_completed as f64;
235 let std_dev = Duration::from_nanos(variance.sqrt() as u64);
236
237 Ok(BenchmarkResult {
238 name: format!("step_performance_size_{}", problem_size),
239 iterations_completed,
240 total_time,
241 avg_time_per_iteration: avg_time,
242 min_time_per_iteration: min_time,
243 max_time_per_iteration: max_time,
244 time_std_dev: std_dev,
245 final_loss: None,
246 memory_stats: None,
247 convergence_rate: None,
248 })
249 }
250
251 pub fn benchmark_quadratic_convergence<O: Optimizer>(
265 &self,
266 mut optimizer: O,
267 dimension: usize,
268 ) -> OptimizerResult<BenchmarkResult> {
269 let mut params = creation::randn::<f32>(&[dimension])?;
270 let target = creation::zeros::<f32>(&[dimension])?;
271
272 let mut losses = Vec::new();
273 let mut iteration_times = Vec::new();
274
275 let initial_loss = params.sub(&target)?.pow(2.0)?.sum()?.item()?;
277 losses.push(initial_loss);
278
279 let start_time = Instant::now();
280 let mut iterations_completed = 0;
281
282 for _i in 0..self.config.num_iterations {
283 if start_time.elapsed().as_secs_f32() > self.config.max_time_seconds {
285 break;
286 }
287
288 let grads = params.sub(&target)?.mul_scalar(2.0)?;
290
291 let iter_start = Instant::now();
292 params.set_grad(Some(grads));
293 optimizer.step()?;
294 let iter_time = iter_start.elapsed();
295
296 iteration_times.push(iter_time);
297
298 let loss = params.sub(&target)?.pow(2.0)?.sum()?.item()?;
300 losses.push(loss);
301
302 iterations_completed += 1;
303
304 if loss < 1e-8 {
306 break;
307 }
308 }
309
310 let total_time = start_time.elapsed();
311 let final_loss = losses.last().copied().unwrap_or(f32::INFINITY);
312
313 let convergence_rate = if losses.len() > 1 {
315 let log_reduction = (initial_loss.ln() - final_loss.ln()).max(0.0);
316 Some(log_reduction / iterations_completed as f32)
317 } else {
318 None
319 };
320
321 let avg_time = total_time / iterations_completed as u32;
323 let min_time = iteration_times
324 .iter()
325 .min()
326 .copied()
327 .unwrap_or(Duration::ZERO);
328 let max_time = iteration_times
329 .iter()
330 .max()
331 .copied()
332 .unwrap_or(Duration::ZERO);
333
334 let mean_nanos = avg_time.as_nanos() as f64;
335 let variance = iteration_times
336 .iter()
337 .map(|t| (t.as_nanos() as f64 - mean_nanos).powi(2))
338 .sum::<f64>()
339 / iterations_completed as f64;
340 let std_dev = Duration::from_nanos(variance.sqrt() as u64);
341
342 Ok(BenchmarkResult {
343 name: format!("quadratic_convergence_dim_{}", dimension),
344 iterations_completed,
345 total_time,
346 avg_time_per_iteration: avg_time,
347 min_time_per_iteration: min_time,
348 max_time_per_iteration: max_time,
349 time_std_dev: std_dev,
350 final_loss: Some(final_loss),
351 memory_stats: None,
352 convergence_rate,
353 })
354 }
355
356 pub fn benchmark_sparse_gradients<O: Optimizer>(
371 &self,
372 mut optimizer: O,
373 total_params: usize,
374 sparsity: f32,
375 ) -> OptimizerResult<BenchmarkResult> {
376 let mut params = creation::randn::<f32>(&[total_params])?;
377
378 let mut iteration_times = Vec::new();
379
380 for _ in 0..self.config.warmup_iterations {
382 let mut grads = creation::zeros::<f32>(&[total_params])?;
383
384 for i in 0..total_params {
386 if (i as f32 / total_params as f32) < sparsity {
387 let grad_val = ((i as f32 * 0.1) % 2.0) - 1.0;
388 grads.set(&[i], grad_val)?;
389 }
390 }
391
392 params.set_grad(Some(grads));
393 optimizer.step()?;
394 }
395
396 let start_time = Instant::now();
397 let mut iterations_completed = 0;
398
399 for _ in 0..self.config.num_iterations {
400 if start_time.elapsed().as_secs_f32() > self.config.max_time_seconds {
401 break;
402 }
403
404 let mut grads = creation::zeros::<f32>(&[total_params])?;
405
406 for i in 0..total_params {
408 if (i as f32 / total_params as f32) < sparsity {
409 let grad_val = ((i as f32 * 0.1) % 2.0) - 1.0;
410 grads.set(&[i], grad_val)?;
411 }
412 }
413
414 let iter_start = Instant::now();
415 params.set_grad(Some(grads));
416 optimizer.step()?;
417 let iter_time = iter_start.elapsed();
418
419 iteration_times.push(iter_time);
420 iterations_completed += 1;
421 }
422
423 let total_time = start_time.elapsed();
424 let avg_time = total_time / iterations_completed as u32;
425 let min_time = iteration_times
426 .iter()
427 .min()
428 .copied()
429 .unwrap_or(Duration::ZERO);
430 let max_time = iteration_times
431 .iter()
432 .max()
433 .copied()
434 .unwrap_or(Duration::ZERO);
435
436 let mean_nanos = avg_time.as_nanos() as f64;
437 let variance = iteration_times
438 .iter()
439 .map(|t| (t.as_nanos() as f64 - mean_nanos).powi(2))
440 .sum::<f64>()
441 / iterations_completed as f64;
442 let std_dev = Duration::from_nanos(variance.sqrt() as u64);
443
444 Ok(BenchmarkResult {
445 name: format!(
446 "sparse_gradients_params_{}_sparsity_{:.2}",
447 total_params, sparsity
448 ),
449 iterations_completed,
450 total_time,
451 avg_time_per_iteration: avg_time,
452 min_time_per_iteration: min_time,
453 max_time_per_iteration: max_time,
454 time_std_dev: std_dev,
455 final_loss: None,
456 memory_stats: None,
457 convergence_rate: None,
458 })
459 }
460
461 pub fn run_comprehensive_benchmarks<O: Optimizer + Clone>(
475 &self,
476 optimizer: O,
477 ) -> OptimizerResult<Vec<BenchmarkResult>> {
478 let mut results = Vec::new();
479
480 for &size in &[100, 1000, 10000] {
482 results.push(self.benchmark_step_performance(optimizer.clone(), size)?);
483 }
484
485 for &dim in &[10, 100, 1000] {
487 results.push(self.benchmark_quadratic_convergence(optimizer.clone(), dim)?);
488 }
489
490 for &sparsity in &[0.1, 0.01, 0.001] {
492 results.push(self.benchmark_sparse_gradients(optimizer.clone(), 10000, sparsity)?);
493 }
494
495 Ok(results)
496 }
497
498 pub fn print_results(&self, results: &[BenchmarkResult]) {
503 println!("\n{:=<100}", "");
504 println!("{:^100}", "OPTIMIZER BENCHMARK RESULTS");
505 println!("{:=<100}", "");
506
507 println!(
508 "{:<40} {:>12} {:>12} {:>12} {:>12} {:>10}",
509 "Benchmark", "Iterations", "Total Time", "Avg Time", "Min Time", "Max Time"
510 );
511 println!("{:-<100}", "");
512
513 for result in results {
514 println!(
515 "{:<40} {:>12} {:>12.3?} {:>12.3?} {:>12.3?} {:>12.3?}",
516 result.name,
517 result.iterations_completed,
518 result.total_time,
519 result.avg_time_per_iteration,
520 result.min_time_per_iteration,
521 result.max_time_per_iteration
522 );
523
524 if let Some(loss) = result.final_loss {
525 println!("{:<40} Final Loss: {:.6e}", "", loss);
526 }
527
528 if let Some(rate) = result.convergence_rate {
529 println!("{:<40} Convergence Rate: {:.6e}", "", rate);
530 }
531 }
532
533 println!("{:=<100}", "");
534 }
535}
536
537impl Default for OptimizerBenchmarks {
538 fn default() -> Self {
539 Self::new()
540 }
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn test_benchmark_config_default() {
549 let config = BenchmarkConfig::default();
550 assert_eq!(config.num_iterations, 1000);
551 assert_eq!(config.warmup_iterations, 100);
552 assert_eq!(config.max_time_seconds, 60.0);
553 }
554
555 #[test]
556 fn test_benchmark_result_creation() {
557 let result = BenchmarkResult {
558 name: "test_benchmark".to_string(),
559 iterations_completed: 100,
560 total_time: Duration::from_secs(1),
561 avg_time_per_iteration: Duration::from_millis(10),
562 min_time_per_iteration: Duration::from_millis(5),
563 max_time_per_iteration: Duration::from_millis(15),
564 time_std_dev: Duration::from_millis(2),
565 final_loss: Some(0.1),
566 memory_stats: None,
567 convergence_rate: Some(0.05),
568 };
569
570 assert_eq!(result.name, "test_benchmark");
571 assert_eq!(result.iterations_completed, 100);
572 assert_eq!(result.final_loss, Some(0.1));
573 }
574
575 #[test]
576 fn test_memory_stats() {
577 let stats = MemoryStats {
578 peak_memory_bytes: 1000,
579 initial_memory_bytes: 500,
580 final_memory_bytes: 800,
581 avg_memory_bytes: 750,
582 };
583
584 assert_eq!(stats.peak_memory_bytes, 1000);
585 assert_eq!(stats.avg_memory_bytes, 750);
586 }
587}