Files
poxularandCursor 9a056baa9e Fix CUDA one-shot LUT drift: use CPU Lanczos ORT feeds on GPU EP.
run_inference_from_images_cuda was routing through GPU bilinear resize on H100,
producing a different net feed and brighter output than MagicMatch (one-shot).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 02:03:34 +02:00

116 lines
3.9 KiB
Python

"""
CUDA ONNX inference for MAGICMATCH color-match graph.
Separate from inference.py (CPU parity default). Scene extract / develop / reference
stay on the NumPy CPU stack — only color_match.onnx sess.run uses the GPU when available.
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
try:
import onnxruntime as ort
except ImportError as e:
raise ImportError("MAGICMATCH CUDA path: pip install onnxruntime-gpu") from e
from .inference import (
DEFAULT_ONNX,
INPUT_NAMES,
OUTPUT_LUT1D,
OUTPUT_LUT3D,
_resize_hwc_to_nhwc,
)
from .onnx_providers import create_onnx_session, cuda_available, session_active_provider
try:
import torch
_TORCH = True
except ImportError:
_TORCH = False
_SESSION_CUDA: ort.InferenceSession | None = None
def get_session_cuda(onnx_path: str | Path | None = None) -> ort.InferenceSession:
global _SESSION_CUDA
path = Path(onnx_path or DEFAULT_ONNX)
if not path.is_file():
raise FileNotFoundError(
f"MAGICMATCH ONNX not found: {path}\n"
"Expected: MAGICMATCH/models/color_match.onnx in this custom node folder."
)
if _SESSION_CUDA is None:
_SESSION_CUDA = create_onnx_session(path, prefer_cuda=True)
return _SESSION_CUDA
def run_inference_from_images_cuda(
source_hwc: np.ndarray,
reference_hwc: np.ndarray,
*,
session: ort.InferenceSession | None = None,
) -> tuple[np.ndarray, np.ndarray]:
"""Run color_match.onnx on GPU EP with CPU-parity ORT feeds (LANCZOS+bilinear)."""
sess = session or get_session_cuda()
feeds = {
INPUT_NAMES[0]: _resize_hwc_to_nhwc(source_hwc),
INPUT_NAMES[1]: _resize_hwc_to_nhwc(reference_hwc),
}
out_map = {o.name: o for o in sess.get_outputs()}
names = [out_map[OUTPUT_LUT3D].name, out_map[OUTPUT_LUT1D].name]
lut3d, lut1d = sess.run(names, feeds)
return np.asarray(lut3d, dtype=np.float32), np.asarray(lut1d, dtype=np.float32)
def run_inference_from_images_cuda_tensors(
source_hwc: "torch.Tensor | np.ndarray",
reference_hwc: "torch.Tensor | np.ndarray",
*,
session: ort.InferenceSession | None = None,
) -> tuple[np.ndarray, np.ndarray]:
"""Run color_match.onnx; source/ref may be CUDA tensors (GPU resize, single ORT sync)."""
from .gpu.resize_torch import hwc_to_nhwc_numpy
sess = session or get_session_cuda()
if _TORCH and isinstance(source_hwc, torch.Tensor):
src_feed = hwc_to_nhwc_numpy(source_hwc)
else:
src_feed = _resize_hwc_to_nhwc(np.asarray(source_hwc, dtype=np.float32))
if _TORCH and isinstance(reference_hwc, torch.Tensor):
ref_feed = hwc_to_nhwc_numpy(reference_hwc)
else:
ref_feed = _resize_hwc_to_nhwc(np.asarray(reference_hwc, dtype=np.float32))
feeds = {
INPUT_NAMES[0]: src_feed,
INPUT_NAMES[1]: ref_feed,
}
out_map = {o.name: o for o in sess.get_outputs()}
names = [out_map[OUTPUT_LUT3D].name, out_map[OUTPUT_LUT1D].name]
lut3d, lut1d = sess.run(names, feeds)
return np.asarray(lut3d, dtype=np.float32), np.asarray(lut1d, dtype=np.float32)
def build_merged_lut_with_base_cuda(
source_hwc: np.ndarray,
reference_hwc: np.ndarray,
) -> tuple[np.ndarray, dict]:
"""CPU parity build; CUDA color_match.onnx only (matches MagicMatch one-shot LUT path)."""
from .probe_parity.pipeline_cuda import build_merged_lut_probe_style_cuda
return build_merged_lut_probe_style_cuda(source_hwc, reference_hwc)
def color_match_session_info() -> dict[str, str | bool]:
"""Report active provider for color_match.onnx (creates session if needed)."""
sess = get_session_cuda()
return {
"model": "color_match.onnx",
"cuda_available": cuda_available(),
"active_provider": session_active_provider(sess),
"providers": list(sess.get_providers()),
}