/ common / benchmark / reporter.py
reporter.py
  1  """
  2  Benchmark reporting utilities for AI System Optimization Series.
  3  Generates Markdown tables and performance charts.
  4  """
  5  
  6  import json
  7  from dataclasses import asdict, dataclass, field
  8  from datetime import datetime
  9  
 10  from .timer import TimingResult
 11  
 12  
 13  @dataclass
 14  class BenchmarkEntry:
 15      """Single benchmark entry."""
 16  
 17      module_name: str
 18      test_name: str
 19      hardware: str
 20      latency_ms: float
 21      throughput: float | None = None
 22      memory_mb: float | None = None
 23      timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
 24  
 25  
 26  @dataclass
 27  class BenchmarkReport:
 28      """Complete benchmark report."""
 29  
 30      entries: list[BenchmarkEntry] = field(default_factory=list)
 31      environment: dict[str, str] = field(default_factory=dict)
 32  
 33      def add_entry(self, entry: BenchmarkEntry) -> None:
 34          self.entries.append(entry)
 35  
 36      def to_dict(self) -> dict:
 37          """Serialize report to a JSON-compatible dictionary."""
 38          return {
 39              "entries": [asdict(e) for e in self.entries],
 40              "environment": self.environment,
 41          }
 42  
 43      def to_json(self, indent: int = 2) -> str:
 44          """Serialize report to a JSON string."""
 45          return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
 46  
 47      def save_json(self, path: str) -> None:
 48          """Save report to a JSON file."""
 49          with open(path, "w", encoding="utf-8") as f:
 50              f.write(self.to_json())
 51  
 52      @classmethod
 53      def from_json(cls, path: str) -> "BenchmarkReport":
 54          """Load report from a JSON file."""
 55          with open(path, encoding="utf-8") as f:
 56              data = json.load(f)
 57          entries = [BenchmarkEntry(**e) for e in data.get("entries", [])]
 58          environment = data.get("environment", {})
 59          return cls(entries=entries, environment=environment)
 60  
 61      def to_markdown_table(self) -> str:
 62          """Generate Markdown table from entries."""
 63          if not self.entries:
 64              return "No benchmark entries."
 65  
 66          lines = [
 67              "| Module | Test | Hardware | Latency (ms) | Throughput | Memory (MB) |",
 68              "|--------|------|----------|--------------|------------|-------------|",
 69          ]
 70  
 71          for e in self.entries:
 72              throughput = f"{e.throughput:.2f}" if e.throughput is not None else "N/A"
 73              memory = f"{e.memory_mb:.1f}" if e.memory_mb is not None else "N/A"
 74              lines.append(
 75                  f"| {e.module_name} | {e.test_name} | {e.hardware} | "
 76                  f"{e.latency_ms:.3f} | {throughput} | {memory} |"
 77              )
 78  
 79          return "\n".join(lines)
 80  
 81  
 82  def generate_comparison_table(
 83      results: dict[str, TimingResult], baseline_key: str = "pytorch_eager"
 84  ) -> str:
 85      """
 86      Generate Markdown comparison table from timing results.
 87  
 88      Args:
 89          results: Dict mapping implementation name to TimingResult
 90          baseline_key: Key for baseline implementation (for speedup calculation)
 91  
 92      Returns:
 93          Markdown formatted table string
 94      """
 95      if not results:
 96          return "No results to display."
 97  
 98      baseline = results.get(baseline_key)
 99      baseline_ms = baseline.mean_ms if baseline else None
100  
101      lines = [
102          "| Implementation | Mean (ms) | Std (ms) | Min (ms) | Max (ms) | Speedup |",
103          "|----------------|-----------|----------|----------|----------|---------|",
104      ]
105  
106      for name, result in results.items():
107          if baseline_ms and baseline_ms > 0:
108              speedup = f"{baseline_ms / result.mean_ms:.2f}x"
109          else:
110              speedup = "1.00x" if name == baseline_key else "N/A"
111  
112          lines.append(
113              f"| {name} | {result.mean_ms:.3f} | {result.std_ms:.3f} | "
114              f"{result.min_ms:.3f} | {result.max_ms:.3f} | {speedup} |"
115          )
116  
117      return "\n".join(lines)
118  
119  
120  def generate_performance_chart(
121      results: dict[str, TimingResult],
122      output_path: str = "performance_chart.png",
123      title: str = "Performance Comparison",
124  ) -> bool:
125      """
126      Generate performance comparison bar chart.
127  
128      Args:
129          results: Dict mapping implementation name to TimingResult
130          output_path: Path to save the chart
131          title: Chart title
132  
133      Returns:
134          True if chart was generated successfully, False otherwise
135      """
136      try:
137          import matplotlib.pyplot as plt
138          import numpy as np
139      except ImportError:
140          print("matplotlib not available, skipping chart generation")
141          return False
142  
143      if not results:
144          return False
145  
146      names = list(results.keys())
147      means = [r.mean_ms for r in results.values()]
148      stds = [r.std_ms for r in results.values()]
149  
150      fig, ax = plt.subplots(figsize=(10, 6))
151  
152      x = np.arange(len(names))
153      bars = ax.bar(x, means, yerr=stds, capsize=5, color="steelblue", alpha=0.8)
154  
155      ax.set_xlabel("Implementation")
156      ax.set_ylabel("Latency (ms)")
157      ax.set_title(title)
158      ax.set_xticks(x)
159      ax.set_xticklabels(names, rotation=45, ha="right")
160  
161      # Add value labels on bars
162      for bar, mean in zip(bars, means):
163          height = bar.get_height()
164          ax.annotate(
165              f"{mean:.2f}",
166              xy=(bar.get_x() + bar.get_width() / 2, height),
167              xytext=(0, 3),
168              textcoords="offset points",
169              ha="center",
170              va="bottom",
171              fontsize=9,
172          )
173  
174      plt.tight_layout()
175      plt.savefig(output_path, dpi=150)
176      plt.close()
177  
178      return True
179  
180  
181  def format_environment_info() -> dict[str, str]:
182      """Collect environment information for benchmark reports."""
183      import platform
184      import sys
185  
186      env = {
187          "python_version": sys.version.split()[0],
188          "platform": platform.platform(),
189          "processor": platform.processor(),
190      }
191  
192      # Try to get CUDA info
193      try:
194          import torch
195  
196          if torch.cuda.is_available():
197              env["cuda_version"] = torch.version.cuda or "N/A"
198              env["gpu_name"] = torch.cuda.get_device_name(0)
199              env["gpu_count"] = str(torch.cuda.device_count())
200      except ImportError:
201          pass
202  
203      return env