torsh_cli/commands/model/
profiling.rs1#![allow(dead_code)]
12use anyhow::Result;
13use std::time::Instant;
14use tracing::{debug, info};
15
16use torsh::core::device::DeviceType;
17use torsh::tensor::Tensor;
18
19use super::types::{LayerInfo, TorshModel};
20
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23pub struct LayerProfile {
24 pub layer_name: String,
25 pub layer_type: String,
26 pub forward_time_ms: f64,
27 pub backward_time_ms: f64,
28 pub memory_allocated_mb: f64,
29 pub memory_peak_mb: f64,
30 pub flops: u64,
31 pub utilization_percent: f64,
32}
33
34#[derive(Debug, serde::Serialize, serde::Deserialize)]
36pub struct ModelProfile {
37 pub model_name: String,
38 pub total_inference_time_ms: f64,
39 pub total_memory_mb: f64,
40 pub peak_memory_mb: f64,
41 pub throughput_samples_per_sec: f64,
42 pub layer_profiles: Vec<LayerProfile>,
43 pub bottlenecks: Vec<String>,
44 pub recommendations: Vec<String>,
45}
46
47#[derive(Debug, Clone)]
49pub struct ProfilingConfig {
50 pub num_warmup_iterations: usize,
51 pub num_benchmark_iterations: usize,
52 pub batch_size: usize,
53 pub profile_memory: bool,
54 pub profile_layers: bool,
55 pub identify_bottlenecks: bool,
56}
57
58impl Default for ProfilingConfig {
59 fn default() -> Self {
60 Self {
61 num_warmup_iterations: 10,
62 num_benchmark_iterations: 100,
63 batch_size: 1,
64 profile_memory: true,
65 profile_layers: true,
66 identify_bottlenecks: true,
67 }
68 }
69}
70
71pub async fn profile_model(model: &TorshModel, config: &ProfilingConfig) -> Result<ModelProfile> {
73 info!(
74 "Profiling model with {} iterations (warmup: {})",
75 config.num_benchmark_iterations, config.num_warmup_iterations
76 );
77
78 debug!("Running warmup iterations");
80 for _ in 0..config.num_warmup_iterations {
81 run_forward_pass(model)?;
82 }
83
84 let mut inference_times = Vec::new();
86 let mut memory_usage = Vec::new();
87
88 for i in 0..config.num_benchmark_iterations {
89 let start = Instant::now();
90 let mem_before = current_process_memory_mb();
91
92 run_forward_pass(model)?;
93
94 let duration = start.elapsed();
95 let mem_after = current_process_memory_mb();
96
97 inference_times.push(duration.as_secs_f64() * 1000.0);
98 memory_usage.push(mem_after - mem_before);
99
100 if i % 10 == 0 {
101 debug!(
102 "Completed {} / {} iterations",
103 i, config.num_benchmark_iterations
104 );
105 }
106 }
107
108 let total_time: f64 = inference_times.iter().sum();
110 let avg_time = total_time / inference_times.len() as f64;
111 let throughput = 1000.0 / avg_time * config.batch_size as f64;
112
113 let avg_memory: f64 = memory_usage.iter().sum::<f64>() / memory_usage.len() as f64;
114 let peak_memory = memory_usage.iter().cloned().fold(0.0f64, f64::max);
115
116 let layer_profiles = if config.profile_layers {
118 profile_layers(model)?
119 } else {
120 Vec::new()
121 };
122
123 let bottlenecks = if config.identify_bottlenecks {
125 identify_bottlenecks(&layer_profiles)
126 } else {
127 Vec::new()
128 };
129
130 let recommendations = generate_recommendations(model, &layer_profiles, avg_time, avg_memory);
132
133 Ok(ModelProfile {
134 model_name: model
135 .metadata
136 .description
137 .clone()
138 .unwrap_or_else(|| "Unknown".to_string()),
139 total_inference_time_ms: avg_time,
140 total_memory_mb: avg_memory,
141 peak_memory_mb: peak_memory,
142 throughput_samples_per_sec: throughput,
143 layer_profiles,
144 bottlenecks,
145 recommendations,
146 })
147}
148
149fn profile_layers(model: &TorshModel) -> Result<Vec<LayerProfile>> {
151 debug!("Profiling individual layers");
152
153 let mut profiles = Vec::new();
154
155 for layer in &model.layers {
156 let profile = profile_single_layer(layer)?;
157 profiles.push(profile);
158 }
159
160 Ok(profiles)
161}
162
163fn profile_single_layer(layer: &LayerInfo) -> Result<LayerProfile> {
165 let forward_time = estimate_layer_time(layer);
167 let backward_time = forward_time * 2.0; let memory_allocated = estimate_layer_memory(layer);
170 let memory_peak = memory_allocated * 1.5;
171
172 let flops = super::types::estimate_flops(layer);
173
174 let utilization = match layer.layer_type.as_str() {
176 "Linear" | "Conv2d" => 85.0, "BatchNorm" | "LayerNorm" => 60.0, "ReLU" | "GELU" => 95.0, _ => 70.0,
180 };
181
182 Ok(LayerProfile {
183 layer_name: layer.name.clone(),
184 layer_type: layer.layer_type.clone(),
185 forward_time_ms: forward_time,
186 backward_time_ms: backward_time,
187 memory_allocated_mb: memory_allocated,
188 memory_peak_mb: memory_peak,
189 flops,
190 utilization_percent: utilization,
191 })
192}
193
194fn estimate_layer_time(layer: &LayerInfo) -> f64 {
196 let flops = super::types::estimate_flops(layer);
197
198 let gflops_capacity = 100.0;
200 let time_ms = (flops as f64 / (gflops_capacity * 1e9)) * 1000.0;
201
202 let overhead = match layer.layer_type.as_str() {
204 "Attention" => 2.0, "Conv2d" => 1.5,
206 _ => 1.0,
207 };
208
209 time_ms * overhead
210}
211
212fn estimate_layer_memory(layer: &LayerInfo) -> f64 {
214 let param_memory = (layer.parameters * 4) as f64 / (1024.0 * 1024.0); let input_size: usize = layer.input_shape.iter().product();
217 let output_size: usize = layer.output_shape.iter().product();
218
219 let activation_memory = ((input_size + output_size) * 4) as f64 / (1024.0 * 1024.0);
220
221 param_memory + activation_memory
222}
223
224fn identify_bottlenecks(layer_profiles: &[LayerProfile]) -> Vec<String> {
226 let mut bottlenecks = Vec::new();
227
228 if layer_profiles.is_empty() {
229 return bottlenecks;
230 }
231
232 let total_time: f64 = layer_profiles.iter().map(|p| p.forward_time_ms).sum();
234 let threshold = total_time * 0.15; for profile in layer_profiles {
237 if profile.forward_time_ms > threshold {
238 bottlenecks.push(format!(
239 "Layer '{}' ({}) takes {:.2}ms ({:.1}% of total time)",
240 profile.layer_name,
241 profile.layer_type,
242 profile.forward_time_ms,
243 (profile.forward_time_ms / total_time) * 100.0
244 ));
245 }
246
247 if profile.utilization_percent < 50.0 {
249 bottlenecks.push(format!(
250 "Layer '{}' has low GPU utilization: {:.1}%",
251 profile.layer_name, profile.utilization_percent
252 ));
253 }
254 }
255
256 let max_memory: f64 = layer_profiles
258 .iter()
259 .map(|p| p.memory_peak_mb)
260 .fold(0.0, f64::max);
261 if max_memory > 1000.0 {
262 bottlenecks.push(format!(
264 "High memory usage detected: {:.1} MB peak",
265 max_memory
266 ));
267 }
268
269 bottlenecks
270}
271
272fn generate_recommendations(
274 model: &TorshModel,
275 layer_profiles: &[LayerProfile],
276 avg_time_ms: f64,
277 avg_memory_mb: f64,
278) -> Vec<String> {
279 let mut recommendations = Vec::new();
280
281 if avg_memory_mb > 100.0 {
283 recommendations
284 .push("Consider INT8 quantization to reduce memory usage by ~75%".to_string());
285 }
286
287 if avg_time_ms < 1.0 {
289 recommendations.push(
290 "Inference time is very short. Consider increasing batch size for better throughput"
291 .to_string(),
292 );
293 }
294
295 let total_params: u64 = model.layers.iter().map(|l| l.parameters).sum();
297 if total_params > 1_000_000 {
298 recommendations.push(
299 "Model has >1M parameters. Consider pruning to reduce size and improve speed"
300 .to_string(),
301 );
302 }
303
304 for profile in layer_profiles {
306 if profile.layer_type == "Attention" && profile.forward_time_ms > avg_time_ms * 0.3 {
307 recommendations.push(format!(
308 "Attention layer '{}' is expensive. Consider Flash Attention or multi-query attention",
309 profile.layer_name
310 ));
311 }
312
313 if profile.layer_type == "Linear" && profile.memory_allocated_mb > 50.0 {
314 recommendations.push(format!(
315 "Large linear layer '{}'. Consider low-rank factorization (LoRA)",
316 profile.layer_name
317 ));
318 }
319 }
320
321 if model.layers.len() > 10 {
323 recommendations
324 .push("Enable JIT compilation for operator fusion and optimization".to_string());
325 }
326
327 recommendations
328}
329
330fn run_forward_pass(model: &TorshModel) -> Result<()> {
338 let input_width = model
340 .layers
341 .first()
342 .and_then(|l| l.input_shape.first().copied())
343 .unwrap_or(1);
344
345 let mut activation = Tensor::ones(&[1, input_width.max(1)], DeviceType::Cpu)?;
348
349 for layer in &model.layers {
350 activation = forward_layer(&activation, layer)?;
351 }
352
353 let _ = activation.shape();
355 Ok(())
356}
357
358fn forward_layer(input: &Tensor<f32>, layer: &LayerInfo) -> Result<Tensor<f32>> {
360 let in_features = layer.input_shape.first().copied().unwrap_or(1).max(1);
361 let out_features = layer.output_shape.first().copied().unwrap_or(1).max(1);
362
363 match layer.layer_type.as_str() {
364 "Linear" | "Dense" => {
365 let weight = Tensor::ones(&[in_features, out_features], DeviceType::Cpu)?;
367 let bias = Tensor::zeros(&[1, out_features], DeviceType::Cpu)?;
368 let projected = input.matmul(&weight)?;
369 Ok(projected.add(&bias)?)
370 }
371 "ReLU" => Ok(input.relu()?),
372 "Sigmoid" => Ok(input.sigmoid()?),
373 "Tanh" => Ok(input.tanh()?),
374 _ => {
375 if in_features == out_features {
380 Ok(input.clone())
381 } else {
382 let weight = Tensor::ones(&[in_features, out_features], DeviceType::Cpu)?;
383 Ok(input.matmul(&weight)?)
384 }
385 }
386 }
387}
388
389fn current_process_memory_mb() -> f64 {
395 use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System};
396
397 let Ok(pid) = sysinfo::get_current_pid() else {
398 return 0.0;
399 };
400
401 let mut system = System::new();
402 system.refresh_processes_specifics(
403 ProcessesToUpdate::Some(&[pid]),
404 true,
405 ProcessRefreshKind::nothing().with_memory(),
406 );
407
408 match system.process(pid) {
409 Some(process) => process.memory() as f64 / (1024.0 * 1024.0),
411 None => 0.0,
412 }
413}
414
415pub fn generate_profiling_report(profile: &ModelProfile) -> String {
417 let mut report = String::new();
418
419 report.push_str(&format!(
420 "# Model Profiling Report: {}\n\n",
421 profile.model_name
422 ));
423
424 report.push_str("## Summary\n\n");
425 report.push_str(&format!(
426 "- **Average Inference Time**: {:.2} ms\n",
427 profile.total_inference_time_ms
428 ));
429 report.push_str(&format!(
430 "- **Throughput**: {:.1} samples/sec\n",
431 profile.throughput_samples_per_sec
432 ));
433 report.push_str(&format!(
434 "- **Memory Usage**: {:.1} MB (peak: {:.1} MB)\n\n",
435 profile.total_memory_mb, profile.peak_memory_mb
436 ));
437
438 if !profile.layer_profiles.is_empty() {
439 report.push_str("## Layer-wise Performance\n\n");
440 report.push_str("| Layer | Type | Forward (ms) | Memory (MB) | FLOPs | Utilization |\n");
441 report.push_str("|-------|------|-------------|-------------|-------|-------------|\n");
442
443 for layer in &profile.layer_profiles {
444 report.push_str(&format!(
445 "| {} | {} | {:.3} | {:.1} | {} | {:.1}% |\n",
446 layer.layer_name,
447 layer.layer_type,
448 layer.forward_time_ms,
449 layer.memory_allocated_mb,
450 format_flops(layer.flops),
451 layer.utilization_percent
452 ));
453 }
454 report.push_str("\n");
455 }
456
457 if !profile.bottlenecks.is_empty() {
458 report.push_str("## Bottlenecks Identified\n\n");
459 for bottleneck in &profile.bottlenecks {
460 report.push_str(&format!("- {}\n", bottleneck));
461 }
462 report.push_str("\n");
463 }
464
465 if !profile.recommendations.is_empty() {
466 report.push_str("## Optimization Recommendations\n\n");
467 for (i, rec) in profile.recommendations.iter().enumerate() {
468 report.push_str(&format!("{}. {}\n", i + 1, rec));
469 }
470 report.push_str("\n");
471 }
472
473 report
474}
475
476fn format_flops(flops: u64) -> String {
478 if flops >= 1_000_000_000 {
479 format!("{:.1}G", flops as f64 / 1_000_000_000.0)
480 } else if flops >= 1_000_000 {
481 format!("{:.1}M", flops as f64 / 1_000_000.0)
482 } else if flops >= 1_000 {
483 format!("{:.1}K", flops as f64 / 1_000.0)
484 } else {
485 format!("{}", flops)
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::commands::model::serialization::create_sample_model;
493
494 #[tokio::test]
495 async fn test_model_profiling() {
496 let model = create_sample_model("test_model", 3);
497 let config = ProfilingConfig::default();
498
499 let profile = profile_model(&model, &config)
500 .await
501 .expect("operation should succeed");
502
503 assert!(profile.total_inference_time_ms > 0.0);
504 assert!(profile.throughput_samples_per_sec > 0.0);
505 assert!(!profile.layer_profiles.is_empty());
506 }
507
508 #[test]
509 fn test_bottleneck_identification() {
510 let profiles = vec![
511 LayerProfile {
512 layer_name: "slow_layer".to_string(),
513 layer_type: "Attention".to_string(),
514 forward_time_ms: 50.0,
515 backward_time_ms: 100.0,
516 memory_allocated_mb: 100.0,
517 memory_peak_mb: 150.0,
518 flops: 1_000_000,
519 utilization_percent: 40.0,
520 },
521 LayerProfile {
522 layer_name: "fast_layer".to_string(),
523 layer_type: "ReLU".to_string(),
524 forward_time_ms: 1.0,
525 backward_time_ms: 2.0,
526 memory_allocated_mb: 10.0,
527 memory_peak_mb: 15.0,
528 flops: 100_000,
529 utilization_percent: 95.0,
530 },
531 ];
532
533 let bottlenecks = identify_bottlenecks(&profiles);
534 assert!(!bottlenecks.is_empty());
535 }
536
537 #[test]
538 fn test_report_generation() {
539 let model = create_sample_model("test", 2);
540 let layer_profiles = profile_layers(&model).expect("profile layers should succeed");
541
542 let profile = ModelProfile {
543 model_name: "test_model".to_string(),
544 total_inference_time_ms: 10.5,
545 total_memory_mb: 55.3,
546 peak_memory_mb: 75.0,
547 throughput_samples_per_sec: 95.2,
548 layer_profiles,
549 bottlenecks: vec!["Test bottleneck".to_string()],
550 recommendations: vec!["Test recommendation".to_string()],
551 };
552
553 let report = generate_profiling_report(&profile);
554 assert!(report.contains("Model Profiling Report"));
555 assert!(report.contains("Summary"));
556 assert!(report.contains("Bottlenecks"));
557 }
558}