Ports scene extract, face ONNX preprocess, and net reference resize to Torch/CUDA with minimal CPU sync, while keeping the CPU parity path unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
214 lines
7.8 KiB
Python
214 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Benchmark CPU vs CUDA ONNX paths on a source/reference pair."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from magicmatch_core.inference import ( # noqa: E402
|
|
build_merged_lut_with_base,
|
|
get_session,
|
|
run_inference_from_images,
|
|
)
|
|
from magicmatch_core.inference_cuda import ( # noqa: E402
|
|
build_merged_lut_with_base_cuda,
|
|
color_match_session_info,
|
|
get_session_cuda,
|
|
run_inference_from_images_cuda,
|
|
)
|
|
from magicmatch_core.onnx_providers import cuda_available, get_available_providers # noqa: E402
|
|
from magicmatch_core.gpu.device import gpu_pipeline_available # noqa: E402
|
|
from magicmatch_core.probe_parity.face_detection import detect_faces # noqa: E402
|
|
from magicmatch_core.probe_parity.face_detection_cuda import ( # noqa: E402
|
|
detect_faces_cuda,
|
|
face_session_info,
|
|
)
|
|
from magicmatch_core.probe_parity.pipeline import ( # noqa: E402
|
|
_render_for_net,
|
|
build_merged_lut_probe_style,
|
|
)
|
|
from magicmatch_core.probe_parity.base_adjustments import estimate_base_adjustments # noqa: E402
|
|
from magicmatch_core.probe_parity.base_adjustments_cuda import estimate_base_adjustments_cuda # noqa: E402
|
|
from magicmatch_core.probe_parity.reference import prepare_net_reference, prepare_worker_bitmap_source # noqa: E402
|
|
from magicmatch_core.probe_parity.scene_extract import render_detection_inputs # noqa: E402
|
|
from magicmatch_core.probe_parity.luminance import get_adjusted_gamma, get_luminance_statistics # noqa: E402
|
|
|
|
|
|
def load_rgb(path: Path) -> np.ndarray:
|
|
return np.asarray(Image.open(path).convert("RGB"), dtype=np.float32) / 255.0
|
|
|
|
|
|
def lut_hash(lut: np.ndarray) -> str:
|
|
return hashlib.sha256(np.asarray(lut, dtype=np.float32).tobytes()).hexdigest()[:16]
|
|
|
|
|
|
def _timed(label: str, fn, *, repeats: int = 1) -> tuple[object, float]:
|
|
# Warm-up once
|
|
result = fn()
|
|
start = time.perf_counter()
|
|
for _ in range(repeats):
|
|
result = fn()
|
|
elapsed = (time.perf_counter() - start) / max(repeats, 1)
|
|
print(f" {label}: {elapsed * 1000:.1f} ms")
|
|
return result, elapsed
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("source", type=Path, nargs="?", help="Source image (PNG/JPG)")
|
|
parser.add_argument("reference", type=Path, nargs="?", help="Reference image (PNG/JPG)")
|
|
parser.add_argument(
|
|
"--repeats",
|
|
type=int,
|
|
default=3,
|
|
help="Timed repeats per benchmark (after warm-up)",
|
|
)
|
|
parser.add_argument(
|
|
"--profile-gpu",
|
|
action="store_true",
|
|
help="Print per-phase GPU pipeline timings and exit",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
default_pair = (
|
|
ROOT.parent / "polarrnext" / "standalone_probe" / "public" / "pair" / "source.png",
|
|
ROOT.parent / "polarrnext" / "standalone_probe" / "public" / "pair" / "reference.jpg",
|
|
)
|
|
source_path = args.source or default_pair[0]
|
|
reference_path = args.reference or default_pair[1]
|
|
if not source_path.is_file() or not reference_path.is_file():
|
|
print(f"Missing test pair: {source_path} / {reference_path}", file=sys.stderr)
|
|
return 2
|
|
|
|
src = load_rgb(source_path)
|
|
ref = load_rgb(reference_path)
|
|
|
|
if args.profile_gpu:
|
|
from magicmatch_core.gpu.pipeline_full_gpu import profile_full_gpu_pipeline
|
|
|
|
print(json.dumps(profile_full_gpu_pipeline(src, ref), indent=2, default=str))
|
|
return 0
|
|
|
|
report: dict[str, object] = {
|
|
"source": str(source_path),
|
|
"reference": str(reference_path),
|
|
"ort_providers_available": get_available_providers(),
|
|
"cuda_available": cuda_available(),
|
|
}
|
|
|
|
print("=== Provider availability ===")
|
|
print(f" ORT providers: {get_available_providers()}")
|
|
print(f" cuda_available(): {cuda_available()}")
|
|
|
|
worker_src = prepare_worker_bitmap_source(src)
|
|
base_cpu = estimate_base_adjustments(worker_src, worker_feed_prepared=True)
|
|
net_source = _render_for_net(worker_src, base_cpu)
|
|
net_ref = prepare_net_reference(ref)
|
|
|
|
print("\n=== color_match.onnx (256 feed) ===")
|
|
get_session()
|
|
get_session_cuda()
|
|
_, t_cpu_infer = _timed(
|
|
"CPU run_inference_from_images",
|
|
lambda: run_inference_from_images(net_source, net_ref),
|
|
repeats=args.repeats,
|
|
)
|
|
_, t_cuda_infer = _timed(
|
|
"CUDA run_inference_from_images_cuda",
|
|
lambda: run_inference_from_images_cuda(net_source, net_ref),
|
|
repeats=args.repeats,
|
|
)
|
|
report["color_match"] = {
|
|
"cpu_ms": round(t_cpu_infer * 1000, 2),
|
|
"cuda_ms": round(t_cuda_infer * 1000, 2),
|
|
"cpu_session": get_session().get_providers(),
|
|
"cuda_session": color_match_session_info(),
|
|
}
|
|
|
|
small, large = render_detection_inputs(worker_src, worker_feed_prepared=True)
|
|
stats = get_luminance_statistics(small, large, [])
|
|
gamma = get_adjusted_gamma(stats)
|
|
|
|
print("\n=== face ONNX (detect on large) ===")
|
|
_, t_cpu_face = _timed(
|
|
"CPU detect_faces",
|
|
lambda: detect_faces(large, adjusted_gamma=gamma),
|
|
repeats=max(1, args.repeats - 1),
|
|
)
|
|
_, t_cuda_face = _timed(
|
|
"CUDA detect_faces_cuda",
|
|
lambda: detect_faces_cuda(large, adjusted_gamma=gamma),
|
|
repeats=max(1, args.repeats - 1),
|
|
)
|
|
report["face_detect"] = {
|
|
"cpu_ms": round(t_cpu_face * 1000, 2),
|
|
"cuda_ms": round(t_cuda_face * 1000, 2),
|
|
"cuda_sessions": face_session_info(),
|
|
}
|
|
|
|
print("\n=== full build (probe-style) ===")
|
|
build_cpu, t_cpu_build = _timed(
|
|
"CPU build_merged_lut_with_base",
|
|
lambda: build_merged_lut_with_base(src, ref),
|
|
repeats=args.repeats,
|
|
)
|
|
build_cuda, t_cuda_build = _timed(
|
|
"CUDA build_merged_lut_with_base_cuda",
|
|
lambda: build_merged_lut_with_base_cuda(src, ref),
|
|
repeats=args.repeats,
|
|
)
|
|
if gpu_pipeline_available():
|
|
from magicmatch_core.gpu.pipeline import build_merged_lut_probe_style_gpu
|
|
|
|
_, t_gpu_build = _timed(
|
|
"GPU full pipeline build_merged_lut_probe_style_gpu",
|
|
lambda: build_merged_lut_probe_style_gpu(src, ref),
|
|
repeats=args.repeats,
|
|
)
|
|
report["gpu_full_build_ms"] = round(t_gpu_build * 1000, 2)
|
|
merged_cpu, _ = build_cpu
|
|
merged_cuda, _ = build_cuda
|
|
hash_cpu = lut_hash(merged_cpu)
|
|
hash_cuda = lut_hash(merged_cuda)
|
|
max_abs = float(np.max(np.abs(np.asarray(merged_cpu) - np.asarray(merged_cuda))))
|
|
report["full_build"] = {
|
|
"cpu_ms": round(t_cpu_build * 1000, 2),
|
|
"cuda_ms": round(t_cuda_build * 1000, 2),
|
|
"lut_hash_cpu": hash_cpu,
|
|
"lut_hash_cuda": hash_cuda,
|
|
"lut_hash_match": hash_cpu == hash_cuda,
|
|
"lut_max_abs_delta": max_abs,
|
|
"golden_lut_hash": "a48758ca22a2e389",
|
|
"cpu_matches_golden": hash_cpu == "a48758ca22a2e389",
|
|
"cuda_matches_golden": hash_cuda == "a48758ca22a2e389",
|
|
}
|
|
print(f" lut_hash CPU: {hash_cpu}")
|
|
print(f" lut_hash CUDA: {hash_cuda}")
|
|
print(f" max |CPU-CUDA| LUT delta: {max_abs:.6g}")
|
|
|
|
if not cuda_available():
|
|
print("\nNote: CUDAExecutionProvider not available — CUDA timings used CPU fallback.")
|
|
|
|
out_path = ROOT / "parity_out" / "bench_cuda_vs_cpu.json"
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
out_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
print(f"\nWrote {out_path}")
|
|
print(json.dumps(report, indent=2))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|