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:
Files under folder onedal/dummy/.
Files under folder sklearnex/dummy/.
The following files might also require changes after adding a new estimator - look out for the “dummy” keyword:
Import-related files:
Test-related files:
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
autousefixture (_array_api_dispatch_for_device_frameworksinsklearnex/conftest.py) inspects thedataframeparameter of each test and, when it is"dpnp"or"array_api", runs the test body insidewith config_context(array_api_dispatch=True). This is applied automatically to every test parametrized withget_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 underarray_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_numpybelow) —numpy.testing.assert_allclosecannot consume a dpnp array on either side.Tests marked
@pytest.mark.allow_sklearn_fallbackare 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 supportsarray_api_dispatchfor a growing set of estimators, so a fallback test given array API inputs may well succeed entirely within scikit-learn without ever enteringsklearnex._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 fornumpy.testing.assert_allclosethat routes both operands through_as_numpyfirst, 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_numpyat each comparison site.onedal.tests.utils._dataframes_support._assert_in_namespace: asserts that an object belongs to the array namespace implied by the test’sdataframeparameter (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 fromassert_allclose_numpyso 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__, whichtorch.Tensordoes not implement since scikit-learn reaches PyTorch througharray_api_compat; an optionaldeviceargument (a SYCL queue, another array, or atorch.device) additionally asserts the result landed on the expected device.onedal.tests.utils._dataframes_support.skip_array_api_strict_readonly: skips a test when thedataframeparameter is"array_api"and NumPy is older than 2.2.5. Needed only for estimators that feed fitted attributes back throughto_table(e.g.PCA/IncrementalPCAcomponents_,DummyRegressorconstant_), 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, soimport dpnpneeds 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 todpnp_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 beTrueiftorch_availableis alsoTrue.onedal.tests.utils._dataframes_support.mixed_device_params: a generator ofpytest.paramentries for tests that pass inputs living on different devices to the same estimator, to be used with@pytest.mark.parametrizealongside thewith_array_apifixture. It yields(X_xp, X_device, y_xp, y_device)tuples, optionally extended with a sample weight (include_weight=True). Everything followsX— the estimator movesyand sample weights toX’s namespace and device — so only devices vary within one namespace here; cross-namespaceX/y(e.g. PyTorchXwith DPNPy) is supported from scikit-learn 1.9 on, but requires both frameworks installed and is covered by thetest_*_mixed_array_namespacestests instead. Passinclude_host_df_y=Trueto also cover a namespace-neutral host data framey/weight, one per library inhost_df_modules, andx_devicesto restrictXfor 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-neutraly/sample weight alongside anyX— Pandas and Polars, both mandatory test dependencies. Tests fed bymixed_device_params(include_host_df_y=True)should branch ony_xp in host_df_modulesrather than comparing againstpd, 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’smove_tocan convert a host data frame object (Pandas, Polars) to a PyTorch tensor. It isFalsefor 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 thatmove_todoes 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 frameyor sample weight alongside a PyTorchXshould skip onnot host_df_to_torch_workingrather 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 underwith 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.
Copyright headers
Each new file added to the project must include the following copyright notice - note that this project is closely tied to the oneAPI Data Analytics Library and hence shares the same copyright header. The following copyright headers should be used:
For Python and YAML files:
# ============================================================================== # Copyright contributors to the oneDAL project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ==============================================================================
For C++ files:
/* * Copyright contributors to the oneDAL project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */
For markdown files:
<!-- ******************************************************************************** * Copyright contributors to the oneDAL project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. *******************************************************************************/-->
For javascript files:
// Copyright contributors to the oneDAL project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License.
For rst files:
.. Copyright contributors to the oneDAL project .. .. Licensed under the Apache License, Version 2.0 (the "License"); .. you may not use this file except in compliance with the License. .. You may obtain a copy of the License at .. .. http://www.apache.org/licenses/LICENSE-2.0 .. .. Unless required by applicable law or agreed to in writing, software .. distributed under the License is distributed on an "AS IS" BASIS, .. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. .. See the License for the specific language governing permissions and .. limitations under the License.
If for some reason it doesn’t make sense to include this copyright header in a text-based file (e.g. json files), said file needs to be added to the exclusion list, but this should be a rare occurrence.