Files
lm-evaluation-harness/lm_eval/models/winml.py
T
b0195a94f4 feat(models): add cross-platform onnxruntime-genai backend + refactor winml (#3960)
* feat(models): add cross-platform onnxruntime-genai backend + refactor winml

Add a cross-platform `onnxruntime-genai` backend that runs ONNX LLMs exported
by the ONNX Runtime GenAI Model Builder on CPU/CUDA/DirectML/WebGPU/AMD-NPU via
the `og.Config` provider API. All lm-eval logic (single-pass log-likelihood,
multiple-choice, rolling perplexity, generation) lives in
`ONNXRuntimeGenAILM(TemplateLM)`, following the `OptimumLM(HFLM)` single-file
subclass precedent.

Refactor `winml` into a thin `WindowsML(ONNXRuntimeGenAILM)` subclass that
overrides only execution-provider selection, inheriting the shared scoring
core. This removes the previous winml quality debt: the per-token
teacher-forcing loop (now a single `get_output("logits")` pass), the direct
`loglikelihood` override that bypassed `TemplateLM` token handling, and the
silent `(0.0, False)`-on-exception path that corrupted scores.

Add a shared parametrized test suite (unit tests for the scoring math with a
fake `_forward_logits`, plus CPU smoke tests over hellaswag/wikitext/gsm8k
using a tiny Model Builder fixture), packaging extra, and docs. The test is
added to the CI `--ignore` list since the ONNX runtimes are not installed in CI.

Part of QUARK-606 Phase 1. The raw `onnxruntime` backend, cross-backend parity
CI, OpenAI-compatible server, and Quark migration are follow-up stages.

Co-Authored-By: Claude <[email protected]>

* docs: keep model_guide subclassing tip generic

Trim the subclassing note to the pre-existing OptimumLM example instead of
documenting the onnxruntime-genai backend internals, which belong in the
backend's own docstrings/README rather than the general model-authoring guide.

Co-Authored-By: Claude <[email protected]>

* test: split winml tests into their own module

Move winml-specific coverage (registration, subclass relationship, non-Windows
CPU fallback) into tests/models/test_winml.py, leaving test_onnxruntime_genai.py
focused on the genai backend and its scoring math. Keeps each backend's tests
self-contained and adds the new file to the CI ignore list.

Co-Authored-By: Claude <[email protected]>

* fix(onnxruntime-genai): honor genai_config.json providers by default

execution_provider now defaults to None, meaning "use the providers the export
declares in genai_config.json" — og.Config(model_dir) seeds its provider list
from that file, and _select_ep no longer calls clear_providers() unless the
user explicitly asks for a provider. Previously the "cpu" default cleared the
list and appended nothing, silently downgrading a CUDA/DML/NPU export to CPU.
Pass execution_provider=cpu to force CPU, or a provider name to override.

winml pins its historical max_length/max_gen_toks (4096) so existing runs are
numerically unchanged, while the cross-platform backend keeps model-aware
defaults (context_length from the config; max_gen_toks=256).

Add regression tests for the three _select_ep branches and winml's defaults.

Co-Authored-By: Claude <[email protected]>

* pacify pre-commmit

---------

Co-authored-by: Claude <[email protected]>
Co-authored-by: Baber Abbasi <[email protected]>
2026-08-21 17:20:42 +05:00

153 lines
5.7 KiB
Python

"""WinML backend for lm-eval-harness with NPU/GPU/CPU support.
This backend runs Model Builder ONNX models through the Windows Machine Learning
(WinML) execution-provider catalog. It is a thin specialization of
:class:`~lm_eval.models.onnxruntime_genai.ONNXRuntimeGenAILM`: all scoring and
generation logic is inherited, and only execution-provider selection is
Windows-specific.
Example usage:
lm_eval --model winml --model_args pretrained=path/to/onnx/model --tasks hellaswag
"""
import logging
from pathlib import Path
from lm_eval.api.registry import register_model
from lm_eval.models.onnxruntime_genai import ONNXRuntimeGenAILM
eval_logger = logging.getLogger(__name__)
@register_model("winml")
class WindowsML(ONNXRuntimeGenAILM):
"""WindowsML backend: onnxruntime-genai with Windows ML provider selection.
Inherits all lm-eval logic (tokenization, single-pass log-likelihood,
rolling perplexity, and generation) from
:class:`~lm_eval.models.onnxruntime_genai.ONNXRuntimeGenAILM`, overriding
only :meth:`_select_ep` to register execution providers through the Windows
ML catalog.
The default ``max_length`` / ``max_gen_toks`` are pinned to the historical
winml values so numeric behavior is unchanged; the cross-platform backend
picks its own (more model-aware) defaults.
"""
# Historical winml defaults, kept so existing winml runs are unaffected.
_WINML_MAX_LENGTH = 4096
_WINML_MAX_GEN_TOKS = 4096
def __init__(
self,
pretrained: str,
max_length: int | None = None,
max_gen_toks: int | None = None,
**kwargs,
) -> None:
super().__init__(
pretrained,
max_length=max_length or self._WINML_MAX_LENGTH,
max_gen_toks=max_gen_toks or self._WINML_MAX_GEN_TOKS,
**kwargs,
)
def _select_ep(self, config) -> None:
"""Register Windows ML execution providers on the ``og.Config``.
Discovers providers via the Windows ML catalog and registers them with
onnxruntime-genai. If discovery fails (e.g. running outside Windows), we
fall back to the cross-platform selection in the base class.
"""
self._fix_winrt_runtime()
if self._register_winml_providers_to_genai():
self._log_winml_devices()
else:
eval_logger.warning(
"Windows ML provider registration failed; falling back to "
"cross-platform execution-provider selection."
)
super()._select_ep(config)
def _fix_winrt_runtime(self) -> None:
"""Remove the bundled msvcp140.dll from winrt-runtime to avoid clashes.
This DLL, shipped by the ``winrt-runtime`` package, can conflict with
other libraries loaded in the same process.
"""
try:
from importlib import metadata
except ImportError:
return
try:
site_packages_path = Path(
str(metadata.distribution("winrt-runtime").locate_file(""))
)
except metadata.PackageNotFoundError:
return
dll_path = site_packages_path / "winrt" / "msvcp140.dll"
if dll_path.exists():
dll_path.unlink()
def _register_winml_providers_to_genai(self) -> bool:
"""Discover Windows ML providers and register them with onnxruntime-genai.
Returns:
True if registration succeeded, False otherwise.
"""
try:
import winui3.microsoft.windows.ai.machinelearning as winml
from winui3.microsoft.windows.applicationmodel.dynamicdependency.bootstrap import (
InitializeOptions,
initialize,
)
with initialize(options=InitializeOptions.ON_NO_MATCH_SHOW_UI):
catalog = winml.ExecutionProviderCatalog.get_default()
providers = catalog.find_all_providers()
for provider in providers:
provider.ensure_ready_async().get()
self.og.register_execution_provider_library(
provider.name, provider.library_path
)
eval_logger.info(
"Registered %s to ONNX Runtime GenAI", provider.name
)
return True
except ImportError as e:
eval_logger.warning("Windows ML import error: %s", e)
return False
except Exception as e: # noqa: BLE001
eval_logger.warning("Error registering providers to GenAI: %s", e)
return False
def _log_winml_devices(self) -> None:
"""Log available execution-provider devices via the Windows ML API."""
try:
import onnxruntime as ort
ep_devices = ort.get_ep_devices()
ep_device_map: dict[str, list] = {}
for device in ep_devices:
ep_device_map.setdefault(device.ep_name, []).append(device)
eval_logger.info(
"Available EP devices: %s execution providers", len(ep_device_map)
)
for name, devices in ep_device_map.items():
eval_logger.info("Execution Provider: %s", name)
for device in devices:
try:
device_type = ort.OrtHardwareDeviceType(device.device.type).name
except Exception: # noqa: BLE001
device_type = "Unknown"
eval_logger.info(
" | Vendor: %-16s | Device Type: %-8s",
device.ep_vendor,
device_type,
)
except Exception as e: # noqa: BLE001
eval_logger.warning("Windows ML device enumeration failed: %s", e)