[diffusion] feat: support comparing batch perf (#14738)
Co-authored-by: shuxiguo <shuxiguo@meituan.com> Co-authored-by: Mick <mickjagger19@icloud.com>
This commit is contained in:
co-authored by
shuxiguo
Mick
parent
13680e5542
commit
6ec77680df
@@ -1,5 +1,6 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
@@ -108,44 +109,40 @@ def _load_benchmark_file(file_path: str) -> Dict[str, Any]:
|
|||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
def compare_benchmarks(
|
def _get_status_emoji_from_diff_percent(diff_pct):
|
||||||
baseline_path: str, new_path: str, output_format: str = "markdown"
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Compares two benchmark JSON files and prints a report.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
base_data = _load_benchmark_file(baseline_path)
|
|
||||||
new_data = _load_benchmark_file(new_path)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading benchmark files: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
base_e2e = base_data.get("total_duration_ms", 0)
|
|
||||||
new_e2e = new_data.get("total_duration_ms", 0)
|
|
||||||
|
|
||||||
diff_ms, diff_pct = calculate_diff(base_e2e, new_e2e)
|
|
||||||
|
|
||||||
if diff_pct < -2.0:
|
if diff_pct < -2.0:
|
||||||
status = "✅"
|
return "✅"
|
||||||
elif diff_pct > 2.0:
|
elif diff_pct > 2.0:
|
||||||
status = "❌"
|
return "❌"
|
||||||
else:
|
else:
|
||||||
status = ""
|
return "⚪️"
|
||||||
|
|
||||||
# --- Stage Breakdown ---
|
|
||||||
base_durations, base_order, base_counts = consolidate_steps(
|
def _print_single_comparison_report(
|
||||||
base_data.get("steps", [])
|
others_data, base_e2e, combined_order, base_durations, others_processed, base_counts
|
||||||
|
):
|
||||||
|
new_data = others_data[0]
|
||||||
|
new_e2e = new_data.get("total_duration_ms", 0)
|
||||||
|
diff_ms, diff_pct = calculate_diff(base_e2e, new_e2e)
|
||||||
|
status = _get_status_emoji_from_diff_percent(diff_pct)
|
||||||
|
|
||||||
|
print("#### 1. High-level Summary")
|
||||||
|
print("| Metric | Baseline | New | Diff | Status |")
|
||||||
|
print("| :--- | :--- | :--- | :--- | :--- |")
|
||||||
|
print(
|
||||||
|
f"| **E2E Latency** | {base_e2e:.2f} ms | {new_e2e:.2f} ms | **{diff_ms:+.2f} ms ({diff_pct:+.1f}%)** | {status} |"
|
||||||
)
|
)
|
||||||
new_durations, new_order, new_counts = consolidate_steps(new_data.get("steps", []))
|
print(
|
||||||
|
f"| **Throughput** | {1000 / base_e2e if base_e2e else 0:.2f} req/s | {1000 / new_e2e if new_e2e else 0:.2f} req/s | - | - |"
|
||||||
|
)
|
||||||
|
print("\n")
|
||||||
|
|
||||||
# Merge orders: Start with New order (execution order), append any missing from Base
|
print("#### 2. Stage Breakdown")
|
||||||
combined_order = list(new_order)
|
print("| Stage Name | Baseline (ms) | New (ms) | Diff (ms) | Diff (%) | Status |")
|
||||||
for name in base_order:
|
print("| :--- | :--- | :--- | :--- | :--- | :--- |")
|
||||||
if name not in combined_order:
|
|
||||||
combined_order.append(name)
|
new_durations, _, new_counts = others_processed[0]
|
||||||
|
|
||||||
stage_rows = []
|
|
||||||
for stage in combined_order:
|
for stage in combined_order:
|
||||||
b_val = base_durations.get(stage, 0.0)
|
b_val = base_durations.get(stage, 0.0)
|
||||||
n_val = new_durations.get(stage, 0.0)
|
n_val = new_durations.get(stage, 0.0)
|
||||||
@@ -154,7 +151,6 @@ def compare_benchmarks(
|
|||||||
|
|
||||||
s_diff, s_pct = calculate_diff(b_val, n_val)
|
s_diff, s_pct = calculate_diff(b_val, n_val)
|
||||||
|
|
||||||
# Format count string if aggregated
|
|
||||||
count_str = ""
|
count_str = ""
|
||||||
if stage == "Denoising Loop":
|
if stage == "Denoising Loop":
|
||||||
count_str = (
|
count_str = (
|
||||||
@@ -163,54 +159,143 @@ def compare_benchmarks(
|
|||||||
else f" ({b_count}->{n_count} steps)"
|
else f" ({b_count}->{n_count} steps)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# filter noise: show if diff is > 0.5ms OR if it's a major stage (like Denoising Loop)
|
status_emoji = get_perf_status_emoji(b_val, n_val)
|
||||||
# always show Denoising Loop or stages with significant duration/diff
|
print(
|
||||||
stage_rows.append((stage + count_str, b_val, n_val, s_diff, s_pct))
|
f"| {stage}{count_str} | {b_val:.2f} | {n_val:.2f} | {s_diff:+.2f} | {s_pct:+.1f}% | {status_emoji} |"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_multi_comparison_report(
|
||||||
|
base_e2e,
|
||||||
|
others_data,
|
||||||
|
other_labels,
|
||||||
|
combined_order,
|
||||||
|
base_durations,
|
||||||
|
others_processed,
|
||||||
|
):
|
||||||
|
print("#### 1. High-level Summary")
|
||||||
|
header = "| Metric | Baseline | " + " | ".join(other_labels) + " |"
|
||||||
|
sep = "| :--- | :--- | " + " | ".join([":---"] * len(other_labels)) + " |"
|
||||||
|
print(header)
|
||||||
|
print(sep)
|
||||||
|
|
||||||
|
# E2E Row
|
||||||
|
row_e2e = f"| **E2E Latency** | {base_e2e:.2f} ms |"
|
||||||
|
for i, d in enumerate(others_data):
|
||||||
|
val = d.get("total_duration_ms", 0)
|
||||||
|
diff_ms, diff_pct = calculate_diff(base_e2e, val)
|
||||||
|
|
||||||
|
status = _get_status_emoji_from_diff_percent(diff_pct)
|
||||||
|
|
||||||
|
row_e2e += f" {val:.2f} ms ({diff_pct:+.1f}%) {status} |"
|
||||||
|
print(row_e2e)
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
print("#### 2. Stage Breakdown")
|
||||||
|
# Header: Stage | Baseline | Label1 | Label2 ...
|
||||||
|
header = "| Stage Name | Baseline | " + " | ".join(other_labels) + " |"
|
||||||
|
sep = "| :--- | :--- | " + " | ".join([":---"] * len(other_labels)) + " |"
|
||||||
|
print(header)
|
||||||
|
print(sep)
|
||||||
|
|
||||||
|
for stage in combined_order:
|
||||||
|
b_val = base_durations.get(stage, 0.0)
|
||||||
|
row_str = f"| {stage} | {b_val:.2f} |"
|
||||||
|
|
||||||
|
for i, (n_durations, _, n_counts) in enumerate(others_processed):
|
||||||
|
n_val = n_durations.get(stage, 0.0)
|
||||||
|
_, s_pct = calculate_diff(b_val, n_val)
|
||||||
|
status_emoji = get_perf_status_emoji(b_val, n_val)
|
||||||
|
|
||||||
|
row_str += f" {n_val:.2f} ({s_pct:+.1f}%) {status_emoji} |"
|
||||||
|
print(row_str)
|
||||||
|
|
||||||
|
|
||||||
|
def compare_benchmarks(file_paths: List[str], output_format: str = "markdown"):
|
||||||
|
"""
|
||||||
|
Compares benchmark JSON files and prints a report.
|
||||||
|
First file is baseline, others will be compared against it.
|
||||||
|
"""
|
||||||
|
if len(file_paths) < 2:
|
||||||
|
print("Error: Need at least 2 files to compare.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data_list = [_load_benchmark_file(f) for f in file_paths]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading benchmark files: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
base_data = data_list[0]
|
||||||
|
others_data = data_list[1:]
|
||||||
|
|
||||||
|
# Use filenames as labels if multiple comparisons, else just "New"
|
||||||
|
other_labels = [os.path.basename(p) for p in file_paths[1:]]
|
||||||
|
|
||||||
|
base_e2e = base_data.get("total_duration_ms", 0)
|
||||||
|
|
||||||
|
base_durations, base_order, base_counts = consolidate_steps(
|
||||||
|
base_data.get("steps", [])
|
||||||
|
)
|
||||||
|
|
||||||
|
others_processed = []
|
||||||
|
for d in others_data:
|
||||||
|
dur, order, counts = consolidate_steps(d.get("steps", []))
|
||||||
|
others_processed.append((dur, order, counts))
|
||||||
|
|
||||||
|
combined_order = []
|
||||||
|
# Collect all unique stages maintaining order from newest to baseline
|
||||||
|
for _, order, _ in reversed(others_processed):
|
||||||
|
for name in order:
|
||||||
|
if name not in combined_order:
|
||||||
|
combined_order.append(name)
|
||||||
|
for name in base_order:
|
||||||
|
if name not in combined_order:
|
||||||
|
combined_order.append(name)
|
||||||
|
|
||||||
if output_format == "markdown":
|
if output_format == "markdown":
|
||||||
print("### Performance Comparison Report\n")
|
print("### Performance Comparison Report\n")
|
||||||
|
|
||||||
# Summary Table
|
if len(others_data) == 1:
|
||||||
print("#### 1. High-level Summary")
|
_print_single_comparison_report(
|
||||||
print("| Metric | Baseline | New | Diff | Status |")
|
others_data,
|
||||||
print("| :--- | :--- | :--- | :--- | :--- |")
|
base_e2e,
|
||||||
print(
|
combined_order,
|
||||||
f"| **E2E Latency** | {base_e2e:.2f} ms | {new_e2e:.2f} ms | **{diff_ms:+.2f} ms ({diff_pct:+.1f}%)** | {status} |"
|
base_durations,
|
||||||
)
|
others_processed,
|
||||||
print(
|
base_counts,
|
||||||
f"| **Throughput** | {1000 / base_e2e if base_e2e else 0:.2f} req/s | {1000 / new_e2e if new_e2e else 0:.2f} req/s | - | - |"
|
)
|
||||||
)
|
else:
|
||||||
print("\n")
|
_print_multi_comparison_report(
|
||||||
|
base_e2e,
|
||||||
# Detailed Breakdown
|
others_data,
|
||||||
print("#### 2. Stage Breakdown")
|
other_labels,
|
||||||
print(
|
combined_order,
|
||||||
"| Stage Name | Baseline (ms) | New (ms) | Diff (ms) | Diff (%) | Status |"
|
base_durations,
|
||||||
)
|
others_processed,
|
||||||
print("| :--- | :--- | :--- | :--- | :--- | :--- |")
|
|
||||||
for name, b, n, d, p in stage_rows:
|
|
||||||
name_str = name
|
|
||||||
status_emoji = get_perf_status_emoji(b, n)
|
|
||||||
print(
|
|
||||||
f"| {name_str} | {b:.2f} | {n:.2f} | {d:+.2f} | {p:+.1f}% | {status_emoji} |"
|
|
||||||
)
|
)
|
||||||
print("\n")
|
|
||||||
|
|
||||||
|
print("\n")
|
||||||
# Metadata
|
# Metadata
|
||||||
print("<details>")
|
print("<details>")
|
||||||
print("<summary>Metadata</summary>\n")
|
print("<summary>Metadata</summary>\n")
|
||||||
print(f"- Baseline Commit: `{base_data.get('commit_hash', 'N/A')}`")
|
print(f"- Baseline Commit: `{base_data.get('commit_hash', 'N/A')}`")
|
||||||
print(f"- New Commit: `{new_data.get('commit_hash', 'N/A')}`")
|
for i, d in enumerate(others_data):
|
||||||
|
label = "New" if len(others_data) == 1 else other_labels[i]
|
||||||
|
print(f"- {label} Commit: `{d.get('commit_hash', 'N/A')}`")
|
||||||
print(f"- Timestamp: {datetime.now().isoformat()}")
|
print(f"- Timestamp: {datetime.now().isoformat()}")
|
||||||
print("</details>")
|
print("</details>")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Compare two sglang-diffusion performance JSON files."
|
description="Compare sglang-diffusion performance JSON files."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"files",
|
||||||
|
nargs="+",
|
||||||
|
help="List of JSON files. First is baseline, others are compared against it.",
|
||||||
)
|
)
|
||||||
parser.add_argument("baseline", help="Path to the baseline JSON file")
|
|
||||||
parser.add_argument("new", help="Path to the new JSON file")
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
compare_benchmarks(args.baseline, args.new)
|
compare_benchmarks(args.files)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ For PRs that impact **latency**, **throughput**, or **memory usage**, you **shou
|
|||||||
|
|
||||||
3. **Compare**: run the compare script, which will print a Markdown table to the console
|
3. **Compare**: run the compare script, which will print a Markdown table to the console
|
||||||
```bash
|
```bash
|
||||||
$ python python/sglang/multimodal_gen/benchmarks/compare_perf.py baseline.json new.json
|
$ python python/sglang/multimodal_gen/benchmarks/compare_perf.py baseline.json new.json [new2.json ...]
|
||||||
### Performance Comparison Report
|
### Performance Comparison Report
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user