utils.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817
  1. """
  2. Utility classes for training progress monitoring, logging, and validation.
  3. This module provides comprehensive utilities for tracking training progress,
  4. professional logging, performance monitoring, and validation metrics.
  5. """
  6. import os
  7. import time
  8. import json
  9. import logging
  10. import threading
  11. from pathlib import Path
  12. from typing import Dict, List, Any, Optional, Callable, Union
  13. from dataclasses import dataclass, field
  14. from datetime import datetime, timedelta
  15. import sys
  16. import torch
  17. import torch.nn as nn
  18. import numpy as np
  19. from sklearn.metrics import (
  20. accuracy_score, precision_recall_fscore_support, confusion_matrix,
  21. classification_report, roc_auc_score, average_precision_score
  22. )
  23. import matplotlib.pyplot as plt
  24. import seaborn as sns
  25. try:
  26. from tensorboard import SummaryWriter
  27. TENSORBOARD_AVAILABLE = True
  28. except ImportError:
  29. TENSORBOARD_AVAILABLE = False
  30. SummaryWriter = None
  31. try:
  32. import wandb
  33. WANDB_AVAILABLE = True
  34. except ImportError:
  35. WANDB_AVAILABLE = False
  36. wandb = None
  37. class TrainerLogger:
  38. """
  39. Professional logging system for ML trainers.
  40. Provides structured logging with file output, console output,
  41. and optional integration with TensorBoard and Weights & Biases.
  42. """
  43. def __init__(self, name: str, log_dir: str = "./logs",
  44. log_level: str = "INFO",
  45. use_tensorboard: bool = True,
  46. use_wandb: bool = False,
  47. wandb_project: Optional[str] = None):
  48. """
  49. Initialize trainer logger.
  50. Args:
  51. name: Logger name
  52. log_dir: Directory for log files
  53. log_level: Logging level
  54. use_tensorboard: Whether to use TensorBoard logging
  55. use_wandb: Whether to use Weights & Biases logging
  56. wandb_project: W&B project name
  57. """
  58. self.name = name
  59. self.log_dir = Path(log_dir)
  60. self.log_dir.mkdir(parents=True, exist_ok=True)
  61. # Setup basic logger
  62. self.logger = self._setup_logger(log_level)
  63. # Setup TensorBoard
  64. self.tb_writer = None
  65. if use_tensorboard and TENSORBOARD_AVAILABLE:
  66. tb_log_dir = self.log_dir / "tensorboard" / name
  67. tb_log_dir.mkdir(parents=True, exist_ok=True)
  68. self.tb_writer = SummaryWriter(str(tb_log_dir))
  69. self.logger.info("TensorBoard logging enabled")
  70. elif use_tensorboard:
  71. self.logger.warning("TensorBoard not available - install with: pip install tensorboard")
  72. # Setup Weights & Biases
  73. self.use_wandb = False
  74. if use_wandb and WANDB_AVAILABLE:
  75. if wandb_project:
  76. wandb.init(project=wandb_project, name=name)
  77. self.use_wandb = True
  78. self.logger.info("Weights & Biases logging enabled")
  79. else:
  80. self.logger.warning("W&B project name required for W&B logging")
  81. elif use_wandb:
  82. self.logger.warning("Weights & Biases not available - install with: pip install wandb")
  83. # Training metrics storage
  84. self.metrics_history = []
  85. self.current_step = 0
  86. def _setup_logger(self, log_level: str) -> logging.Logger:
  87. """Setup the basic logger with file and console handlers."""
  88. logger = logging.getLogger(self.name)
  89. logger.setLevel(getattr(logging, log_level.upper()))
  90. # Clear existing handlers
  91. logger.handlers.clear()
  92. # Create formatters
  93. detailed_formatter = logging.Formatter(
  94. '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
  95. )
  96. simple_formatter = logging.Formatter(
  97. '%(asctime)s - %(levelname)s - %(message)s'
  98. )
  99. # File handler (detailed)
  100. log_file = self.log_dir / f"{self.name}.log"
  101. file_handler = logging.FileHandler(log_file)
  102. file_handler.setLevel(logging.DEBUG)
  103. file_handler.setFormatter(detailed_formatter)
  104. logger.addHandler(file_handler)
  105. # Console handler (simple)
  106. console_handler = logging.StreamHandler(sys.stdout)
  107. console_handler.setLevel(logging.INFO)
  108. console_handler.setFormatter(simple_formatter)
  109. logger.addHandler(console_handler)
  110. return logger
  111. def log_metrics(self, metrics: Dict[str, float], step: Optional[int] = None):
  112. """Log training metrics to all enabled loggers."""
  113. if step is None:
  114. step = self.current_step
  115. self.current_step += 1
  116. # Store metrics
  117. metric_entry = {"step": step, "timestamp": datetime.now().isoformat()}
  118. metric_entry.update(metrics)
  119. self.metrics_history.append(metric_entry)
  120. # Log to basic logger
  121. metric_str = ", ".join([f"{k}: {v:.6f}" for k, v in metrics.items()])
  122. self.logger.info(f"Step {step} - {metric_str}")
  123. # Log to TensorBoard
  124. if self.tb_writer:
  125. for key, value in metrics.items():
  126. self.tb_writer.add_scalar(key, value, step)
  127. self.tb_writer.flush()
  128. # Log to Weights & Biases
  129. if self.use_wandb:
  130. wandb.log(metrics, step=step)
  131. def log_model_graph(self, model: nn.Module, input_sample: torch.Tensor):
  132. """Log model graph to TensorBoard."""
  133. if self.tb_writer:
  134. try:
  135. self.tb_writer.add_graph(model, input_sample)
  136. self.logger.info("Model graph logged to TensorBoard")
  137. except Exception as e:
  138. self.logger.warning(f"Failed to log model graph: {str(e)}")
  139. def log_hyperparameters(self, hparams: Dict[str, Any]):
  140. """Log hyperparameters."""
  141. # Log to basic logger
  142. self.logger.info("Hyperparameters:")
  143. for key, value in hparams.items():
  144. self.logger.info(f" {key}: {value}")
  145. # Log to TensorBoard
  146. if self.tb_writer:
  147. # Convert non-scalar values to strings
  148. tb_hparams = {}
  149. for key, value in hparams.items():
  150. if isinstance(value, (int, float, str, bool)):
  151. tb_hparams[key] = value
  152. else:
  153. tb_hparams[key] = str(value)
  154. self.tb_writer.add_hparams(tb_hparams, {})
  155. # Log to Weights & Biases
  156. if self.use_wandb:
  157. wandb.config.update(hparams)
  158. def log_confusion_matrix(self, y_true: np.ndarray, y_pred: np.ndarray,
  159. class_names: List[str], step: int):
  160. """Log confusion matrix visualization."""
  161. cm = confusion_matrix(y_true, y_pred)
  162. # Create matplotlib figure
  163. fig, ax = plt.subplots(figsize=(10, 8))
  164. sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
  165. xticklabels=class_names, yticklabels=class_names, ax=ax)
  166. ax.set_xlabel('Predicted')
  167. ax.set_ylabel('Actual')
  168. ax.set_title('Confusion Matrix')
  169. # Log to TensorBoard
  170. if self.tb_writer:
  171. self.tb_writer.add_figure('confusion_matrix', fig, step)
  172. # Log to Weights & Biases
  173. if self.use_wandb:
  174. wandb.log({"confusion_matrix": wandb.Image(fig)}, step=step)
  175. plt.close(fig)
  176. def save_metrics_history(self, filepath: Optional[str] = None):
  177. """Save metrics history to JSON file."""
  178. if filepath is None:
  179. filepath = self.log_dir / f"{self.name}_metrics.json"
  180. with open(filepath, 'w') as f:
  181. json.dump(self.metrics_history, f, indent=2)
  182. self.logger.info(f"Metrics history saved to: {filepath}")
  183. def close(self):
  184. """Close all logging handlers."""
  185. if self.tb_writer:
  186. self.tb_writer.close()
  187. if self.use_wandb:
  188. wandb.finish()
  189. # Save metrics history
  190. self.save_metrics_history()
  191. class ProgressMonitor:
  192. """
  193. Real-time training progress monitor with ETA estimation.
  194. Provides progress tracking, ETA calculation, and performance monitoring
  195. during training with thread-safe updates.
  196. """
  197. def __init__(self, total_epochs: int, steps_per_epoch: int = None):
  198. """
  199. Initialize progress monitor.
  200. Args:
  201. total_epochs: Total number of training epochs
  202. steps_per_epoch: Number of steps per epoch (for step-level tracking)
  203. """
  204. self.total_epochs = total_epochs
  205. self.steps_per_epoch = steps_per_epoch
  206. self.total_steps = total_epochs * (steps_per_epoch or 1)
  207. # Progress tracking
  208. self.current_epoch = 0
  209. self.current_step = 0
  210. self.global_step = 0
  211. # Timing
  212. self.start_time = None
  213. self.epoch_start_time = None
  214. self.step_times = []
  215. self.epoch_times = []
  216. # Thread safety
  217. self.lock = threading.Lock()
  218. # Display settings
  219. self.update_interval = 1.0 # seconds
  220. self.last_update = 0
  221. def start_training(self):
  222. """Mark the start of training."""
  223. with self.lock:
  224. self.start_time = time.time()
  225. self.epoch_start_time = self.start_time
  226. def start_epoch(self, epoch: int):
  227. """Mark the start of an epoch."""
  228. with self.lock:
  229. self.current_epoch = epoch
  230. self.current_step = 0
  231. self.epoch_start_time = time.time()
  232. def update_step(self, step: int = None):
  233. """Update step progress."""
  234. with self.lock:
  235. if step is not None:
  236. self.current_step = step
  237. else:
  238. self.current_step += 1
  239. self.global_step += 1
  240. # Record step time
  241. current_time = time.time()
  242. if len(self.step_times) > 0:
  243. step_duration = current_time - self.step_times[-1]
  244. else:
  245. step_duration = current_time - self.epoch_start_time
  246. self.step_times.append(current_time)
  247. # Limit step times history
  248. if len(self.step_times) > 100:
  249. self.step_times = self.step_times[-100:]
  250. def end_epoch(self):
  251. """Mark the end of an epoch."""
  252. with self.lock:
  253. current_time = time.time()
  254. epoch_duration = current_time - self.epoch_start_time
  255. self.epoch_times.append(epoch_duration)
  256. # Limit epoch times history
  257. if len(self.epoch_times) > 50:
  258. self.epoch_times = self.epoch_times[-50:]
  259. def get_progress_info(self) -> Dict[str, Any]:
  260. """Get current progress information."""
  261. with self.lock:
  262. current_time = time.time()
  263. if self.start_time is None:
  264. return {"status": "not_started"}
  265. # Calculate progress
  266. epoch_progress = self.current_epoch / self.total_epochs
  267. if self.steps_per_epoch:
  268. step_progress = self.current_step / self.steps_per_epoch
  269. total_progress = (self.current_epoch + step_progress) / self.total_epochs
  270. else:
  271. total_progress = epoch_progress
  272. # Calculate timing
  273. elapsed_time = current_time - self.start_time
  274. # Estimate remaining time
  275. if total_progress > 0:
  276. eta = (elapsed_time / total_progress) * (1 - total_progress)
  277. else:
  278. eta = 0
  279. # Calculate average times
  280. avg_step_time = np.mean([
  281. self.step_times[i] - self.step_times[i-1]
  282. for i in range(1, len(self.step_times))
  283. ]) if len(self.step_times) > 1 else 0
  284. avg_epoch_time = np.mean(self.epoch_times) if self.epoch_times else 0
  285. return {
  286. "status": "running",
  287. "current_epoch": self.current_epoch,
  288. "total_epochs": self.total_epochs,
  289. "current_step": self.current_step,
  290. "steps_per_epoch": self.steps_per_epoch,
  291. "global_step": self.global_step,
  292. "total_steps": self.total_steps,
  293. "epoch_progress": epoch_progress,
  294. "total_progress": total_progress,
  295. "elapsed_time": elapsed_time,
  296. "eta": eta,
  297. "avg_step_time": avg_step_time,
  298. "avg_epoch_time": avg_epoch_time,
  299. "elapsed_str": str(timedelta(seconds=int(elapsed_time))),
  300. "eta_str": str(timedelta(seconds=int(eta))),
  301. }
  302. def print_progress(self, metrics: Dict[str, float] = None, force: bool = False):
  303. """Print progress information to console."""
  304. current_time = time.time()
  305. if not force and (current_time - self.last_update) < self.update_interval:
  306. return
  307. self.last_update = current_time
  308. info = self.get_progress_info()
  309. if info["status"] != "running":
  310. return
  311. # Format progress bar
  312. bar_length = 40
  313. filled_length = int(bar_length * info["total_progress"])
  314. bar = "█" * filled_length + "░" * (bar_length - filled_length)
  315. # Format output
  316. output = f"\rEpoch {info['current_epoch']}/{info['total_epochs']} "
  317. output += f"[{bar}] {info['total_progress']*100:.1f}% "
  318. output += f"- {info['elapsed_str']} / {info['eta_str']} "
  319. if metrics:
  320. metric_str = " - ".join([f"{k}: {v:.4f}" for k, v in metrics.items()])
  321. output += f"- {metric_str}"
  322. # Add step info if available
  323. if info["steps_per_epoch"]:
  324. output += f" - Step {info['current_step']}/{info['steps_per_epoch']}"
  325. print(output, end="", flush=True)
  326. class ValidationMetrics:
  327. """
  328. Comprehensive validation and testing metrics calculator.
  329. Provides calculation of various performance metrics for classification
  330. and regression tasks with support for multi-class and multi-label scenarios.
  331. """
  332. def __init__(self, task_type: str = "classification",
  333. num_classes: int = None,
  334. class_names: List[str] = None):
  335. """
  336. Initialize validation metrics calculator.
  337. Args:
  338. task_type: Type of task ('classification', 'regression', 'multilabel')
  339. num_classes: Number of classes for classification
  340. class_names: Names of classes
  341. """
  342. self.task_type = task_type
  343. self.num_classes = num_classes
  344. self.class_names = class_names or [f"Class_{i}" for i in range(num_classes or 2)]
  345. def calculate_classification_metrics(self, y_true: np.ndarray,
  346. y_pred: np.ndarray,
  347. y_prob: np.ndarray = None) -> Dict[str, Any]:
  348. """
  349. Calculate comprehensive classification metrics.
  350. Args:
  351. y_true: True labels
  352. y_pred: Predicted labels
  353. y_prob: Prediction probabilities (for AUC calculation)
  354. Returns:
  355. Dictionary containing various metrics
  356. """
  357. metrics = {}
  358. # Basic metrics
  359. metrics["accuracy"] = accuracy_score(y_true, y_pred)
  360. # Precision, recall, F1-score
  361. precision, recall, f1, support = precision_recall_fscore_support(
  362. y_true, y_pred, average=None, zero_division=0
  363. )
  364. # Per-class metrics
  365. for i, class_name in enumerate(self.class_names[:len(precision)]):
  366. metrics[f"precision_{class_name}"] = precision[i]
  367. metrics[f"recall_{class_name}"] = recall[i]
  368. metrics[f"f1_{class_name}"] = f1[i]
  369. metrics[f"support_{class_name}"] = support[i]
  370. # Averaged metrics
  371. metrics["precision_macro"] = np.mean(precision)
  372. metrics["recall_macro"] = np.mean(recall)
  373. metrics["f1_macro"] = np.mean(f1)
  374. # Weighted averages
  375. precision_weighted = np.average(precision, weights=support)
  376. recall_weighted = np.average(recall, weights=support)
  377. f1_weighted = np.average(f1, weights=support)
  378. metrics["precision_weighted"] = precision_weighted
  379. metrics["recall_weighted"] = recall_weighted
  380. metrics["f1_weighted"] = f1_weighted
  381. # Confusion matrix
  382. cm = confusion_matrix(y_true, y_pred)
  383. metrics["confusion_matrix"] = cm.tolist()
  384. # AUC metrics (if probabilities provided)
  385. if y_prob is not None:
  386. try:
  387. if len(np.unique(y_true)) == 2:
  388. # Binary classification
  389. metrics["auc_roc"] = roc_auc_score(y_true, y_prob[:, 1])
  390. metrics["auc_pr"] = average_precision_score(y_true, y_prob[:, 1])
  391. else:
  392. # Multi-class classification
  393. metrics["auc_roc_ovr"] = roc_auc_score(
  394. y_true, y_prob, multi_class='ovr', average='macro'
  395. )
  396. metrics["auc_roc_ovo"] = roc_auc_score(
  397. y_true, y_prob, multi_class='ovo', average='macro'
  398. )
  399. except Exception as e:
  400. # AUC calculation failed (e.g., only one class present)
  401. pass
  402. # Classification report
  403. report = classification_report(y_true, y_pred,
  404. target_names=self.class_names[:len(np.unique(y_true))],
  405. output_dict=True, zero_division=0)
  406. metrics["classification_report"] = report
  407. return metrics
  408. def calculate_regression_metrics(self, y_true: np.ndarray,
  409. y_pred: np.ndarray) -> Dict[str, float]:
  410. """
  411. Calculate regression metrics.
  412. Args:
  413. y_true: True values
  414. y_pred: Predicted values
  415. Returns:
  416. Dictionary containing regression metrics
  417. """
  418. from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
  419. metrics = {}
  420. # Basic metrics
  421. metrics["mse"] = mean_squared_error(y_true, y_pred)
  422. metrics["rmse"] = np.sqrt(metrics["mse"])
  423. metrics["mae"] = mean_absolute_error(y_true, y_pred)
  424. metrics["r2"] = r2_score(y_true, y_pred)
  425. # Additional metrics
  426. metrics["mean_error"] = np.mean(y_pred - y_true)
  427. metrics["std_error"] = np.std(y_pred - y_true)
  428. # Percentage errors
  429. mape = np.mean(np.abs((y_true - y_pred) / (y_true + 1e-8))) * 100
  430. metrics["mape"] = mape
  431. return metrics
  432. def calculate_embedding_metrics(self, embeddings: np.ndarray,
  433. labels: np.ndarray) -> Dict[str, float]:
  434. """
  435. Calculate metrics specific to embedding models (e.g., speaker recognition).
  436. Args:
  437. embeddings: Embedding vectors
  438. labels: True labels
  439. Returns:
  440. Dictionary containing embedding metrics
  441. """
  442. from sklearn.metrics.pairwise import cosine_similarity
  443. metrics = {}
  444. # Calculate pairwise similarities
  445. similarities = cosine_similarity(embeddings)
  446. # Intra-class and inter-class similarities
  447. intra_class_sims = []
  448. inter_class_sims = []
  449. unique_labels = np.unique(labels)
  450. for i in range(len(embeddings)):
  451. for j in range(i + 1, len(embeddings)):
  452. sim = similarities[i, j]
  453. if labels[i] == labels[j]:
  454. intra_class_sims.append(sim)
  455. else:
  456. inter_class_sims.append(sim)
  457. metrics["intra_class_similarity_mean"] = np.mean(intra_class_sims)
  458. metrics["intra_class_similarity_std"] = np.std(intra_class_sims)
  459. metrics["inter_class_similarity_mean"] = np.mean(inter_class_sims)
  460. metrics["inter_class_similarity_std"] = np.std(inter_class_sims)
  461. # Separation metric
  462. separation = (np.mean(intra_class_sims) - np.mean(inter_class_sims))
  463. metrics["class_separation"] = separation
  464. # Equal Error Rate (EER) calculation
  465. # Create pairs and labels for verification task
  466. pairs = []
  467. pair_labels = []
  468. for i in range(len(embeddings)):
  469. for j in range(i + 1, len(embeddings)):
  470. pairs.append((i, j))
  471. pair_labels.append(1 if labels[i] == labels[j] else 0)
  472. # Calculate similarities for pairs
  473. pair_similarities = [similarities[i, j] for i, j in pairs]
  474. # Find EER threshold
  475. thresholds = np.linspace(min(pair_similarities), max(pair_similarities), 1000)
  476. best_eer = 1.0
  477. best_threshold = 0.0
  478. for threshold in thresholds:
  479. predictions = (np.array(pair_similarities) >= threshold).astype(int)
  480. # Calculate FAR and FRR
  481. true_pairs = np.array(pair_labels) == 1
  482. false_pairs = np.array(pair_labels) == 0
  483. if np.sum(true_pairs) > 0 and np.sum(false_pairs) > 0:
  484. far = np.sum(predictions[false_pairs]) / np.sum(false_pairs) # False Accept Rate
  485. frr = 1 - np.sum(predictions[true_pairs]) / np.sum(true_pairs) # False Reject Rate
  486. if abs(far - frr) < abs(best_eer - best_eer):
  487. best_eer = (far + frr) / 2
  488. best_threshold = threshold
  489. metrics["eer"] = best_eer
  490. metrics["eer_threshold"] = best_threshold
  491. return metrics
  492. def calculate_all_metrics(self, y_true: np.ndarray, y_pred: np.ndarray,
  493. y_prob: np.ndarray = None,
  494. embeddings: np.ndarray = None) -> Dict[str, Any]:
  495. """
  496. Calculate all appropriate metrics based on task type.
  497. Args:
  498. y_true: True labels/values
  499. y_pred: Predicted labels/values
  500. y_prob: Prediction probabilities (for classification)
  501. embeddings: Embedding vectors (for embedding tasks)
  502. Returns:
  503. Dictionary containing all calculated metrics
  504. """
  505. if self.task_type == "classification":
  506. metrics = self.calculate_classification_metrics(y_true, y_pred, y_prob)
  507. # Add embedding metrics if available
  508. if embeddings is not None:
  509. embedding_metrics = self.calculate_embedding_metrics(embeddings, y_true)
  510. metrics.update(embedding_metrics)
  511. return metrics
  512. elif self.task_type == "regression":
  513. return self.calculate_regression_metrics(y_true, y_pred)
  514. else:
  515. raise ValueError(f"Unsupported task type: {self.task_type}")
  516. class ModelProfiler:
  517. """
  518. Model performance profiler for analyzing computational requirements.
  519. Provides profiling of model inference time, memory usage, FLOPs,
  520. and other performance characteristics.
  521. """
  522. def __init__(self, model: nn.Module, device: str = "cpu"):
  523. """
  524. Initialize model profiler.
  525. Args:
  526. model: PyTorch model to profile
  527. device: Device for profiling
  528. """
  529. self.model = model.to(device)
  530. self.device = device
  531. self.model.eval()
  532. def profile_inference(self, input_tensor: torch.Tensor,
  533. num_runs: int = 100,
  534. warmup_runs: int = 10) -> Dict[str, float]:
  535. """
  536. Profile model inference performance.
  537. Args:
  538. input_tensor: Sample input tensor
  539. num_runs: Number of inference runs for timing
  540. warmup_runs: Number of warmup runs
  541. Returns:
  542. Dictionary containing timing statistics
  543. """
  544. input_tensor = input_tensor.to(self.device)
  545. # Warmup runs
  546. with torch.no_grad():
  547. for _ in range(warmup_runs):
  548. _ = self.model(input_tensor)
  549. # Synchronize if using CUDA
  550. if self.device.startswith('cuda'):
  551. torch.cuda.synchronize()
  552. # Timing runs
  553. times = []
  554. with torch.no_grad():
  555. for _ in range(num_runs):
  556. start_time = time.perf_counter()
  557. output = self.model(input_tensor)
  558. if self.device.startswith('cuda'):
  559. torch.cuda.synchronize()
  560. end_time = time.perf_counter()
  561. times.append(end_time - start_time)
  562. return {
  563. "mean_inference_time": np.mean(times),
  564. "std_inference_time": np.std(times),
  565. "min_inference_time": np.min(times),
  566. "max_inference_time": np.max(times),
  567. "median_inference_time": np.median(times),
  568. "throughput_samples_per_second": input_tensor.shape[0] / np.mean(times)
  569. }
  570. def profile_memory(self, input_tensor: torch.Tensor) -> Dict[str, float]:
  571. """
  572. Profile memory usage during inference.
  573. Args:
  574. input_tensor: Sample input tensor
  575. Returns:
  576. Dictionary containing memory statistics
  577. """
  578. input_tensor = input_tensor.to(self.device)
  579. if not self.device.startswith('cuda'):
  580. return {"error": "Memory profiling only available for CUDA devices"}
  581. # Clear cache and reset peak memory stats
  582. torch.cuda.empty_cache()
  583. torch.cuda.reset_peak_memory_stats(self.device)
  584. # Record initial memory
  585. initial_memory = torch.cuda.memory_allocated(self.device)
  586. # Run inference
  587. with torch.no_grad():
  588. output = self.model(input_tensor)
  589. # Record peak memory
  590. peak_memory = torch.cuda.max_memory_allocated(self.device)
  591. final_memory = torch.cuda.memory_allocated(self.device)
  592. return {
  593. "initial_memory_mb": initial_memory / (1024 * 1024),
  594. "peak_memory_mb": peak_memory / (1024 * 1024),
  595. "final_memory_mb": final_memory / (1024 * 1024),
  596. "memory_increase_mb": (peak_memory - initial_memory) / (1024 * 1024)
  597. }
  598. def count_parameters(self) -> Dict[str, int]:
  599. """Count model parameters."""
  600. total_params = sum(p.numel() for p in self.model.parameters())
  601. trainable_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
  602. return {
  603. "total_parameters": total_params,
  604. "trainable_parameters": trainable_params,
  605. "non_trainable_parameters": total_params - trainable_params
  606. }
  607. def estimate_model_size(self) -> Dict[str, float]:
  608. """Estimate model size in different formats."""
  609. # Parameter size
  610. param_size = sum(p.numel() * p.element_size() for p in self.model.parameters())
  611. buffer_size = sum(b.numel() * b.element_size() for b in self.model.buffers())
  612. total_size = param_size + buffer_size
  613. return {
  614. "total_size_bytes": total_size,
  615. "total_size_mb": total_size / (1024 * 1024),
  616. "parameter_size_mb": param_size / (1024 * 1024),
  617. "buffer_size_mb": buffer_size / (1024 * 1024)
  618. }
  619. def generate_profile_report(self, input_tensor: torch.Tensor) -> str:
  620. """Generate comprehensive profiling report."""
  621. inference_stats = self.profile_inference(input_tensor)
  622. memory_stats = self.profile_memory(input_tensor)
  623. param_stats = self.count_parameters()
  624. size_stats = self.estimate_model_size()
  625. report = "# Model Profiling Report\n\n"
  626. # Model info
  627. report += "## Model Information\n"
  628. report += f"- Model: {self.model.__class__.__name__}\n"
  629. report += f"- Device: {self.device}\n"
  630. report += f"- Input shape: {input_tensor.shape}\n\n"
  631. # Parameters
  632. report += "## Parameters\n"
  633. report += f"- Total parameters: {param_stats['total_parameters']:,}\n"
  634. report += f"- Trainable parameters: {param_stats['trainable_parameters']:,}\n"
  635. report += f"- Non-trainable parameters: {param_stats['non_trainable_parameters']:,}\n\n"
  636. # Model size
  637. report += "## Model Size\n"
  638. report += f"- Total size: {size_stats['total_size_mb']:.2f} MB\n"
  639. report += f"- Parameter size: {size_stats['parameter_size_mb']:.2f} MB\n"
  640. report += f"- Buffer size: {size_stats['buffer_size_mb']:.2f} MB\n\n"
  641. # Inference performance
  642. report += "## Inference Performance\n"
  643. report += f"- Mean inference time: {inference_stats['mean_inference_time']*1000:.2f} ms\n"
  644. report += f"- Std inference time: {inference_stats['std_inference_time']*1000:.2f} ms\n"
  645. report += f"- Throughput: {inference_stats['throughput_samples_per_second']:.1f} samples/sec\n\n"
  646. # Memory usage
  647. if "error" not in memory_stats:
  648. report += "## Memory Usage\n"
  649. report += f"- Peak memory: {memory_stats['peak_memory_mb']:.2f} MB\n"
  650. report += f"- Memory increase: {memory_stats['memory_increase_mb']:.2f} MB\n\n"
  651. return report