Contributor Reference

Adding an estimator

Estimator classes in the Extension for scikit-learn* are wrappers over algorithms from the oneAPI Data Analytics Library. In order to add a new estimator, an example class DummyEstimator is available in the library, along with code comments and tests which explain how it should work. Estimators spawn multiple files, ranging from C++ wrappers from PyBind11, direct wrappers in the onedal/ module, scikit-learn-conformant wrappers over those in the sklearnex/ module, direct tests, configurations for general tests, and others.

Example estimator

The following files and folders might be of help when looking at how the example DummyEstimator works and what is needed of an estimator:

The following files might also require changes after adding a new estimator - look out for the “dummy” keyword:

Note

The library contains lots of classes with legacy code from previous designs that do not work in the same way as the DummyEstimator class, such as classes based off daal4py. New estimators should nevertheless not try to mimic those, and follow instead the design from DummyEstimator.

Tip

Another good reference example for how estimators should be implemented is sklearn.linear_model.LinearRegression from the sklearnex module.

For estimators that somehow depend on functionality that is only exposed through daal4py, an internal wrapper akin to the files under onedal/ must first be created under daal4py/sklearn, and then imported in a corresponding class on onedal/. Note that new functionalities in the oneAPI Data Analytics Library are meant to be introduced through the oneAPI interface, so only legacy functionalities should ever need to go through this route.

Version compatibilities

OneDAL

The Extension for scikit-learn* is intended to be backwards-compatible with different versions of the oneAPI Data Analytics Library, but not forwards-compatible except within a major release series - meaning: it is meant to run with a version of the oneAPI Data Analytics Library that is lower or equal than the version of the Extension for scikit-learn*, such that onedal==2025.0 + sklearnex==2025.0 and onedal==2025.0 + sklearnex==2025.2 should both work correctly, even though the latter might not expose the same functionalities with onedal==2025.0 as with onedal==2025.2.

This is achieved with conditional runtime checks of the library versions in order to determine whether some class or function or similar should be defined or not. This is provided through the provided function onedal_check_version for onedal and daal_check_version for daal4py. onedal_check_version accepts three integer inputs: major version, minor version, and update version. daal_check_version accepts a tuple as argument containing the major version number, the "P" string (other possibilities for this parameter are not used anymore), and the minor version multiplied by 100. So for example, if a given piece of code requires onedal>=2025.2, the function should be called as follows:

if onedal_check_version(2025, 2, 0):
    # code branch for onedal>=2025.2
else:
    # code branch for onedal<2025.2

# equivalent code for the daal4py module
if daal_check_version((2025, "P", 200)):
    # code branch for onedal>=2025.2
else:
    # code branch for onedal<2025.2

Hint

This helper is meant for usage in both source code and tests.

On C++ code, the macro ONEDAL_VERSION should be checked at compile-time for conditional code inclusions or exclusions. This macro contains a single integral number with the major version, followed by the minor version using 2 digits, and other patch versions using another two digits. For example, if a given piece of code requires onedal>=2025.2, the check would be as follows:

#if defined(ONEDAL_VERSION) && ONEDAL_VERSION >= 20250200
// code for newer version
#else
// code for older version
#endif

Scikit-learn

The Extension for scikit-learn* is intended to be compatible with multiple versions of scikit-learn. In order to achieve this compatibility, conditional runtime checks for the version of scikit-learn are executed in order to offer different code paths for different versions, through function sklearn_check_version, which accepts a string with the major and minor version as recognized by pip. For example, in order to have different code branches depending on sklearn>=1.7 (which would also trigger for sklearn==1.7.2, for example), the following can be used:

if sklearn_check_version("1.7"):
    # code branch for sklearn>=1.7
else:
    # code branch for sklearn<1.7

Other libraries

Beyond the oneAPI Data Analytics Library and scikit-learn, it is also necessary to be compatible with different versions of their transitive dependencies, such as SciPy. There are no dedicated functions to check the versions of these libraries, but a common helper daal4py.sklearn._utils._package_check_version can be used together with a version string.

For example:

import scipy
from daal4py.sklearn._utils import _package_check_version

if _package_check_version("1.14", scipy.__version__):
    # branch for scipy>=1.14
    ...
else:
    # branch for scipy<1.14
    ...

Test helpers

Note that not all estimators offer the same functionalities, and thus tests should be designed accordingly. The tests provide some custom marks, fixtures, and helpers that one might to use for some cases:

  • Automatic array API dispatch for on-device inputs: an autouse fixture (_array_api_dispatch_for_device_frameworks in sklearnex/conftest.py) inspects the dataframe parameter of each test and, when it is "dpnp" or "array_api", runs the test body inside with config_context(array_api_dispatch=True). This is applied automatically to every test parametrized with get_dataframes_and_queues — no per-test opt-in is needed. The rationale: non-NumPy array API inputs (e.g. dpnp) are only kept on device under array_api_dispatch; without it they are silently transferred to host and computed as NumPy, which leaves the on-device code paths untested and can hide device-specific correctness bugs. Two consequences to keep in mind when writing tests:

    • Under dispatch, fitted attributes and method outputs stay in the input’s namespace (a dpnp input yields dpnp results). Comparisons against a NumPy expected value must therefore convert the result first (see assert_allclose_numpy / _as_numpy below) — numpy.testing.assert_allclose cannot consume a dpnp array on either side.

    • Tests marked @pytest.mark.allow_sklearn_fallback are left untouched by this fixture (dispatch is not forced), since those tests intentionally exercise the host/fallback path. Use this mark for any dpnp test whose configuration is expected to fall back to scikit-learn rather than run on device. Note that such a test makes no statement about the Extension for scikit-learn* device code paths: scikit-learn supports array_api_dispatch for a growing set of estimators, so a fallback test given array API inputs may well succeed entirely within scikit-learn without ever entering sklearnex._device_offload. Treat the mark as an assertion about fallback behavior only, and cover the on-device path with a separate non-fallback test.

  • @pytest.mark.allow_sklearn_fallback: will avoid having tests fail when they end up calling procedures from scikit-learn instead of from the oneAPI Data Analytics Library. This can be helpful for example when testing that some corner case falls back correctly when it should. It also disables the automatic array API dispatch described above.

  • onedal.tests.utils._dataframes_support._as_numpy: this function can be used to convert an input array or data frame to NumPy, regardless of whether it lives on host or on device, and regardless of array API support.

  • onedal.tests.utils._dataframes_support.assert_allclose_numpy: a drop-in replacement for numpy.testing.assert_allclose that routes both operands through _as_numpy first, so either side may be a dpnp or other array API result. All remaining arguments (rtol, atol, err_msg, …) are forwarded unchanged. Prefer it over calling _as_numpy at each comparison site.

  • onedal.tests.utils._dataframes_support._assert_in_namespace: asserts that an object belongs to the array namespace implied by the test’s dataframe parameter (e.g. a "dpnp" test must produce a dpnp result), ignoring scalars. Use it once per test on a result-side value to verify that outputs stay on device (dpnp-in → dpnp-out) rather than being silently host-transferred. It is deliberately separate from assert_allclose_numpy so that the namespace check is an explicit, intentional assertion rather than a side effect of every value comparison. PyTorch is checked by tensor type instead of __array_namespace__, which torch.Tensor does not implement since scikit-learn reaches PyTorch through array_api_compat; an optional device argument (a SYCL queue, another array, or a torch.device) additionally asserts the result landed on the expected device.

  • onedal.tests.utils._dataframes_support.skip_array_api_strict_readonly: skips a test when the dataframe parameter is "array_api" and NumPy is older than 2.2.5. Needed only for estimators that feed fitted attributes back through to_table (e.g. PCA/IncrementalPCA components_, DummyRegressor constant_), since older NumPy hands those arrays back read-only and the DLPack export rejects them.

  • onedal.tests.utils._dataframes_support.dpnp_available: a boolean variable that checks for whether the DPNP library is importable at the moment of running the tests. Note that, even if available, it will not be imported automatically in the file where the test happens, so import dpnp needs to be placed inside the test, or a conditional import needs to be done outside of it.

  • onedal.tests.utils._dataframes_support.torch_available: a boolean variable akin to dpnp_available, but for PyTorch.

  • onedal.tests.utils._dataframes_support.torch_xpu_available: a boolean variable telling whether an XPU device (what PyTorch calls Intel GPUs) is available. This can only be True if torch_available is also True.

  • onedal.tests.utils._dataframes_support.mixed_device_params: a generator of pytest.param entries for tests that pass inputs living on different devices to the same estimator, to be used with @pytest.mark.parametrize alongside the with_array_api fixture. It yields (X_xp, X_device, y_xp, y_device) tuples, optionally extended with a sample weight (include_weight=True). Everything follows X — the estimator moves y and sample weights to X’s namespace and device — so only devices vary within one namespace here; cross-namespace X/y (e.g. PyTorch X with DPNP y) is supported from scikit-learn 1.9 on, but requires both frameworks installed and is covered by the test_*_mixed_array_namespaces tests instead. Pass include_host_df_y=True to also cover a namespace-neutral host data frame y/weight, one per library in host_df_modules, and x_devices to restrict X for estimators supporting only some devices (e.g. x_devices=("cpu",)).

  • onedal.tests.utils._dataframes_support.host_df_modules: the tuple of host data frame libraries usable as a namespace-neutral y/sample weight alongside any X — Pandas and Polars, both mandatory test dependencies. Tests fed by mixed_device_params(include_host_df_y=True) should branch on y_xp in host_df_modules rather than comparing against pd, and construct data through the yielded module (y_xp.Series(...)) so that both libraries are exercised.

  • onedal.tests.utils._dataframes_support.host_df_to_torch_working: a boolean variable telling whether scikit-learn’s move_to can convert a host data frame object (Pandas, Polars) to a PyTorch tensor. It is False for versions affected by scikit-learn#34046, in which the host round-trip fallback for inputs lacking __dlpack__ is skipped because PyTorch signals that case with an exception type that move_to does not catch. Other namespaces (e.g. array_api_strict) convert such inputs without issue, and any host data frame is affected rather than just Pandas, so the Pandas probe behind this variable is representative of Polars too. Tests that pass a host data frame y or sample weight alongside a PyTorch X should skip on not host_df_to_torch_working rather than skipping for PyTorch unconditionally, so that they start running again once the scikit-learn fix is released.

  • pass_if_not_implemented_for_gpu: skips tests not implemented for GPU when GPU support is enabled. Requires a skip reason argument that matches the backend’s error message.

  • with_array_api: fixture that makes the test run under array API dispatching, equivalent to putting the test under with confix_context(array_api_dispatch=True). Note that this is a fixture rather than a mark, so it should be specified as a function argument in the test definition.

Tests with optional dependencies

Tests that require optional dependencies in order to execute should have a conditional skip logic through usage of @pytest.mark.skipif. The test files are meant to be executable without the optional dependencies being installed, so they should be imported conditionally or in a try + except ImportError block.

Note that, in the case of DPNP, there is a special variable onedal.tests.utils._dataframes_support.dpnp_available should be reused for conditional skips (see section above).

SPMD tests

Tests that involve distributed mode functionalities should rely on pytest-mpi and need to be marked with @pytest.mark.mpi.

Running benchmarks

As this library aims to offer accelerated versions of algorithms, when it comes to adding or modifying estimators and related helper functions, it is usually helpful - and in many cases required - to conduct benchmarks to assess the performance implications of changes, whether against scikit-learn or against the current version of the Extension for scikit-learn* when introducing changes.

Benchmarks are usually conducted through the scikit-learn_bench tool, which lives in a different repository. See the instructions in that repository for how to run the appropriate benchmarks.

Results from benchmarks are usually shared as a relative improvement over the baseline being compared against, which will be available in the sheets of the generated .xlsx comparison reports from that repository. Usually, the geometric mean is used as a final number, but changes for individual datasets and estimator methods are typically still of interest within a given pull request.

Building the documentation

The source code for the documentation being rendered here is available from the same repository as the library’s source code, and hosted on GitHub pages through automated deployments. The source code for the documentation is written in Sphinx, taking some docstrings from the classes and functions in the library to render them.

Thus, building the documentation from source requires being able to import the library in the same Python environment that is building the documentation, in addition to having all of the Python packages used by the Sphinx built script, such as Sphinx itself and the Sphinx extensions used throughout these docs.

Building documentation locally

For development purposes, it’s helpful to build the docs locally to inspect them offline without deploying, based off the current version of the source code instead of a public release version. This can be done using the provided scripts in this repository.

Requirements

Being based off Sphinx, the scripts for building documentation require a Python environment with documentation-related packages installed. The locked requirements (and note that in many cases specific versions of the dependencies might be needed) are available in file requirements-doc.txt. They can be installed from the root of the repository as follows:

pip install -r requirements-doc.txt

Tip

It’s advised to create a separate Python environment for building the docs due to the locked requirements and version conflicts with what’s used for the tests.

Instructions

With the necessary dependencies being installed, the docs can then be built locally on Linux* by executing the following script from the root of the repository:

./doc/build-doc.sh

Note

The script accepts additional arguments and environment variables which are used for the versioned doc pages hosted on GitHub pages. Those are not meant to be used for local development.

The script will copy over necessary files to the docs folder and make calls to Sphinx to build the docs as HTML. After that script is executed for the first time, if no new embedded notebooks / examples from .py files have been added, the docs can be built without the script using the provided Makefile:

cd doc
make clean
make html

Note

The docs can be built on Windows* using the file make.bat, but be aware that it will not render everything correctly if the commands from build-doc.sh that copy files haven’t been executed.