Source code for openstef_foundation_models.inference.onnx_backend

# SPDX-FileCopyrightText: 2025 Contributors to the OpenSTEF project <openstef@lfenergy.org>
#
# SPDX-License-Identifier: MPL-2.0

"""ONNX Runtime execution backend.

Importing this module requires ONNX Runtime (the ``[cpu]`` or ``[gpu]`` extra)
and raises :class:`MissingExtraError` if it is missing.
"""

import logging
from collections.abc import Mapping, Sequence
from typing import Self

import numpy as np

from openstef_core.exceptions import MissingExtraError
from openstef_foundation_models.inference.provider_selection import (
    DefaultProviderPolicy,
    HostCapabilities,
    ProviderPolicy,
)
from openstef_foundation_models.inference.providers import (
    ExecutionProvider,
    SessionOptionsConfig,
)
from openstef_foundation_models.models.checkpoint import CheckpointMetadata, ResolvedCheckpoint

try:
    import onnxruntime as ort
except ImportError as e:
    raise MissingExtraError("onnxruntime", "openstef-foundation-models", install_extra="cpu") from e

# onnxruntime-gpu ships the CUDA execution-provider plugin but loads the CUDA/cuDNN
# runtime (the nvidia-*-cu12 wheels the [gpu] extra pulls) lazily at session creation.
# preload_dlls() loads them from the nvidia site-packages so the CUDA provider can be
# realized without a system CUDA install or LD_LIBRARY_PATH. It is a no-op on the CPU
# runtime; the guard covers onnxruntime < 1.21, which predates the API.
if hasattr(ort, "preload_dlls"):
    ort.preload_dlls()

logger = logging.getLogger(__name__)


[docs] class OnnxBackend: """An :class:`~openstef_foundation_models.inference.backend.InferenceBackend` backed by ONNX Runtime. The session is built once on construction and reused for every :meth:`run` call, so a single backend instance can be shared across an entire backtest. Users may either let the backend build a session from a resolved checkpoint and provider chain, or pass a pre-built session they own. """
[docs] def __init__( self, metadata: CheckpointMetadata, session: ort.InferenceSession, ) -> None: """Wrap a pre-built ONNX Runtime session. Prefer :meth:`from_checkpoint` unless you need to own the session lifecycle yourself. Args: metadata: Metadata describing the checkpoint the session executes. session: A pre-built ONNX Runtime inference session. """ self._metadata = metadata self._session: ort.InferenceSession | None = session
[docs] @classmethod def from_checkpoint( cls, checkpoint: ResolvedCheckpoint, providers: Sequence[ExecutionProvider] | None = None, session_options: SessionOptionsConfig | None = None, *, policy: ProviderPolicy | None = None, ) -> Self: """Build a backend by loading a checkpoint into a new ONNX Runtime session. With ``providers=None`` the *policy* selects a chain from the checkpoint and host; an explicit ``providers`` list is used as given and *policy* is ignored. See :class:`~openstef_foundation_models.inference.provider_selection.ProviderPolicy` for how a chain is chosen and how strictly its realization is enforced. Args: checkpoint: The resolved checkpoint (weights + metadata) to load. providers: Ordered execution providers to try. ``None`` lets the policy pick a host-appropriate chain from the checkpoint metadata. session_options: Optional ONNX Runtime session options. policy: Selection policy used when ``providers is None``. Defaults to :class:`DefaultProviderPolicy`. Returns: A backend wrapping the newly built session. """ metadata = checkpoint.metadata if providers is not None: provider_configs = list(providers) explicit = True logger.info( "Using explicit execution-provider chain %s for checkpoint '%s'.", [config.to_ort()[0] for config in provider_configs], metadata.model_family, ) else: selector = policy or DefaultProviderPolicy() host = HostCapabilities.detect() provider_configs = selector.select(metadata, host) explicit = False logger.debug( "Detected host: platform=%s, available_providers=%s", host.platform, sorted(host.available_providers), ) logger.info( "%s selected execution-provider chain %s for checkpoint '%s' (precision=%s, static_shapes=%s) on %s.", type(selector).__name__, [config.to_ort()[0] for config in provider_configs], metadata.model_family, metadata.precision, metadata.static_shapes, host.platform, ) ort_providers = [config.to_ort() for config in provider_configs] so = _build_session_options(session_options) if session_options else None session = ort.InferenceSession( str(checkpoint.weights_path), sess_options=so, providers=ort_providers, ) logger.info("ONNX Runtime session built; realized providers: %s.", session.get_providers()) _check_provider_fallback( requested=provider_configs, realized=session.get_providers(), strict=explicit, ) return cls(metadata=checkpoint.metadata, session=session)
@property def metadata(self) -> CheckpointMetadata: """Metadata describing the checkpoint this backend executes.""" return self._metadata
[docs] def run(self, inputs: Mapping[str, np.ndarray]) -> Mapping[str, np.ndarray]: """Execute the ONNX model on a batch of named input tensors. Args: inputs: Named input tensors. Keys must match ``metadata.input_names``. Returns: Named output tensors keyed by the model's output names. Raises: RuntimeError: If the backend has been closed. """ if self._session is None: msg = "OnnxBackend has been closed." raise RuntimeError(msg) output_names = [out.name for out in self._session.get_outputs()] results = self._session.run(output_names, dict(inputs)) return {name: np.asarray(result) for name, result in zip(output_names, results, strict=True)}
[docs] def close(self) -> None: """Release the underlying ONNX Runtime session. ONNX Runtime frees native resources on garbage collection, so dropping the reference is the supported way to release them. """ self._session = None
def _build_session_options(config: SessionOptionsConfig) -> ort.SessionOptions: """Translate a :class:`SessionOptionsConfig` into ONNX Runtime session options. Args: config: The typed session-options configuration. Returns: The corresponding ONNX Runtime ``SessionOptions``. """ so = ort.SessionOptions() so.graph_optimization_level = getattr( ort.GraphOptimizationLevel, f"ORT_{config.graph_optimization_level}", ) if config.intra_op_num_threads is not None: so.intra_op_num_threads = config.intra_op_num_threads if config.inter_op_num_threads is not None: so.inter_op_num_threads = config.inter_op_num_threads return so def _check_provider_fallback( requested: Sequence[ExecutionProvider], realized: Sequence[str], *, strict: bool, ) -> None: """Detect and report a silent fallback to the CPU execution provider. Compares the requested chain against what ONNX Runtime actually realized. See :class:`~openstef_foundation_models.inference.provider_selection.ProviderPolicy` for the strict-vs-graceful contract this enforces. Args: requested: The execution providers that were requested. realized: The provider names ONNX Runtime actually loaded. strict: When ``True``, raise on any missing accelerator; otherwise warn only on a full fallback to CPU. Raises: RuntimeError: If ``strict`` is set and a requested accelerator is missing. """ requested_names = {config.to_ort()[0] for config in requested} accelerators = requested_names - {"CPUExecutionProvider"} if not accelerators: return realized_set = set(realized) missing = accelerators - realized_set if strict: if missing: msg = ( f"Requested execution provider(s) {sorted(missing)} were not realized; " f"ONNX Runtime fell back to {realized}." ) raise RuntimeError(msg) return if accelerators & realized_set: return logger.warning( "No requested accelerator (%s) was realized; ONNX Runtime fell back to %s. Inference will run on CPU.", sorted(accelerators), realized, )