#!/usr/bin/env python3
"""
Generate HTML comparison report for PyTorch vs RustyTorch++ PINN benchmarks.
This script parses benchmark results from:
- PyTorch JSON output (from benchmark_runner.py)
- Rust Criterion text output
And generates:
- comparison_report.html (visual report with charts)
- comparison_report.md (markdown summary)
Usage:
python generate_comparison_report.py \
--output-dir ./benchmark_reports/hostname-rust-py-pinn-benchmark-12-10-2025/ \
--system-info system_info.json \
--pytorch-cpu pytorch_cpu_results.json \
--pytorch-gpu pytorch_gpu_results.json \
--rust-cpu rust_cpu_output.txt \
--rust-gpu rust_gpu_output.txt
"""
import argparse
import json
import re
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
@dataclass
class BenchmarkResult:
"""Parsed benchmark result."""
name: str
n_points: int
mean_time_us: float # microseconds
std_time_us: float = 0.0
framework: str = ""
device: str = ""
def parse_pytorch_json(filepath: Path) -> List[BenchmarkResult]:
"""Parse PyTorch benchmark JSON output."""
results = []
if not filepath.exists():
return results
with open(filepath) as f:
data = json.load(f)
framework = data.get("framework", "pytorch")
device = data.get("device", "unknown")
for bench in data.get("benchmarks", []):
name = bench.get("name", "unknown")
n_points = bench.get("n_points", 0)
mean_ms = bench.get("mean_time_ms", 0)
std_ms = bench.get("std_time_ms", 0)
results.append(BenchmarkResult(
name=name,
n_points=n_points,
mean_time_us=mean_ms * 1000, # ms -> us
std_time_us=std_ms * 1000,
framework=framework,
device=device.split(":")[0] if ":" in device else device
))
return results
def parse_criterion_output(filepath: Path) -> List[BenchmarkResult]:
"""Parse Rust Criterion text output."""
results = []
if not filepath.exists():
return results
content = filepath.read_text()
# Pattern: benchmark_name/n_points time: [low mean high]
# Example: forward_pass/200_points time: [40.123 us 41.456 us 42.789 us]
pattern = r'(\S+)/(\d+)_points\s+time:\s+\[[\d.]+ [nuµm]s\s+([\d.]+)\s+([nuµm]s)'
for match in re.finditer(pattern, content):
name, n_points, mean_val, unit = match.groups()
n_points = int(n_points)
mean_val = float(mean_val)
# Convert to microseconds
if unit == "ns":
mean_us = mean_val / 1000
elif unit in ("us", "µs"):
mean_us = mean_val
elif unit == "ms":
mean_us = mean_val * 1000
else:
mean_us = mean_val
results.append(BenchmarkResult(
name=name,
n_points=n_points,
mean_time_us=mean_us,
framework="rustytorch",
device="gpu" if "cuda" in str(filepath).lower() else "cpu"
))
return results
def load_system_info(filepath: Path) -> Dict:
"""Load system info JSON."""
if not filepath.exists():
return {}
with open(filepath) as f:
return json.load(f)
def calculate_speedup(pytorch_us: float, rust_us: float) -> Tuple[float, str]:
"""Calculate speedup ratio and winner."""
if pytorch_us <= 0 or rust_us <= 0:
return 1.0, "tie"
if rust_us < pytorch_us:
speedup = pytorch_us / rust_us
winner = "rust"
else:
speedup = rust_us / pytorch_us
winner = "pytorch"
return speedup, winner
def generate_html_report(
output_dir: Path,
system_info: Dict,
pytorch_cpu: List[BenchmarkResult],
pytorch_gpu: List[BenchmarkResult],
rust_cpu: List[BenchmarkResult],
rust_gpu: List[BenchmarkResult]
) -> None:
"""Generate HTML comparison report with charts."""
# Build comparison data
benchmarks = {}
# Organize by (name, n_points)
for result in pytorch_cpu:
key = (result.name, result.n_points)
if key not in benchmarks:
benchmarks[key] = {"pytorch_cpu": None, "pytorch_gpu": None, "rust_cpu": None, "rust_gpu": None}
benchmarks[key]["pytorch_cpu"] = result.mean_time_us
for result in pytorch_gpu:
key = (result.name, result.n_points)
if key not in benchmarks:
benchmarks[key] = {"pytorch_cpu": None, "pytorch_gpu": None, "rust_cpu": None, "rust_gpu": None}
benchmarks[key]["pytorch_gpu"] = result.mean_time_us
for result in rust_cpu:
key = (result.name, result.n_points)
if key not in benchmarks:
benchmarks[key] = {"pytorch_cpu": None, "pytorch_gpu": None, "rust_cpu": None, "rust_gpu": None}
benchmarks[key]["rust_cpu"] = result.mean_time_us
for result in rust_gpu:
key = (result.name, result.n_points)
if key not in benchmarks:
benchmarks[key] = {"pytorch_cpu": None, "pytorch_gpu": None, "rust_cpu": None, "rust_gpu": None}
benchmarks[key]["rust_gpu"] = result.mean_time_us
# Build table rows
table_rows = []
chart_data_cpu = []
chart_data_gpu = []
for (name, n_points), data in sorted(benchmarks.items()):
py_cpu = data["pytorch_cpu"]
py_gpu = data["pytorch_gpu"]
rust_cpu_val = data["rust_cpu"]
rust_gpu_val = data["rust_gpu"]
# CPU speedup
cpu_speedup = ""
cpu_winner = ""
if py_cpu and rust_cpu_val:
speedup, winner = calculate_speedup(py_cpu, rust_cpu_val)
cpu_speedup = f"{speedup:.2f}x"
cpu_winner = winner
chart_data_cpu.append({
"name": f"{name}/{n_points}",
"pytorch": py_cpu,
"rust": rust_cpu_val
})
# GPU speedup
gpu_speedup = ""
gpu_winner = ""
if py_gpu and rust_gpu_val:
speedup, winner = calculate_speedup(py_gpu, rust_gpu_val)
gpu_speedup = f"{speedup:.2f}x"
gpu_winner = winner
chart_data_gpu.append({
"name": f"{name}/{n_points}",
"pytorch": py_gpu,
"rust": rust_gpu_val
})
def fmt(val):
if val is None:
return "-"
if val >= 1000:
return f"{val/1000:.2f} ms"
return f"{val:.1f} µs"
def winner_class(winner, framework):
if winner == framework:
return "winner"
return ""
table_rows.append(f"""
| {name} |
{n_points} |
{fmt(py_cpu)} |
{fmt(rust_cpu_val)} |
{cpu_speedup} |
{fmt(py_gpu)} |
{fmt(rust_gpu_val)} |
{gpu_speedup} |
""")
table_html = "\n".join(table_rows)
# System info
sys_os = system_info.get("os", {})
sys_cpu = system_info.get("cpu", {})
sys_gpu = system_info.get("gpu", {})
sys_mem = system_info.get("memory", {})
hostname = system_info.get("hostname", "unknown")
timestamp = system_info.get("timestamp", datetime.now().isoformat())
commit = system_info.get("commit", "unknown")
html = f"""
PyTorch vs RustyTorch++ PINN Benchmark
PyTorch vs RustyTorch++ PINN Benchmark
Physics-Informed Neural Network Performance Comparison
Host: {hostname} | Generated: {timestamp} | Commit: {commit}
System Specifications
💻
Operating System
{sys_os.get('name', 'Unknown')} {sys_os.get('version', '')}
⚙
CPU
{sys_cpu.get('model', 'Unknown')}
🎮
GPU
{sys_gpu.get('name', 'Unknown')}
💾
GPU Memory
{sys_gpu.get('memory', 'Unknown')}
🧠
System Memory
{sys_mem.get('total', 'Unknown')}
🔍
Compute Capability
{sys_gpu.get('compute_capability', 'N/A')}
Benchmark Results
| Benchmark |
Points |
PyTorch CPU |
Rust CPU |
CPU Speedup |
PyTorch GPU |
Rust GPU |
GPU Speedup |
{table_html}
Visual Comparison
CPU Performance (lower is better)
GPU Performance (lower is better)
"""
html_path = output_dir / "comparison_report.html"
html_path.write_text(html)
print(f" HTML report: {html_path}")
def generate_markdown_report(
output_dir: Path,
system_info: Dict,
pytorch_cpu: List[BenchmarkResult],
pytorch_gpu: List[BenchmarkResult],
rust_cpu: List[BenchmarkResult],
rust_gpu: List[BenchmarkResult]
) -> None:
"""Generate Markdown summary report."""
hostname = system_info.get("hostname", "unknown")
timestamp = system_info.get("timestamp", datetime.now().isoformat())
commit = system_info.get("commit", "unknown")
sys_os = system_info.get("os", {})
sys_cpu = system_info.get("cpu", {})
sys_gpu = system_info.get("gpu", {})
sys_mem = system_info.get("memory", {})
# Build comparison table
benchmarks = {}
for result in pytorch_cpu + pytorch_gpu + rust_cpu + rust_gpu:
key = (result.name, result.n_points)
if key not in benchmarks:
benchmarks[key] = {"pytorch_cpu": None, "pytorch_gpu": None, "rust_cpu": None, "rust_gpu": None}
if result.framework == "pytorch" and result.device == "cpu":
benchmarks[key]["pytorch_cpu"] = result.mean_time_us
elif result.framework == "pytorch" and "cuda" in result.device.lower():
benchmarks[key]["pytorch_gpu"] = result.mean_time_us
elif result.framework == "rustytorch" and result.device == "cpu":
benchmarks[key]["rust_cpu"] = result.mean_time_us
elif result.framework == "rustytorch" and result.device == "gpu":
benchmarks[key]["rust_gpu"] = result.mean_time_us
table_rows = []
for (name, n_points), data in sorted(benchmarks.items()):
py_cpu = data["pytorch_cpu"]
py_gpu = data["pytorch_gpu"]
rust_cpu_val = data["rust_cpu"]
rust_gpu_val = data["rust_gpu"]
def fmt(val):
if val is None:
return "-"
if val >= 1000:
return f"{val/1000:.2f} ms"
return f"{val:.1f} µs"
cpu_speedup = ""
if py_cpu and rust_cpu_val:
speedup, winner = calculate_speedup(py_cpu, rust_cpu_val)
cpu_speedup = f"{speedup:.2f}x ({winner})"
gpu_speedup = ""
if py_gpu and rust_gpu_val:
speedup, winner = calculate_speedup(py_gpu, rust_gpu_val)
gpu_speedup = f"{speedup:.2f}x ({winner})"
table_rows.append(
f"| {name} | {n_points} | {fmt(py_cpu)} | {fmt(rust_cpu_val)} | {cpu_speedup} | {fmt(py_gpu)} | {fmt(rust_gpu_val)} | {gpu_speedup} |"
)
table_str = "\n".join(table_rows)
md = f"""# PyTorch vs RustyTorch++ PINN Benchmark
**Host:** {hostname}
**Generated:** {timestamp}
**Commit:** {commit}
---
## System Specifications
| Component | Details |
|-----------|---------|
| **OS** | {sys_os.get('name', 'Unknown')} {sys_os.get('version', '')} |
| **CPU** | {sys_cpu.get('model', 'Unknown')} |
| **GPU** | {sys_gpu.get('name', 'Unknown')} |
| **GPU Memory** | {sys_gpu.get('memory', 'Unknown')} |
| **System Memory** | {sys_mem.get('total', 'Unknown')} |
---
## Benchmark Results
| Benchmark | Points | PyTorch CPU | Rust CPU | CPU Speedup | PyTorch GPU | Rust GPU | GPU Speedup |
|-----------|--------|-------------|----------|-------------|-------------|----------|-------------|
{table_str}
---
*Auto-generated by run_pinn_comparison.sh*
"""
md_path = output_dir / "comparison_report.md"
md_path.write_text(md)
print(f" Markdown report: {md_path}")
def main():
parser = argparse.ArgumentParser(description="Generate PyTorch vs RustyTorch++ comparison report")
parser.add_argument("--output-dir", type=str, required=True, help="Output directory")
parser.add_argument("--system-info", type=str, help="System info JSON file")
parser.add_argument("--pytorch-cpu", type=str, help="PyTorch CPU results JSON")
parser.add_argument("--pytorch-gpu", type=str, help="PyTorch GPU results JSON")
parser.add_argument("--rust-cpu", type=str, help="Rust CPU Criterion output")
parser.add_argument("--rust-gpu", type=str, help="Rust GPU Criterion output")
args = parser.parse_args()
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
# Load system info
system_info = {}
if args.system_info:
system_info = load_system_info(Path(args.system_info))
# Parse benchmark results
pytorch_cpu = []
pytorch_gpu = []
rust_cpu = []
rust_gpu = []
if args.pytorch_cpu:
pytorch_cpu = parse_pytorch_json(Path(args.pytorch_cpu))
print(f" Parsed {len(pytorch_cpu)} PyTorch CPU results")
if args.pytorch_gpu:
pytorch_gpu = parse_pytorch_json(Path(args.pytorch_gpu))
print(f" Parsed {len(pytorch_gpu)} PyTorch GPU results")
if args.rust_cpu:
rust_cpu = parse_criterion_output(Path(args.rust_cpu))
print(f" Parsed {len(rust_cpu)} Rust CPU results")
if args.rust_gpu:
rust_gpu = parse_criterion_output(Path(args.rust_gpu))
print(f" Parsed {len(rust_gpu)} Rust GPU results")
# Generate reports
generate_html_report(output_dir, system_info, pytorch_cpu, pytorch_gpu, rust_cpu, rust_gpu)
generate_markdown_report(output_dir, system_info, pytorch_cpu, pytorch_gpu, rust_cpu, rust_gpu)
if __name__ == "__main__":
main()