diff --git a/olive/evaluator/lmms_ort.py b/olive/evaluator/lmms_ort.py new file mode 100644 index 000000000..f1f8948c2 --- /dev/null +++ b/olive/evaluator/lmms_ort.py @@ -0,0 +1,876 @@ +# ------------------------------------------------------------------------- +# ORT-GenAI wrapper for lmms-eval multimodal evaluation +# +# Supports evaluating multimodal ONNX +# models through the EvolvingLMMs-Lab/lmms-eval harness, mirroring how +# olive/evaluator/lmeval_ort.py wraps lm-evaluation-harness for text models. +# +# Registers an LMMSORTGenAIEvaluator class with lmms-eval's legacy +# @register_model registry under the name "ortgenai_mm". Consumers obtain it +# via lmms_eval.api.registry.get_model("ortgenai_mm"). +# ------------------------------------------------------------------------- +"""ORT-GenAI wrapper for lmms-eval multimodal evaluation.""" + +from __future__ import annotations + +import io +import json +import logging +import re +import tempfile +from functools import lru_cache +from pathlib import Path +from typing import Any + +import numpy as np +import PIL.Image +from tqdm import tqdm + +try: + import onnxruntime_genai as og +except ImportError: # pragma: no cover - optional dep + og = None + +try: + from lmms_eval.api.instance import Instance + from lmms_eval.api.model import lmms + from lmms_eval.api.registry import register_model + + _LMMS_EVAL_IMPORT_ERROR = None +except ImportError as e: # pragma: no cover - optional dep + Instance = Any + _LMMS_EVAL_IMPORT_ERROR = e + + class lmms: # noqa: N801 + pass + + def register_model(_name): + def decorator(cls): + return cls + + return decorator + + +logger = logging.getLogger(__name__) + + +@lru_cache(maxsize=1) +def warmup_image_decoder() -> None: + """Force ORT-GenAI's image decoder to initialize once, early in the process. + + ORT-GenAI's ``ImageDecoder`` statically links its own zlib. Pillow, torch, + ``decord`` and other libraries loaded by the HuggingFace evaluation path also + bundle their own zlib. Because these are static copies of the same symbols + (an ODR violation), the *first* library to actually decode an image binds its + zlib process-wide. When ``LMMSEvaluator`` runs an HF input-model evaluation + before the ORT-GenAI (ONNX) evaluation in the same process + (``evaluate_input_model: true``), the HF path decodes images first, so + ORT-GenAI's PNG decoder later binds the wrong zlib and fails with + ``libpng error: bad parameters to zlib`` on every image. + + Decoding a tiny throwaway PNG through ORT-GenAI here — before the HF path + runs — makes ORT-GenAI's zlib win the binding, so the later ONNX evaluation + decodes correctly. Idempotent and best-effort: a no-op if onnxruntime-genai + is unavailable or the warmup fails (nothing is broken for HF-only usage). + """ + if og is None: + return + try: + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "warmup.png" + PIL.Image.new("RGB", (8, 8), (0, 0, 0)).save(path, format="PNG") + # og.Images.open touches ORT-GenAI's libpng/zlib, binding its symbols. + og.Images.open(str(path)) + except Exception as e: # pragma: no cover - defensive, must never break a run + logger.debug("ORT-GenAI image decoder warmup skipped: %s", e) + + +_PROVIDER_ALIASES = { + "cuda": "cuda", + "cudaexecutionprovider": "cuda", + "gpu": "cuda", + "cpu": "cpu", + "cpuexecutionprovider": "cpu", + "dml": "dml", + "dmlexecutionprovider": "dml", + "directml": "dml", + "webgpu": "webgpu", + "webgpuexecutionprovider": "webgpu", + "js": "web", + "jsexecutionprovider": "web", + "nvtensorrtrtx": "NvTensorRtRtx", + "nvtensorrtrtxexecutionprovider": "NvTensorRtRtx", +} + + +# ----------------------------------------------------------------------------- +# Helpers +# ----------------------------------------------------------------------------- + + +_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"} +_AUDIO_SUFFIXES = {".wav", ".mp3", ".flac", ".ogg", ".m4a"} +_VIDEO_SUFFIXES = {".mp4", ".avi", ".mov", ".mkv", ".webm"} +_MEDIA_PLACEHOLDER_RE = re.compile(r"(|