Files
poxularandCursor 4965f27ba4 Add full GPU pipeline with ComfyUI nodes and phase profiling.
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>
2026-06-02 01:01:46 +02:00

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())