Files
2026-07-13 13:33:03 +08:00

341 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Sana Model Benchmark Script
Usage examples:
# Run on host (Mac) with default settings
python run_benchmark.py -i /path/to/images -r host -m /path/to/models
# Run on Android device
python run_benchmark.py -i /path/to/images -r android -b build_android -m sana_mnn_models_distill
# Test multiple backends at once
python run_benchmark.py -i /path/to/images -r host -m /path/to/models -k cpu metal -s 5 10
# Single backend test
python run_benchmark.py -i /path/to/image.jpg -r host -m /path/to/models -k cpu -s 10
# Full options (Android)
python run_benchmark.py -i /path/to/images -r android -o results -b build_android -m sana_mnn_models_distill -k cpu opencl -s 5 10 -p "your prompt"
"""
import subprocess
import os
import time
import csv
import shutil
import argparse
from pathlib import Path
VALID_BACKENDS = {
"android": ["cpu", "opencl", "npu"],
"host": ["cpu", "metal"]
}
def parse_args():
parser = argparse.ArgumentParser(
description="Sana Model Benchmark Script",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
parser.add_argument(
"-i", "--input",
required=True,
help="Input image file or directory containing images"
)
parser.add_argument(
"-r", "--runner",
choices=["android", "host"],
default="android",
help="Runner type: android (via adb) or host (local Mac) (default: android)"
)
parser.add_argument(
"-o", "--output",
default="benchmark_results",
help="Output directory for results (default: benchmark_results)"
)
parser.add_argument(
"-b", "--build-dir",
help="Build directory (default: build_android for android, build_sana for host)"
)
parser.add_argument(
"-m", "--model-dir",
required=True,
help="Model directory path"
)
parser.add_argument(
"-k", "--backend",
nargs="+",
default=None,
help="Backend type(s): cpu/opencl/npu for android, cpu/metal for host. Can specify multiple. (default: opencl for android, cpu for host)"
)
parser.add_argument(
"-s", "--steps",
type=int,
nargs="+",
default=[5, 10, 20],
help="List of inference steps to test (default: 5 10 20)"
)
parser.add_argument(
"-p", "--prompt",
default="A beautiful scenery in Studio Ghibli style",
help="Prompt for image generation"
)
parser.add_argument(
"--no-zip",
action="store_true",
help="Do not create zip archive of results"
)
parser.add_argument(
"--keep-output",
action="store_true",
help="Keep existing output directory (do not clean)"
)
args = parser.parse_args()
# Set defaults based on runner type
if args.build_dir is None:
args.build_dir = "build_android" if args.runner == "android" else "build_sana"
if args.backend is None:
args.backend = ["opencl"] if args.runner == "android" else ["cpu"]
# Validate backend choices
valid = VALID_BACKENDS[args.runner]
for backend in args.backend:
if backend not in valid:
parser.error(f"Invalid backend '{backend}' for {args.runner}. Choose from: {', '.join(valid)}")
return args
def get_image_files(input_path):
"""Get list of image files from input path (file or directory)."""
input_path = Path(input_path)
valid_extensions = {'.jpg', '.jpeg', '.png'}
if input_path.is_file():
if input_path.suffix.lower() in valid_extensions:
return [input_path]
else:
raise ValueError(f"Invalid image file: {input_path}")
elif input_path.is_dir():
images = [
f for f in input_path.iterdir()
if f.is_file() and f.suffix.lower() in valid_extensions
]
images.sort(key=lambda x: x.name)
return images
else:
raise ValueError(f"Input path does not exist: {input_path}")
def parse_timer_output(stdout):
"""Parse timing metrics from command output."""
metrics = {
"Load LLM (ms)": "",
"Init Diff (ms)": "",
"LLM Infer (ms)": "",
"Diff Infer (ms)": "",
"Total (ms)": ""
}
import re
# Parse [TIMER] format (from sana_diffusion_demo.cpp)
timer_patterns = {
r'\[TIMER\] Load LLM:\s*([\d.]+)': "Load LLM (ms)",
r'\[TIMER\] Init Diffusion:\s*([\d.]+)': "Init Diff (ms)",
r'\[TIMER\] LLM Inference:\s*([\d.]+)': "LLM Infer (ms)",
r'\[TIMER\] Diffusion Inference:\s*([\d.]+)': "Diff Infer (ms)",
r'\[TIMER\] Total:\s*([\d.]+)': "Total (ms)",
}
for pattern, metric_key in timer_patterns.items():
match = re.search(pattern, stdout)
if match:
metrics[metric_key] = match.group(1)
# Fallback: Parse "cost time" format from host script output (AUTOTIME macro)
# e.g., "vae_encoder, 208, cost time: 2737.709961 ms"
if not metrics["Load LLM (ms)"]:
vae_enc_match = re.search(r'vae_encoder.*cost time:\s*([\d.]+)', stdout)
if vae_enc_match:
metrics["Load LLM (ms)"] = vae_enc_match.group(1)
if not metrics["Diff Infer (ms)"]:
# Sum up diffusion step times
step_times = re.findall(r'Step \d+/\d+.*?run.*?cost time:\s*([\d.]+)', stdout, re.DOTALL)
if step_times:
total_step_time = sum(float(t) for t in step_times)
metrics["Diff Infer (ms)"] = f"{total_step_time:.2f}"
if not metrics["Total (ms)"]:
vae_dec_match = re.search(r'vae_decoder.*cost time:\s*([\d.]+)', stdout)
if vae_dec_match:
# Use last run time as total if available
total_match = re.findall(r'run, \d+, cost time:\s*([\d.]+)', stdout)
if total_match:
metrics["Total (ms)"] = total_match[-1]
return metrics
def run_benchmark(args, backend, images, output_root):
"""Run benchmark tests for a single backend."""
script_dir = Path(__file__).parent.resolve()
# Convert paths to absolute
build_dir = str(Path(args.build_dir).resolve())
model_dir = str(Path(args.model_dir).resolve())
results = []
for img_path in images:
img_name = img_path.name
for steps in args.steps:
print(f"Testing {img_name} with {steps} steps on {args.runner}/{backend}...")
local_output = os.path.join(
output_root,
f"{img_path.stem}_{backend}_step_{steps}.jpg"
)
# Build command based on runner type
if args.runner == "android":
cmd = [
str(script_dir / "run_sana_on_android.sh"),
"-b", build_dir,
"-m", model_dir,
"-M", "img2img",
"-i", str(img_path),
"-o", str(Path(local_output).resolve()),
"-p", args.prompt,
"-k", backend,
"-s", str(steps)
]
else: # host
cmd = [
str(script_dir / "run_sana_benchmark_host.sh"),
"-m", model_dir,
"-i", str(img_path),
"-b", backend,
"-s", str(steps),
"-M", "img2img"
]
start_time = time.time()
try:
process = subprocess.run(cmd, capture_output=True, text=True)
end_time = time.time()
# Combine stdout and stderr for parsing
output_text = process.stdout + "\n" + process.stderr
metrics = parse_timer_output(output_text)
result = {
"Image": img_name,
"Runner": args.runner,
"Backend": backend,
"Steps": steps,
**metrics,
"Total Script Time (s)": round(end_time - start_time, 2)
}
results.append(result)
if process.returncode != 0:
print(f" Warning: Command returned non-zero exit code ({process.returncode})")
if process.stderr:
# Print last few lines of stderr
stderr_lines = process.stderr.strip().split('\n')[-3:]
for line in stderr_lines:
print(f" stderr: {line[:100]}")
except Exception as e:
print(f" Error: {e}")
return results
def save_results(results, output_root, create_zip=True):
"""Save results to CSV and optionally create zip archive."""
if not results:
print("No results to save.")
return
# Save CSV
csv_path = os.path.join(output_root, "summary.csv")
with open(csv_path, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=results[0].keys())
writer.writeheader()
writer.writerows(results)
print(f"CSV saved to: {csv_path}")
# Print summary table
print("\n" + "=" * 80)
print("BENCHMARK SUMMARY")
print("=" * 80)
headers = list(results[0].keys())
print(" | ".join(f"{h[:15]:>15}" for h in headers))
print("-" * 80)
for r in results:
print(" | ".join(f"{str(v)[:15]:>15}" for v in r.values()))
# Create zip
if create_zip:
zip_name = f"{output_root}_results"
shutil.make_archive(zip_name, 'zip', output_root)
print(f"\nResults archived to: {zip_name}.zip")
def main():
args = parse_args()
print("=" * 50)
print("Sana Model Benchmark")
print("=" * 50)
# Get image files
try:
images = get_image_files(args.input)
except ValueError as e:
print(f"Error: {e}")
return
if not images:
print(f"No valid images found in: {args.input}")
return
print(f"Found {len(images)} image(s) to test")
print(f"Runner: {args.runner}")
print(f"Backends: {', '.join(args.backend)}")
print(f"Steps: {args.steps}")
print(f"Model dir: {args.model_dir}")
print("-" * 50)
# Setup output directory
backends_str = "_".join(args.backend)
output_root = f"{args.output}_{args.runner}_{backends_str}"
if not args.keep_output and os.path.exists(output_root):
shutil.rmtree(output_root)
os.makedirs(output_root, exist_ok=True)
# Run benchmarks for each backend
all_results = []
for backend in args.backend:
print(f"\n{'='*50}")
print(f"Testing backend: {backend}")
print(f"{'='*50}")
results = run_benchmark(args, backend, images, output_root)
all_results.extend(results)
save_results(all_results, output_root, create_zip=not args.no_zip)
print("\nBenchmark completed.")
if __name__ == "__main__":
main()