.. _l-design-backend-tests:
Using backend tests to evaluate a runtime
==========================================
This page explains how to use the backend test suite shipped with
*onnx-light* to validate that a custom ONNX runtime produces correct
numerical results.
The backend test infrastructure is located in
:mod:`onnx_light.backend.test.case` and mirrors the structure of the
official ONNX backend test suite. The registered node test cases are
generated by the C++ ``lib_onnx_backend_test`` library and exposed to
Python through :func:`~onnx_light.backend.test.case.base.collect_test_case`.
Downstream code can still register additional Python-only test cases
by subclassing :class:`~onnx_light.backend.test.case.base.Base` and
calling the :func:`~onnx_light.backend.test.case.base.expect` helper.
The :func:`~onnx_light.backend.test.case.base.make_test_class` function
then turns those test cases into a standard :class:`unittest.TestCase`
subclass that calls into a user-supplied runtime function.
.. contents::
:local:
:depth: 2
----
Defining a runtime function
----------------------------
The only requirement for plugging in a runtime is to write a callable
with the following signature:
.. code-block:: python
def my_runtime(model, *inputs: np.ndarray) -> list[np.ndarray]:
...
where
* ``model`` is an :class:`onnx_light.onnx.ModelProto` (the ONNX model
for the test case),
* ``*inputs`` are :class:`numpy.ndarray` objects corresponding to the
model's graph inputs in order, and
* the return value is a list of :class:`numpy.ndarray` objects
corresponding to the model's graph outputs in order.
The runtime may serialize the ``ModelProto`` to bytes, pass it to any
ONNX-compatible engine, and return the results.
----
Generating a test class
-----------------------
Call :func:`~onnx_light.backend.test.case.base.make_test_class` with
the runtime callable to obtain a
:class:`~onnx_light.ext_test_case.ExtTestCase` subclass whose methods
are one test per registered test case:
.. code-block:: python
import unittest
import numpy as np
from onnx_light.backend.test.case import make_test_class
def my_runtime(model, *inputs: np.ndarray) -> list[np.ndarray]:
# replace with the actual engine call
raise NotImplementedError
MyBackendTests = make_test_class(my_runtime)
if __name__ == "__main__":
unittest.main(verbosity=2)
Running the file with ``python`` or through any unittest-compatible
runner (pytest, etc.) will execute every registered node test case and
report failures when the runtime output differs from the expected output.
----
Filtering tests
---------------
Two optional parameters let you restrict which test cases are executed.
``include_regex``
A list of regular-expression patterns. Only test cases whose name
matches *at least one* pattern are kept.
``exclude_regex``
A list of regular-expression patterns. Test cases whose name
matches *at least one* pattern are discarded (evaluated before
``include_regex``).
Example — run only tests related to element-wise arithmetic:
.. code-block:: python
ArithmeticTests = make_test_class(
my_runtime,
include_regex=[r"^test_add", r"^test_sub", r"^test_mul", r"^test_div"],
)
Example — run everything except the quantization operators:
.. code-block:: python
NoQuantTests = make_test_class(
my_runtime,
exclude_regex=[r"quantize", r"dequantize"],
)
----
Adjusting numerical tolerances
--------------------------------
By default each test case uses ``atol=1e-7`` and ``rtol=1e-3``. These
values can be overridden globally per test-case name via the ``atols``
and ``rtols`` dictionaries:
.. code-block:: python
MyBackendTests = make_test_class(
my_runtime,
atols={"test_cast_FLOAT_to_FLOAT16": 1e-3},
rtols={"test_cast_FLOAT_to_FLOAT16": 1e-2},
)
----
Filtering test cases by operator and opset
--------------------------------------------
The helper
:func:`~onnx_light.backend.test.case.base.get_test_cases_for_op` returns
the subset of collected backend test cases whose model contains a node
with a given ``op_type`` (and optionally a given ``domain`` /
``opset_version``). This is convenient when a backend wants to focus on
a single operator (and version) at a time:
.. code-block:: python
from onnx_light.backend.test.case import get_test_cases_for_op
# All cases that exercise Abs in the default ai.onnx domain.
abs_cases = get_test_cases_for_op("Abs")
# Cases that import ai.onnx at exactly version 13 and use Abs.
abs_v13 = get_test_cases_for_op("Abs", opset_version=13)
# Cases that use Abs from a custom domain.
custom = get_test_cases_for_op("Abs", domain="my.custom.domain")
When called without ``test_cases``, the helper calls
:func:`~onnx_light.backend.test.case.base.collect_test_case` internally.
A precomputed mapping can be passed via the ``test_cases`` argument to
avoid recollecting test cases on repeated lookups.
----
Full example: ONNXRuntime backend
-----------------------------------
The file ``unittests/backend/test_backend_with_onnxruntime.py`` in the
repository is a ready-to-run example that exercises every registered
backend test case through
`ONNXRuntime `_:
.. literalinclude:: ../../unittests/backend/test_backend_with_onnxruntime.py
:language: python
The runtime function serialises the :class:`~onnx_light.onnx.ModelProto`
to bytes with :meth:`~onnx_light.onnx.ModelProto.SerializeToString`,
creates an ``onnxruntime.InferenceSession``, and returns the inference
outputs.
Run it with:
.. code-block:: bash
python -m pytest unittests/backend/test_backend_with_onnxruntime.py -v
or, to run only the ``Abs`` test cases:
.. code-block:: bash
python -m pytest unittests/backend/test_backend_with_onnxruntime.py -v -k abs
----
How test cases are collected
-----------------------------
:func:`~onnx_light.backend.test.case.base.collect_test_case` first
collects every node test case registered by the C++
``lib_onnx_backend_test`` library (exposed through the
``onnx_light.onnx_py._onnxpy.backend_test`` Python bindings). It then
runs every ``export_*`` class method declared on any user-defined
subclass of :class:`~onnx_light.backend.test.case.base.Base`; each call
to :func:`~onnx_light.backend.test.case.base.expect` appends one
:class:`~onnx_light.backend.test.case.base.TestCase` to the global
``ALL_TESTS`` dictionary. Python-defined cases take precedence over
C++ cases with the same name.
:func:`~onnx_light.backend.test.case.base.make_test_class` calls
:func:`~onnx_light.backend.test.case.base.collect_test_case` internally,
so tests are always re-collected from scratch when the function is called.
----
Running backend tests in C++
-----------------------------
The exact same node test cases are also available directly from C++ via
the ``lib_onnx_backend_test`` static library, with no dependency on
Python. The library lives in ``onnx_light/onnx_backend_test/`` and only
depends on ``lib_onnx_proto``. It exposes:
* a runtime :cpp:struct:`onnx::onnx_backend_test::Tensor` (distinct from
:cpp:class:`onnx::TensorProto`) that stores raw element bytes,
* a :cpp:struct:`onnx::onnx_backend_test::TestCase` bundle of
:cpp:class:`onnx::ModelProto` and expected input/output data sets,
* the :cpp:func:`onnx::onnx_backend_test::Expect` helper used by every
``RegisterXxxCases`` function to register a single-node model, and
* :cpp:func:`onnx::onnx_backend_test::CollectTestCases`, which returns
the full registry of node test cases (the same registry that the
Python bindings expose through
``onnx_light.onnx_py._onnxpy.backend_test``).
Per-operator cases are organised under
``onnx_light/onnx_backend_test/cases//`` (``math``, ``logical``,
``nn``, ``tensor``, …) and the expected outputs are computed with the
reference kernels under
``onnx_light/onnx_backend_test/kernels//`` so the registry is
fully self-contained and deterministic.
A minimal C++ runtime evaluator therefore looks like:
.. code-block:: cpp
#include "onnx_backend_test/test_case.h"
using namespace onnx::onnx_backend_test;
int main() {
std::vector cases = CollectTestCases();
for (const TestCase &tc : cases) {
// Serialize tc.model and run it through your engine, then
// compare against tc.data_sets[*].outputs using tc.atol / tc.rtol.
}
return 0;
}
The library ships its own GoogleTest-based unit tests under
``unittests/cc_onnx_backend_test/``. To build and run them, configure
the project with ``ONNX_LIGHT_BUILD_TESTS=ON`` and use ``ctest``:
.. code-block:: bash
cmake -S . -B build -DONNX_LIGHT_BUILD_TESTS=ON
cmake --build build -j
ctest --test-dir build -R Backend --output-on-failure
The ``-R`` regex can be tightened (for example ``-R BackendKernelClass``)
to focus on a single test group.
----
See also
--------
* :ref:`l-api-backend` — Python API reference for the backend module.
* :doc:`../api/cpp/onnx_backend_test/index` — C++ API reference for
the ``lib_onnx_backend_test`` library.