diff --git a/doc/sources/input-types.rst b/doc/sources/input-types.rst index 8e1ce8427d..acdca9d523 100644 --- a/doc/sources/input-types.rst +++ b/doc/sources/input-types.rst @@ -30,8 +30,8 @@ Just like |sklearn| estimators, estimators from the |sklearnex| are able to work - Pandas :external+pandas:doc:`DataFrame and Series ` classes. - Other DataFrame classes recognize by data validators from |sklearn|, such as the ones from `Polars `__. -In addition, |sklearnex| also supports |dpnp_array| arrays (with and without array API mode) in estimators with -:ref:`GPU support ` (see also :doc:`array_api`). +In addition, |sklearnex| also supports |dpnp_array| arrays in estimators with +:ref:`GPU support ` when array API mode is enabled (see also :doc:`array_api`). |sklearnex| currently does not offer accelerated routines for input types not listed here - when receiving an unsupported class, estimators will either convert to a supported class under some @@ -51,7 +51,7 @@ enabled but the input is unsupported). - If a SYCL queue is used for :ref:`target_offload` for a device without ``float64`` support but data is ``float64``, data will be converted to ``float32``. - If a |dpnp_array| array on GPU is used as input without :doc:`array_api` being enabled, then data will be transferred - to CPU for validations and then back to GPU for computations (see :doc:`oneapi-gpu` for details). + to CPU and the result returned as a NumPy array (see :doc:`oneapi-gpu` for details). - If a |dpnp_array| array on GPU is passed to an estimator with :ref:`GPU support ` but the requested operation is not supported on GPU, and if array API is not enabled or |sklearn| does not support array API for the requested operation, then data might be transferred to CPU and the operation done there (see :doc:`config-contexts`). diff --git a/doc/sources/oneapi-gpu.rst b/doc/sources/oneapi-gpu.rst index 75be29aac7..0fe0596e08 100644 --- a/doc/sources/oneapi-gpu.rst +++ b/doc/sources/oneapi-gpu.rst @@ -265,31 +265,6 @@ See :doc:`array_api` for details, instructions, and limitations. Example: DPNP Arrays ~~~~~~~~~~~ -As a special case, GPU arrays from |dpnp| can be used without enabling array API in estimators with :ref:`array API support `, but note that using this alternative without array API enabled always involves data movement to host and back, thus not being the most efficient route in computational terms and **not recommended**. - -Example: - -.. code-block:: python - - import numpy as np - import dpnp - from sklearnex import config_context - from sklearnex.linear_model import LinearRegression - - rng = np.random.default_rng(seed=123) - X_np = rng.standard_normal(size=(100, 10), dtype=np.float32) - y_np = rng.standard_normal(size=100, dtype=np.float32) - - X = dpnp.array(X_np, device="gpu") - y = dpnp.array(y_np, device="gpu") - - model = LinearRegression() - model.fit(X, y) - - -Note that, if array API had been enabled, the snippet above would use the data as-is on the device where it resides, but without array API, it implies data movements using the SYCL queue contained by those objects. - -.. note:: - All the input data for an algorithm must reside on the same device when not using array API. +GPU arrays from |dpnp| are array-API-compliant and are used like any other array API framework: array API dispatch must be enabled through scikit-learn's ``config_context(array_api_dispatch=True)`` for the data to be consumed as-is on the device where it resides. Without array API enabled, |dpnp| arrays are treated as unsupported device inputs and moved to host for computation, with results returned as NumPy arrays. When a |dpnp_array| on GPU is passed as input but the operation is not supported on GPU, the operation may be executed on CPU instead - see :doc:`config-contexts` for more details. diff --git a/examples/sklearnex/basic_statistics_spmd.py b/examples/sklearnex/basic_statistics_spmd.py index ac1f065cd2..1b8f403e37 100644 --- a/examples/sklearnex/basic_statistics_spmd.py +++ b/examples/sklearnex/basic_statistics_spmd.py @@ -19,6 +19,7 @@ from dpctl import SyclQueue from mpi4py import MPI +from sklearnex import config_context from sklearnex.spmd.basic_statistics import BasicStatistics as BasicStatisticsSpmd @@ -57,8 +58,11 @@ def generate_data(par, size, seed=777): gtr_mean = np.mean(weighted_data, axis=0) gtr_std = np.std(weighted_data, axis=0) -bss = BasicStatisticsSpmd(["mean", "standard_deviation"]) -bss.fit(dpnp_data, dpnp_weights) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + bss = BasicStatisticsSpmd(["mean", "standard_deviation"]) + bss.fit(dpnp_data, dpnp_weights) -print(f"Computed mean on rank {rank}:\n", bss.mean_) -print(f"Computed std on rank {rank}:\n", bss.standard_deviation_) + print(f"Computed mean on rank {rank}:\n", bss.mean_) + print(f"Computed std on rank {rank}:\n", bss.standard_deviation_) diff --git a/examples/sklearnex/covariance_spmd.py b/examples/sklearnex/covariance_spmd.py index 8c2467d1a7..25ef587735 100644 --- a/examples/sklearnex/covariance_spmd.py +++ b/examples/sklearnex/covariance_spmd.py @@ -19,6 +19,7 @@ import numpy as np from mpi4py import MPI +from sklearnex import config_context from sklearnex.spmd.covariance import EmpiricalCovariance @@ -37,6 +38,9 @@ def get_data(data_seed): X = get_data(rank) dpnp_X = dpnp.asarray(X, usm_type="device", sycl_queue=q) -cov = EmpiricalCovariance().fit(dpnp_X) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + cov = EmpiricalCovariance().fit(dpnp_X) -print(f"Computed covariance values on rank {rank}:\n", cov.covariance_) + print(f"Computed covariance values on rank {rank}:\n", cov.covariance_) diff --git a/examples/sklearnex/dbscan_spmd.py b/examples/sklearnex/dbscan_spmd.py index 96d39c2ae9..479213b061 100644 --- a/examples/sklearnex/dbscan_spmd.py +++ b/examples/sklearnex/dbscan_spmd.py @@ -26,6 +26,7 @@ from mpi4py import MPI from sklearn.datasets import load_digits +from sklearnex import config_context from sklearnex.spmd.cluster import DBSCAN @@ -57,6 +58,9 @@ def get_test_data(size): dpnp_X = dpnp.asarray(X, usm_type="device", sycl_queue=queue) -model = DBSCAN(eps=3, min_samples=2).fit(dpnp_X) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + model = DBSCAN(eps=3, min_samples=2).fit(dpnp_X) -print(f"Labels on rank {rank} (slice of 2):\n", model.labels_[:2]) + print(f"Labels on rank {rank} (slice of 2):\n", model.labels_[:2]) diff --git a/examples/sklearnex/incremental_basic_statistics_dpnp.py b/examples/sklearnex/incremental_basic_statistics_dpnp.py index a2a9b9c3aa..9ed3a7b612 100644 --- a/examples/sklearnex/incremental_basic_statistics_dpnp.py +++ b/examples/sklearnex/incremental_basic_statistics_dpnp.py @@ -17,6 +17,7 @@ import dpctl import dpnp +from sklearnex import config_context from sklearnex.basic_statistics import IncrementalBasicStatistics # We create GPU SyclQueue and then put data to dpnp arrays using @@ -24,28 +25,33 @@ queue = dpctl.SyclQueue("gpu") -incbs = IncrementalBasicStatistics(result_options=["mean", "max", "sum"]) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + incbs = IncrementalBasicStatistics(result_options=["mean", "max", "sum"]) -# We do partial_fit for each batch and then print final result. -X_1 = dpnp.asarray([[0, 1], [0, 1]], sycl_queue=queue) -result = incbs.partial_fit(X_1) + # We do partial_fit for each batch and then print final result. + X_1 = dpnp.asarray([[0, 1], [0, 1]], sycl_queue=queue) + result = incbs.partial_fit(X_1) -X_2 = dpnp.asarray([[1, 2]], sycl_queue=queue) -result = incbs.partial_fit(X_2) + X_2 = dpnp.asarray([[1, 2]], sycl_queue=queue) + result = incbs.partial_fit(X_2) -X_3 = dpnp.asarray([[1, 1], [1, 2], [2, 3]], sycl_queue=queue) -result = incbs.partial_fit(X_3) + X_3 = dpnp.asarray([[1, 1], [1, 2], [2, 3]], sycl_queue=queue) + result = incbs.partial_fit(X_3) -print(f"Mean:\n{result.mean_}") -print(f"Max:\n{result.max_}") -print(f"Sum:\n{result.sum_}") + print(f"Mean:\n{result.mean_}") + print(f"Max:\n{result.max_}") + print(f"Sum:\n{result.sum_}") -# We put the whole data to fit method, it is split automatically and then -# partial_fit is called for each batch. -incbs = IncrementalBasicStatistics(result_options=["mean", "max", "sum"], batch_size=3) -X = dpnp.asarray([[0, 1], [0, 1], [1, 2], [1, 1], [1, 2], [2, 3]], sycl_queue=queue) -result = incbs.fit(X) + # We put the whole data to fit method, it is split automatically and then + # partial_fit is called for each batch. + incbs = IncrementalBasicStatistics( + result_options=["mean", "max", "sum"], batch_size=3 + ) + X = dpnp.asarray([[0, 1], [0, 1], [1, 2], [1, 1], [1, 2], [2, 3]], sycl_queue=queue) + result = incbs.fit(X) -print(f"Mean:\n{result.mean_}") -print(f"Max:\n{result.max_}") -print(f"Sum:\n{result.sum_}") + print(f"Mean:\n{result.mean_}") + print(f"Max:\n{result.max_}") + print(f"Sum:\n{result.sum_}") diff --git a/examples/sklearnex/incremental_covariance_spmd.py b/examples/sklearnex/incremental_covariance_spmd.py index 213964ca1b..87032b789e 100644 --- a/examples/sklearnex/incremental_covariance_spmd.py +++ b/examples/sklearnex/incremental_covariance_spmd.py @@ -19,6 +19,7 @@ import numpy as np from mpi4py import MPI +from sklearnex import config_context from sklearnex.spmd.covariance import IncrementalEmpiricalCovariance @@ -45,14 +46,17 @@ def get_local_data(data, comm): X_local = get_local_data(X, comm) X_split = np.array_split(X_local, num_batches) -cov = IncrementalEmpiricalCovariance() +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + cov = IncrementalEmpiricalCovariance() -# Partial fit is called for each batch on each GPU + # Partial fit is called for each batch on each GPU -for i in range(num_batches): - dpnp_X = dpnp.asarray(X_split[i], usm_type="device", sycl_queue=q) - cov.partial_fit(dpnp_X) + for i in range(num_batches): + dpnp_X = dpnp.asarray(X_split[i], usm_type="device", sycl_queue=q) + cov.partial_fit(dpnp_X) -# Finalization of results is performed in a lazy way after requesting results like in non-SPMD incremental estimators. + # Finalization of results is performed in a lazy way after requesting results like in non-SPMD incremental estimators. -print(f"Computed covariance values on rank {comm.Get_rank()}:\n", cov.covariance_) + print(f"Computed covariance values on rank {comm.Get_rank()}:\n", cov.covariance_) diff --git a/examples/sklearnex/incremental_linear_regression_dpnp.py b/examples/sklearnex/incremental_linear_regression_dpnp.py index 7433e91de6..45d7c081a7 100644 --- a/examples/sklearnex/incremental_linear_regression_dpnp.py +++ b/examples/sklearnex/incremental_linear_regression_dpnp.py @@ -17,6 +17,7 @@ import dpctl import dpnp +from sklearnex import config_context from sklearnex.linear_model import IncrementalLinearRegression # We create GPU SyclQueue and then put data to dpnp arrays using @@ -24,32 +25,37 @@ queue = dpctl.SyclQueue("gpu") -inclin = IncrementalLinearRegression() - -# We do partial_fit for each batch and then print final result. -X_1, y_1 = dpnp.asarray([[0, 1], [1, 2]], sycl_queue=queue), dpnp.asarray( - [2, 4], sycl_queue=queue -) -result = inclin.partial_fit(X_1, y_1) - -X_2, y_2 = dpnp.asarray([[2, 3]], sycl_queue=queue), dpnp.asarray([6], sycl_queue=queue) -result = inclin.partial_fit(X_2, y_2) - -X_3, y_3 = dpnp.asarray([[0, 2], [1, 3], [2, 4]], sycl_queue=queue), dpnp.asarray( - [3, 5, 7], sycl_queue=queue -) -result = inclin.partial_fit(X_3, y_3) - -print(f"Coefs:\n{result.coef_}") -print(f"Intercept:\n{result.intercept_}") - -# We put the whole data to fit method, it is split automatically and then -# partial_fit is called for each batch. -inclin = IncrementalLinearRegression(batch_size=3) -X, y = dpnp.asarray( - [[0, 1], [1, 2], [2, 3], [0, 2], [1, 3], [2, 4]], sycl_queue=queue -), dpnp.asarray([2, 4, 6, 3, 5, 7], sycl_queue=queue) -result = inclin.fit(X, y) - -print(f"Coefs:\n{result.coef_}") -print(f"Intercept:\n{result.intercept_}") +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + inclin = IncrementalLinearRegression() + + # We do partial_fit for each batch and then print final result. + X_1, y_1 = dpnp.asarray([[0, 1], [1, 2]], sycl_queue=queue), dpnp.asarray( + [2, 4], sycl_queue=queue + ) + result = inclin.partial_fit(X_1, y_1) + + X_2, y_2 = dpnp.asarray([[2, 3]], sycl_queue=queue), dpnp.asarray( + [6], sycl_queue=queue + ) + result = inclin.partial_fit(X_2, y_2) + + X_3, y_3 = dpnp.asarray([[0, 2], [1, 3], [2, 4]], sycl_queue=queue), dpnp.asarray( + [3, 5, 7], sycl_queue=queue + ) + result = inclin.partial_fit(X_3, y_3) + + print(f"Coefs:\n{result.coef_}") + print(f"Intercept:\n{result.intercept_}") + + # We put the whole data to fit method, it is split automatically and then + # partial_fit is called for each batch. + inclin = IncrementalLinearRegression(batch_size=3) + X, y = dpnp.asarray( + [[0, 1], [1, 2], [2, 3], [0, 2], [1, 3], [2, 4]], sycl_queue=queue + ), dpnp.asarray([2, 4, 6, 3, 5, 7], sycl_queue=queue) + result = inclin.fit(X, y) + + print(f"Coefs:\n{result.coef_}") + print(f"Intercept:\n{result.intercept_}") diff --git a/examples/sklearnex/incremental_pca_dpnp.py b/examples/sklearnex/incremental_pca_dpnp.py index 802993bc1d..92f9be16fa 100644 --- a/examples/sklearnex/incremental_pca_dpnp.py +++ b/examples/sklearnex/incremental_pca_dpnp.py @@ -21,7 +21,7 @@ import dpnp # Import estimator via sklearnex's patch mechanism from sklearn -from sklearnex import patch_sklearn, sklearn_is_patched +from sklearnex import config_context, patch_sklearn, sklearn_is_patched # IncrementalPCA is currently in preview module, so extra flag is required patch_sklearn(preview=True) @@ -39,32 +39,37 @@ # the queue. It allows us to do computation on GPU. queue = dpctl.SyclQueue("gpu") -incpca = IncrementalPCA() +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + incpca = IncrementalPCA() -# We do partial_fit for each batch and then print final result. -X_1 = dpnp.asarray([[-1, -1], [-2, -1]], sycl_queue=queue) -result = incpca.partial_fit(X_1) + # We do partial_fit for each batch and then print final result. + X_1 = dpnp.asarray([[-1, -1], [-2, -1]], sycl_queue=queue) + result = incpca.partial_fit(X_1) -X_2 = dpnp.asarray([[-3, -2], [1, 1]], sycl_queue=queue) -result = incpca.partial_fit(X_2) + X_2 = dpnp.asarray([[-3, -2], [1, 1]], sycl_queue=queue) + result = incpca.partial_fit(X_2) -X_3 = dpnp.asarray([[2, 1], [3, 2]], sycl_queue=queue) -result = incpca.partial_fit(X_3) + X_3 = dpnp.asarray([[2, 1], [3, 2]], sycl_queue=queue) + result = incpca.partial_fit(X_3) -X = dpnp.concat((X_1, X_2, X_3)) -transformed_X = incpca.transform(X) + X = dpnp.concat((X_1, X_2, X_3)) + transformed_X = incpca.transform(X) -print(f"Principal components:\n{result.components_}") -print(f"Explained variance ratio:\n{result.explained_variance_ratio_}") -print(f"Transformed data:\n{transformed_X}") + print(f"Principal components:\n{result.components_}") + print(f"Explained variance ratio:\n{result.explained_variance_ratio_}") + print(f"Transformed data:\n{transformed_X}") -# We put the whole data to fit method, it is split automatically and then -# partial_fit is called for each batch. -incpca = IncrementalPCA(batch_size=3) -X = dpnp.asarray([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]]) -result = incpca.fit(X) -transformed_X = incpca.transform(X) + # We put the whole data to fit method, it is split automatically and then + # partial_fit is called for each batch. + incpca = IncrementalPCA(batch_size=3) + X = dpnp.asarray( + [[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]], sycl_queue=queue + ) + result = incpca.fit(X) + transformed_X = incpca.transform(X) -print(f"Principal components:\n{result.components_}") -print(f"Explained variance ratio:\n{result.explained_variance_ratio_}") -print(f"Transformed data:\n{transformed_X}") + print(f"Principal components:\n{result.components_}") + print(f"Explained variance ratio:\n{result.explained_variance_ratio_}") + print(f"Transformed data:\n{transformed_X}") diff --git a/examples/sklearnex/kmeans_spmd.py b/examples/sklearnex/kmeans_spmd.py index e1f055214e..e4b93e43fc 100644 --- a/examples/sklearnex/kmeans_spmd.py +++ b/examples/sklearnex/kmeans_spmd.py @@ -22,6 +22,7 @@ from mpi4py import MPI from sklearn.datasets import load_digits +from sklearnex import config_context from sklearnex.spmd.cluster import KMeans @@ -53,15 +54,18 @@ def get_test_data(size): dpnp_X = dpnp.asarray(X, usm_type="device", sycl_queue=queue) -model = KMeans(n_clusters=10).fit(dpnp_X) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + model = KMeans(n_clusters=10).fit(dpnp_X) -print(f"Number of iterations on {rank}:\n", model.n_iter_) -print(f"Labels on rank {rank} (slice of 2):\n", model.labels_[:2]) -print(f"Centers on rank {rank} (slice of 2):\n", model.cluster_centers_[:2, :]) + print(f"Number of iterations on {rank}:\n", model.n_iter_) + print(f"Labels on rank {rank} (slice of 2):\n", model.labels_[:2]) + print(f"Centers on rank {rank} (slice of 2):\n", model.cluster_centers_[:2, :]) -X_test, _ = get_test_data(size) -dpnp_X_test = dpnp.asarray(X_test, usm_type="device", sycl_queue=queue) + X_test, _ = get_test_data(size) + dpnp_X_test = dpnp.asarray(X_test, usm_type="device", sycl_queue=queue) -result = model.predict(dpnp_X_test) + result = model.predict(dpnp_X_test) -print(f"Result labels on rank {rank} (slice of 5):\n", result[:5]) + print(f"Result labels on rank {rank} (slice of 5):\n", result[:5]) diff --git a/examples/sklearnex/knn_bf_classification_spmd.py b/examples/sklearnex/knn_bf_classification_spmd.py index b8dc7833b9..f7ac0f5c1b 100644 --- a/examples/sklearnex/knn_bf_classification_spmd.py +++ b/examples/sklearnex/knn_bf_classification_spmd.py @@ -22,6 +22,7 @@ from mpi4py import MPI from sklearn.metrics import accuracy_score +from sklearnex import config_context from sklearnex.spmd.neighbors import KNeighborsClassifier @@ -57,22 +58,25 @@ def generate_X_y(par, seed): dpnp_y_train = dpnp.asarray(y_train, usm_type="device", sycl_queue=q) dpnp_X_test = dpnp.asarray(X_test, usm_type="device", sycl_queue=q) -model_spmd = KNeighborsClassifier( - algorithm="brute", n_neighbors=20, weights="uniform", p=2, metric="minkowski" -) -model_spmd.fit(dpnp_X_train, dpnp_y_train) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + model_spmd = KNeighborsClassifier( + algorithm="brute", n_neighbors=20, weights="uniform", p=2, metric="minkowski" + ) + model_spmd.fit(dpnp_X_train, dpnp_y_train) -y_predict = model_spmd.predict(dpnp_X_test) + y_predict = model_spmd.predict(dpnp_X_test) -print("Brute Force Distributed kNN classification results:") -print("Ground truth (first 5 observations on rank {}):\n{}".format(rank, y_test[:5])) -print( - "Classification results (first 5 observations on rank {}):\n{}".format( - rank, y_predict[:5] + print("Brute Force Distributed kNN classification results:") + print("Ground truth (first 5 observations on rank {}):\n{}".format(rank, y_test[:5])) + print( + "Classification results (first 5 observations on rank {}):\n{}".format( + rank, y_predict[:5] + ) ) -) -print( - "Accuracy for entire rank {} (256 classes): {}\n".format( - rank, accuracy_score(y_test, dpnp.asnumpy(y_predict)) + print( + "Accuracy for entire rank {} (256 classes): {}\n".format( + rank, accuracy_score(y_test, dpnp.asnumpy(y_predict)) + ) ) -) diff --git a/examples/sklearnex/knn_bf_regression_spmd.py b/examples/sklearnex/knn_bf_regression_spmd.py index 887f11dc8e..6adfc92d30 100644 --- a/examples/sklearnex/knn_bf_regression_spmd.py +++ b/examples/sklearnex/knn_bf_regression_spmd.py @@ -23,6 +23,7 @@ from numpy.testing import assert_allclose from sklearn.metrics import mean_squared_error +from sklearnex import config_context from sklearnex.spmd.neighbors import KNeighborsRegressor @@ -63,23 +64,26 @@ def generate_X_y(par, coef_seed, data_seed): assert_allclose(coef_train, coef_test) -model_spmd = KNeighborsRegressor( - algorithm="brute", n_neighbors=5, weights="uniform", p=2, metric="minkowski" -) -model_spmd.fit(dpnp_X_train, dpnp_y_train) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + model_spmd = KNeighborsRegressor( + algorithm="brute", n_neighbors=5, weights="uniform", p=2, metric="minkowski" + ) + model_spmd.fit(dpnp_X_train, dpnp_y_train) -y_predict = model_spmd.predict(dpnp_X_test) + y_predict = model_spmd.predict(dpnp_X_test) -print("Brute Force Distributed kNN regression results:") -print("Ground truth (first 5 observations on rank {}):\n{}".format(rank, y_test[:5])) -print( - "Regression results (first 5 observations on rank {}):\n{}".format( - rank, y_predict[:5] + print("Brute Force Distributed kNN regression results:") + print("Ground truth (first 5 observations on rank {}):\n{}".format(rank, y_test[:5])) + print( + "Regression results (first 5 observations on rank {}):\n{}".format( + rank, y_predict[:5] + ) ) -) -print( - "MSE for entire rank {}: {}\n".format( - rank, - mean_squared_error(y_test, dpnp.asnumpy(y_predict)), + print( + "MSE for entire rank {}: {}\n".format( + rank, + mean_squared_error(y_test, dpnp.asnumpy(y_predict)), + ) ) -) diff --git a/examples/sklearnex/linear_regression_spmd.py b/examples/sklearnex/linear_regression_spmd.py index 7e253a9a92..721cc80c14 100755 --- a/examples/sklearnex/linear_regression_spmd.py +++ b/examples/sklearnex/linear_regression_spmd.py @@ -21,6 +21,7 @@ from dpctl import SyclQueue from mpi4py import MPI +from sklearnex import config_context from sklearnex.spmd.linear_model import LinearRegression @@ -63,14 +64,17 @@ def get_test_data(rank): dpnp_X = dpnp.asarray(X, usm_type="device", sycl_queue=queue) dpnp_y = dpnp.asarray(y, usm_type="device", sycl_queue=queue) -model = LinearRegression().fit(dpnp_X, dpnp_y) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + model = LinearRegression().fit(dpnp_X, dpnp_y) -print(f"Coefficients on rank {rank}:\n", model.coef_) -print(f"Intercept on rank {rank}:\n", model.intercept_) + print(f"Coefficients on rank {rank}:\n", model.coef_) + print(f"Intercept on rank {rank}:\n", model.intercept_) -X_test, _ = get_test_data(rank) -dpnp_X_test = dpnp.asarray(X_test, usm_type="device", sycl_queue=queue) + X_test, _ = get_test_data(rank) + dpnp_X_test = dpnp.asarray(X_test, usm_type="device", sycl_queue=queue) -result = model.predict(dpnp_X_test) + result = model.predict(dpnp_X_test) -print(f"Result on rank {rank}:\n", result) + print(f"Result on rank {rank}:\n", result) diff --git a/examples/sklearnex/logistic_regression_spmd.py b/examples/sklearnex/logistic_regression_spmd.py index 4f0e66ba2f..9d98b6a949 100644 --- a/examples/sklearnex/logistic_regression_spmd.py +++ b/examples/sklearnex/logistic_regression_spmd.py @@ -24,6 +24,7 @@ from sklearn.metrics import accuracy_score from sklearn.model_selection import train_test_split +from sklearnex import config_context from sklearnex.spmd.linear_model import LogisticRegression @@ -68,22 +69,25 @@ def generate_X_y(par, seed): dpnp_y_train = dpnp.asarray(y_train, usm_type="device", sycl_queue=q) dpnp_X_test = dpnp.asarray(X_test, usm_type="device", sycl_queue=q) -model_spmd = LogisticRegression(solver="newton-cg") -model_spmd.fit(dpnp_X_train, dpnp_y_train) - -y_predict = model_spmd.predict(dpnp_X_test) - -print("Distributed LogisticRegression results:") -print("Coefficients on rank {}:\n{}:".format(rank, model_spmd.coef_)) -print("Intercept on rank {}:\n{}:".format(rank, model_spmd.intercept_)) -print("Ground truth (first 5 observations on rank {}):\n{}".format(rank, y_test[:5])) -print( - "Classification results (first 5 observations on rank {}):\n{}".format( - rank, y_predict[:5] +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + model_spmd = LogisticRegression(solver="newton-cg") + model_spmd.fit(dpnp_X_train, dpnp_y_train) + + y_predict = model_spmd.predict(dpnp_X_test) + + print("Distributed LogisticRegression results:") + print("Coefficients on rank {}:\n{}:".format(rank, model_spmd.coef_)) + print("Intercept on rank {}:\n{}:".format(rank, model_spmd.intercept_)) + print("Ground truth (first 5 observations on rank {}):\n{}".format(rank, y_test[:5])) + print( + "Classification results (first 5 observations on rank {}):\n{}".format( + rank, y_predict[:5] + ) ) -) -print( - "Accuracy for entire rank {} (2 classes): {}\n".format( - rank, accuracy_score(y_test, dpnp.asnumpy(y_predict)) + print( + "Accuracy for entire rank {} (2 classes): {}\n".format( + rank, accuracy_score(y_test, dpnp.asnumpy(y_predict)) + ) ) -) diff --git a/examples/sklearnex/pca_spmd.py b/examples/sklearnex/pca_spmd.py index 26703a8ea7..24fe779065 100644 --- a/examples/sklearnex/pca_spmd.py +++ b/examples/sklearnex/pca_spmd.py @@ -19,6 +19,7 @@ import numpy as np from mpi4py import MPI +from sklearnex import config_context from sklearnex.spmd.decomposition import PCA @@ -37,7 +38,10 @@ def get_data(data_seed): X = get_data(rank) dpnp_X = dpnp.asarray(X, usm_type="device", sycl_queue=q) -pca = PCA(n_components=2).fit(dpnp_X) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + pca = PCA(n_components=2).fit(dpnp_X) -print(f"Singular values on rank {rank}:\n", pca.singular_values_) -print(f"Explained variance Ratio on rank {rank}:\n", pca.explained_variance_ratio_) + print(f"Singular values on rank {rank}:\n", pca.singular_values_) + print(f"Explained variance Ratio on rank {rank}:\n", pca.explained_variance_ratio_) diff --git a/examples/sklearnex/random_forest_classifier_spmd.py b/examples/sklearnex/random_forest_classifier_spmd.py index 4704243afc..dd5aa8be6d 100644 --- a/examples/sklearnex/random_forest_classifier_spmd.py +++ b/examples/sklearnex/random_forest_classifier_spmd.py @@ -23,6 +23,7 @@ import numpy as np from mpi4py import MPI +from sklearnex import config_context from sklearnex.spmd.ensemble import RandomForestClassifier @@ -52,14 +53,21 @@ def generate_X_y(par, seed): dpnp_y_train = dpnp.asarray(y_train, usm_type="device", sycl_queue=q) dpnp_X_test = dpnp.asarray(X_test, usm_type="device", sycl_queue=q) -rf = RandomForestClassifier(max_depth=2, random_state=0).fit(dpnp_X_train, dpnp_y_train) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + rf = RandomForestClassifier(max_depth=2, random_state=0).fit( + dpnp_X_train, dpnp_y_train + ) -pred = rf.predict(dpnp_X_test) + pred = rf.predict(dpnp_X_test) -print("Random Forest classification results:") -print("Ground truth (first 5 observations on rank {}):\n{}".format(mpi_rank, y_test[:5])) -print( - "Classification results (first 5 observations on rank {}):\n{}".format( - mpi_rank, dpnp.asnumpy(pred)[:5] + print("Random Forest classification results:") + print( + "Ground truth (first 5 observations on rank {}):\n{}".format(mpi_rank, y_test[:5]) + ) + print( + "Classification results (first 5 observations on rank {}):\n{}".format( + mpi_rank, dpnp.asnumpy(pred)[:5] + ) ) -) diff --git a/examples/sklearnex/random_forest_regressor_spmd.py b/examples/sklearnex/random_forest_regressor_spmd.py index 7c7198b94d..2cf6df9f70 100644 --- a/examples/sklearnex/random_forest_regressor_spmd.py +++ b/examples/sklearnex/random_forest_regressor_spmd.py @@ -24,6 +24,7 @@ from mpi4py import MPI from numpy.testing import assert_allclose +from sklearnex import config_context from sklearnex.spmd.ensemble import RandomForestRegressor @@ -59,13 +60,20 @@ def generate_X_y(par, coef_seed, data_seed): dpnp_X_test = dpnp.asarray(X_test, usm_type="device", sycl_queue=q) -rf = RandomForestRegressor(max_depth=2, random_state=0).fit(dpnp_X_train, dpnp_y_train) +# Array API dispatch keeps dpnp data on device throughout the computation. +# The SCIPY_ARRAY_API environment variable must also be set to enable this. +with config_context(array_api_dispatch=True): + rf = RandomForestRegressor(max_depth=2, random_state=0).fit( + dpnp_X_train, dpnp_y_train + ) -y_predict = rf.predict(dpnp_X_test) + y_predict = rf.predict(dpnp_X_test) -print("Ground truth (first 5 observations on rank {}):\n{}".format(mpi_rank, y_test[:5])) -print( - "Regression results (first 5 observations on rank {}):\n{}".format( - mpi_rank, y_predict[:5] + print( + "Ground truth (first 5 observations on rank {}):\n{}".format(mpi_rank, y_test[:5]) + ) + print( + "Regression results (first 5 observations on rank {}):\n{}".format( + mpi_rank, y_predict[:5] + ) ) -) diff --git a/onedal/covariance/covariance.py b/onedal/covariance/covariance.py index 99968da984..274c64f252 100644 --- a/onedal/covariance/covariance.py +++ b/onedal/covariance/covariance.py @@ -23,7 +23,6 @@ from ..common._backend import bind_default_backend from ..common.hyperparameters import get_hyperparameters from ..datatypes import from_table, to_table -from ..utils._array_api import _get_sycl_namespace class BaseEmpiricalCovariance(metaclass=ABCMeta): diff --git a/onedal/datatypes/__init__.py b/onedal/datatypes/__init__.py index 1a6cfc3751..581bf4df0e 100644 --- a/onedal/datatypes/__init__.py +++ b/onedal/datatypes/__init__.py @@ -16,10 +16,8 @@ from ._data_conversion import from_table, return_type_constructor, to_table from ._dlpack import dlpack_to_numpy, get_torch_queue -from ._sycl_usm import copy_to_dpnp __all__ = [ - "copy_to_dpnp", "dlpack_to_numpy", "from_table", "get_torch_queue", diff --git a/onedal/datatypes/_sycl_usm.py b/onedal/datatypes/_sycl_usm.py deleted file mode 100644 index 9ee198192d..0000000000 --- a/onedal/datatypes/_sycl_usm.py +++ /dev/null @@ -1,44 +0,0 @@ -# ============================================================================== -# 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. -# ============================================================================== - -from collections.abc import Iterable - -import numpy as np -import scipy.sparse as sp - -from ..utils._third_party import lazy_import - - -@lazy_import("dpnp") -def _to_dpnp(dpnp, queue, array): - try: - return dpnp.asarray(array, usm_type="device", sycl_queue=queue) - except ValueError as e: - # ValueError will raise if device does not support the dtype - # retry with float32 (needed for fp16 and fp64 support issues) - # try again as float32, if it is a float32 just raise the error. - if array.dtype == np.float32: - raise e - return _to_dpnp(queue, array.astype(np.float32)) - - -def copy_to_dpnp(queue, array): - if hasattr(array, "tobytes"): - return _to_dpnp(queue, array) - else: - if isinstance(array, Iterable) and not sp.issparse(array): - array = [copy_to_dpnp(queue, i) for i in array] - return array diff --git a/onedal/datatypes/tests/test_data.py b/onedal/datatypes/tests/test_data.py index 771cf14400..43167eb31b 100644 --- a/onedal/datatypes/tests/test_data.py +++ b/onedal/datatypes/tests/test_data.py @@ -46,7 +46,6 @@ get_dataframes_and_queues, ) from onedal.tests.utils._device_selection import get_queues -from onedal.utils._array_api import _get_sycl_namespace data_shapes = [ pytest.param((1000, 100), id="(1000, 100)"), # 2-D array @@ -71,7 +70,7 @@ def fit(self, X, y=None, queue=None): if not backend.is_dpc: raise RuntimeError("Table conversions should be done with DPC backend.") - sua_iface, xp, _ = _get_sycl_namespace(X) + xp = X.__array_namespace__() dbscan = DBSCAN() types = [xp.float32, xp.float64] if get_dtype(X) not in types: @@ -219,8 +218,6 @@ def test_input_zero_copy_sycl_usm(dataframe, queue, order, dtype): X_dp = _convert_to_dataframe(X_np, sycl_queue=queue, target_df=dataframe) - sua_iface, X_dp_namespace, _ = _get_sycl_namespace(X_dp) - X_table = to_table(X_dp) _assert_sua_iface_fields(X_dp, X_table) @@ -394,7 +391,6 @@ def test_to_table_non_contiguous_input(dataframe, queue): def test_interop_if_no_dpc_backend_sycl_usm(dataframe, queue, dtype): X = np.zeros((10, 20), dtype=dtype) X = _convert_to_dataframe(X, sycl_queue=queue, target_df=dataframe) - sua_iface, _, _ = _get_sycl_namespace(X) expected_err_msg = "SYCL usm array conversion to table requires the DPC backend" with pytest.raises(RuntimeError, match=expected_err_msg): diff --git a/onedal/decomposition/pca.py b/onedal/decomposition/pca.py index 78bf5c091e..d1dd892bc2 100644 --- a/onedal/decomposition/pca.py +++ b/onedal/decomposition/pca.py @@ -21,7 +21,6 @@ from ..common._backend import bind_default_backend from ..common.hyperparameters import get_hyperparameters from ..datatypes import from_table, to_table -from ..utils._array_api import _get_sycl_namespace class PCA(metaclass=ABCMeta): diff --git a/onedal/utils/_array_api.py b/onedal/utils/_array_api.py index 304dac1db3..f588127457 100644 --- a/onedal/utils/_array_api.py +++ b/onedal/utils/_array_api.py @@ -17,13 +17,9 @@ """Tools to support array_api.""" from collections.abc import Iterable -from functools import lru_cache -import numpy as np import scipy.sparse as sp -from ..utils._third_party import _is_subclass_fast - def _supports_buffer_protocol(obj): # the array_api standard mandates conversion with the buffer protocol, @@ -61,33 +57,3 @@ def _is_numpy_namespace(xp): "numpy.array_api", "sklearn.externals.array_api_compat.numpy", } - - -@lru_cache(100) -def _cls_to_sycl_namespace(cls): - if _is_subclass_fast(cls, "dpnp", "ndarray"): - import dpnp - - return dpnp - else: - raise ValueError(f"SYCL type not recognized: {cls}") - - -def _get_sycl_namespace(*arrays): - """Get namespace of sycl arrays.""" - - # sycl support designed to work regardless of array_api_dispatch sklearn global value - sua_iface = {type(x): x for x in arrays if hasattr(x, "__sycl_usm_array_interface__")} - - if len(sua_iface) > 1: - raise ValueError(f"Multiple SYCL types for array inputs: {sua_iface}") - - if sua_iface: - (X,) = sua_iface.values() - return ( - sua_iface, - _cls_to_sycl_namespace(type(X)), - hasattr(X, "__array_namespace__"), - ) - - return sua_iface, np, False diff --git a/sklearnex/_device_offload.py b/sklearnex/_device_offload.py index 1037362614..bd99c0774a 100644 --- a/sklearnex/_device_offload.py +++ b/sklearnex/_device_offload.py @@ -20,18 +20,16 @@ from typing import Any, Union from sklearn.utils import get_tags +from sklearn.utils._array_api import get_namespace from daal4py.sklearn._utils import sklearn_check_version from onedal._device_offload import _get_host_inputs, _transfer_to_host -from onedal.datatypes import copy_to_dpnp from onedal.utils import _sycl_queue_manager as QM -from onedal.utils._array_api import _asarray, _get_sycl_namespace, _is_numpy_namespace -from onedal.utils._third_party import is_dpnp_ndarray +from onedal.utils._array_api import _asarray, _is_numpy_namespace from ._config import get_config from ._utils import PatchingConditionsChain from .base import oneDALEstimator -from .utils._array_api import get_namespace def _get_backend( @@ -216,9 +214,6 @@ def wrapper(self, *args, **kwargs) -> Any: data = (*args, *kwargs.values())[0] - if usm_iface := getattr(data, "__sycl_usm_array_interface__", None): - return copy_to_dpnp(usm_iface["syclobj"], result) - if hasattr(data, "dtype"): xp, is_array_api = get_namespace(data) if is_array_api and not _is_numpy_namespace(xp): @@ -235,9 +230,9 @@ def wrapper(self, *args, **kwargs) -> Any: def support_input_format(func): """Transform input and output function arrays to/from host. - Converts and moves the output arrays of the decorated function - to match the input array type and device. - Puts SYCLQueue from data to decorated function arguments. + Wraps host-side scikit-learn / daal4py fallback functions (device offload to + oneDAL happens in ``dispatch``): inputs are transferred to host and the output + is converted back to the input's array API namespace and device. Parameters ---------- @@ -265,23 +260,12 @@ def wrapper_impl(*args, **kwargs): else: self = None - if "queue" not in kwargs and "queue" in inspect.signature(func).parameters: - if usm_iface := getattr(args[0], "__sycl_usm_array_interface__", None): - kwargs["queue"] = usm_iface["syclobj"] + if len(args) == 0 and len(kwargs) == 0: + return invoke_func(self, *args, **kwargs) - if kwargs.get("queue") is not None: - # Device path — function accepts queue, pass device data directly - result = invoke_func(self, *args, **kwargs) - else: - # Host path — sklearn function or host data, transfer to host - if len(args) == 0 and len(kwargs) == 0: - return invoke_func(self, *args, **kwargs) - - with QM.manage_global_queue(None, *args) as queue: - hostargs, hostkwargs = _get_host_inputs(*args, **kwargs) - result = invoke_func(self, *hostargs, **hostkwargs) - if queue and hasattr(args[0], "__sycl_usm_array_interface__"): - return copy_to_dpnp(queue, result) + with QM.manage_global_queue(None, *args): + hostargs, hostkwargs = _get_host_inputs(*args, **kwargs) + result = invoke_func(self, *hostargs, **hostkwargs) data = (*args, *kwargs.values())[0] if get_config().get("transform_output") in ("default", None): @@ -292,27 +276,3 @@ def wrapper_impl(*args, **kwargs): return result return wrapper_impl - - -def support_sycl_format(func): - # This wrapper enables scikit-learn functions and methods to work with - # all sycl data frameworks as they no longer support numpy implicit - # conversion and must be manually converted. This is only necessary - # when array API is supported but not active. - - @wraps(func) - def wrapper(*args, **kwargs): - if ( - not get_config().get("array_api_dispatch", False) - and _get_sycl_namespace(*args)[2] - ): - with QM.manage_global_queue(kwargs.get("queue"), *args): - if inspect.isfunction(func) and "." in func.__qualname__: - self, (args, kwargs) = args[0], _get_host_inputs(*args[1:], **kwargs) - return func(self, *args, **kwargs) - else: - args, kwargs = _get_host_inputs(*args, **kwargs) - return func(*args, **kwargs) - return func(*args, **kwargs) - - return wrapper diff --git a/sklearnex/basic_statistics/basic_statistics.py b/sklearnex/basic_statistics/basic_statistics.py index 9ebd580260..7253cd768c 100644 --- a/sklearnex/basic_statistics/basic_statistics.py +++ b/sklearnex/basic_statistics/basic_statistics.py @@ -15,6 +15,7 @@ # ============================================================================== from sklearn.base import BaseEstimator +from sklearn.utils._array_api import get_namespace from sklearn.utils._param_validation import StrOptions from daal4py.sklearn._n_jobs_support import control_n_jobs @@ -25,7 +26,7 @@ from .._device_offload import dispatch from .._utils import PatchingConditionsChain from ..base import oneDALEstimator -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import _check_sample_weight, validate_data if sklearn_check_version("1.9"): diff --git a/sklearnex/basic_statistics/incremental_basic_statistics.py b/sklearnex/basic_statistics/incremental_basic_statistics.py index 5da9328a07..8120393704 100644 --- a/sklearnex/basic_statistics/incremental_basic_statistics.py +++ b/sklearnex/basic_statistics/incremental_basic_statistics.py @@ -16,6 +16,7 @@ from sklearn.base import BaseEstimator from sklearn.utils import gen_batches +from sklearn.utils._array_api import get_namespace from sklearn.utils._param_validation import Interval, StrOptions from daal4py.sklearn._n_jobs_support import control_n_jobs @@ -27,7 +28,7 @@ from .._device_offload import dispatch from .._utils import PatchingConditionsChain, _add_inc_serialization_note from ..base import oneDALEstimator -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import _check_sample_weight, validate_data if sklearn_check_version("1.9"): diff --git a/sklearnex/cluster/dbscan.py b/sklearnex/cluster/dbscan.py index 35310ca075..7a7c415d07 100755 --- a/sklearnex/cluster/dbscan.py +++ b/sklearnex/cluster/dbscan.py @@ -15,6 +15,7 @@ # =============================================================================== from sklearn.cluster import DBSCAN as _sklearn_DBSCAN +from sklearn.utils._array_api import get_namespace from daal4py.sklearn._n_jobs_support import control_n_jobs from daal4py.sklearn._utils import is_sparse @@ -24,7 +25,7 @@ from .._device_offload import dispatch from .._utils import PatchingConditionsChain from ..base import oneDALEstimator -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import _check_sample_weight, validate_data diff --git a/sklearnex/cluster/k_means.py b/sklearnex/cluster/k_means.py index 29b139e074..f368c3e90c 100644 --- a/sklearnex/cluster/k_means.py +++ b/sklearnex/cluster/k_means.py @@ -30,6 +30,7 @@ from sklearn.metrics.pairwise import ( euclidean_distances as _sklearn_euclidean_distances, ) + from sklearn.utils._array_api import get_namespace from sklearn.utils._openmp_helpers import _openmp_effective_n_threads from sklearn.utils.validation import ( _num_samples, @@ -44,7 +45,7 @@ from .._device_offload import dispatch, wrap_output_data from .._utils import PatchingConditionsChain from ..base import oneDALEstimator - from ..utils._array_api import enable_array_api, get_namespace + from ..utils._array_api import enable_array_api from ..utils.validation import validate_data if sklearn_check_version("1.9"): diff --git a/sklearnex/covariance/incremental_covariance.py b/sklearnex/covariance/incremental_covariance.py index 2164c0cfb4..d72451fa1d 100644 --- a/sklearnex/covariance/incremental_covariance.py +++ b/sklearnex/covariance/incremental_covariance.py @@ -18,10 +18,10 @@ import warnings from functools import partial -import numpy as np from sklearn.base import BaseEstimator, clone from sklearn.covariance import EmpiricalCovariance as _sklearn_EmpiricalCovariance from sklearn.utils import gen_batches +from sklearn.utils._array_api import get_namespace from sklearn.utils._param_validation import Interval from sklearn.utils.validation import _num_features, check_is_fitted @@ -34,15 +34,10 @@ from onedal.utils._array_api import _is_numpy_namespace from .._config import config_context -from .._device_offload import ( - dispatch, - support_input_format, - support_sycl_format, - wrap_output_data, -) +from .._device_offload import dispatch, support_input_format, wrap_output_data from .._utils import PatchingConditionsChain, _add_inc_serialization_note from ..base import oneDALEstimator -from ..utils._array_api import _pinvh, enable_array_api, get_namespace, log_likelihood +from ..utils._array_api import _pinvh, enable_array_api, log_likelihood from ..utils.validation import validate_data if sklearn_check_version("1.9"): @@ -323,12 +318,9 @@ def _onedal_fit(self, X, queue=None): return self @wrap_output_data - @support_sycl_format def score(self, X_test, y=None): check_is_fitted(self) - # Only covariance evaluated for get_namespace due to dpnp - # support without array_api_dispatch if sklearn_check_version("1.9"): check_same_namespace(X_test, self, attribute="covariance_", method="score") xp, _ = get_namespace(X_test, self.covariance_) @@ -346,7 +338,7 @@ def score(self, X_test, y=None): est = clone(self) est.set_params(assume_centered=True) - # test_cov is a numpy array, but calculated on device + # ensure test_cov shares X's namespace and device before log_likelihood test_cov = est.fit(X - self.location_).covariance_ if not _is_numpy_namespace(xp): test_cov = xp.asarray(test_cov, device=X_test.device) @@ -355,16 +347,11 @@ def score(self, X_test, y=None): return res @wrap_output_data - @support_sycl_format def error_norm(self, comp_cov, norm="frobenius", scaling=True, squared=True): # equivalent to the sklearn implementation but written for array API # in the case of numpy-like inputs it will use sklearn's version instead. # This can be deprecated if/when sklearn makes the equivalent array API enabled. - # This includes a validate_data call and an unusual call to get_namespace in - # order to also support dpnp without array_api_dispatch. check_is_fitted(self) - # Only covariance evaluated for get_namespace due to dpnp - # support without array_api_dispatch if sklearn_check_version("1.9"): check_same_namespace( comp_cov, self, attribute="covariance_", method="error_norm" @@ -406,7 +393,6 @@ def error_norm(self, comp_cov, norm="frobenius", scaling=True, squared=True): return result # expose sklearnex pairwise_distances if mahalanobis distance eventually supported - @support_sycl_format def mahalanobis(self, X): # This must be done as ```support_input_format``` is insufficient for array API # support when attributes are non-numpy. @@ -419,12 +405,6 @@ def mahalanobis(self, X): # do not check dtype, done in pairwise_distances X_in = validate_data(self, X, reset=False) - if not _is_numpy_namespace(xp) and isinstance(X_in, np.ndarray): - # corrects issues with respect to dpnp support without array_api_dispatch - X_in = X - loc = xp.asarray(loc, device=X.device) - precision = xp.asarray(precision, device=X.device) - with config_context(assume_finite=True): try: dist = _mahalanobis(X_in, loc, VI=precision) diff --git a/sklearnex/covariance/tests/test_incremental_covariance.py b/sklearnex/covariance/tests/test_incremental_covariance.py index 0efcafc938..cddf54a54a 100644 --- a/sklearnex/covariance/tests/test_incremental_covariance.py +++ b/sklearnex/covariance/tests/test_incremental_covariance.py @@ -185,6 +185,7 @@ def test_sklearnex_fit_on_random_data( @pytest.mark.parametrize("dataframe,queue", get_dataframes_and_queues()) def test_whitened_toy_score(dataframe, queue): + from sklearnex import config_context from sklearnex.covariance import IncrementalEmpiricalCovariance # Load a sklearn toy dataset with sufficient data @@ -197,10 +198,9 @@ def test_whitened_toy_score(dataframe, queue): # change dataframe X_df = _convert_to_dataframe(X, sycl_queue=queue, target_df=dataframe) - # fit data - est = IncrementalEmpiricalCovariance() - est.fit(X_df) - # location_ attribute approximately zero (10,), covariance_ identity (10,10) + # non-numpy array API inputs (e.g. dpnp) require array_api_dispatch + array_api = dataframe not in ("numpy", "pandas") + context = config_context(array_api_dispatch=True) if array_api else nullcontext() # The log-likelihood can be calculated simply due to covariance_ # use of scipy.linalg.pinvh, np.linalg.sloget and np.cov for estimator @@ -209,7 +209,11 @@ def test_whitened_toy_score(dataframe, queue): -(n - slogdet(pinvh(np.cov(X.T, bias=1)))[1] + n * np.log(2 * np.pi)) / 2 ) # expected_result = -14.1780602988 - result = _as_numpy(est.score(X_df)) + with context: + # fit data; location_ approximately zero (10,), covariance_ identity (10,10) + est = IncrementalEmpiricalCovariance() + est.fit(X_df) + result = _as_numpy(est.score(X_df)) assert_allclose(expected_result, result, atol=1e-6) @@ -296,7 +300,8 @@ def test_score_verify_namespace(dispatch, dataframe, queue): cfg_context = config_context(array_api_dispatch=True) err = pytest.raises((TypeError, ValueError)) else: - # support_sycl_format will cause it to function + # without array_api_dispatch, fitted attributes are host numpy, so a + # numpy score input shares their namespace and the call succeeds cfg_context = nullcontext() err = nullcontext() diff --git a/sklearnex/decomposition/pca.py b/sklearnex/decomposition/pca.py index 9909ba8e91..e8279bc786 100755 --- a/sklearnex/decomposition/pca.py +++ b/sklearnex/decomposition/pca.py @@ -25,6 +25,7 @@ import numpy as np from sklearn.decomposition import PCA as _sklearn_PCA from sklearn.decomposition._pca import _infer_dimension + from sklearn.utils._array_api import get_namespace from sklearn.utils._param_validation import StrOptions from sklearn.utils.extmath import stable_cumsum from sklearn.utils.validation import check_is_fitted @@ -36,10 +37,10 @@ from onedal.utils._array_api import _is_numpy_namespace from onedal.utils.validation import _num_features, _num_samples - from .._device_offload import dispatch, support_sycl_format, wrap_output_data + from .._device_offload import dispatch, wrap_output_data from .._utils import PatchingConditionsChain, register_hyperparameters from ..base import oneDALEstimator - from ..utils._array_api import enable_array_api, get_namespace + from ..utils._array_api import enable_array_api from ..utils.validation import validate_data if sklearn_check_version("1.9"): @@ -83,9 +84,6 @@ def __init__( self.random_state = random_state _onedal_PCA = staticmethod(onedal_PCA) - # guarantee operability with dpnp, runs on CPU unless - # array_api_dispatch is enabled. - score_samples = support_sycl_format(_sklearn_PCA.score_samples) def _onedal_supported(self, method_name, *data): class_name = self.__class__.__name__ @@ -443,7 +441,6 @@ def explained_variance_(self, value): transform.__doc__ = _sklearn_PCA.transform.__doc__ fit_transform.__doc__ = _sklearn_PCA.fit_transform.__doc__ inverse_transform.__doc__ = _sklearn_PCA.inverse_transform.__doc__ - score_samples.__doc__ = _sklearn_PCA.score_samples.__doc__ else: raise ImportError( diff --git a/sklearnex/dummy/_dummy.py b/sklearnex/dummy/_dummy.py index 10d12b498f..b5432e365f 100644 --- a/sklearnex/dummy/_dummy.py +++ b/sklearnex/dummy/_dummy.py @@ -22,6 +22,7 @@ import numpy as np from sklearn.dummy import DummyRegressor as _sklearn_DummyRegressor +from sklearn.utils._array_api import get_namespace from sklearn.utils.validation import check_is_fitted from daal4py.sklearn._n_jobs_support import control_n_jobs @@ -31,7 +32,7 @@ from .._device_offload import dispatch, support_input_format from .._utils import PatchingConditionsChain from ..base import oneDALEstimator -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import validate_data ################ diff --git a/sklearnex/ensemble/_forest.py b/sklearnex/ensemble/_forest.py index e942a11f37..10a92f0a83 100644 --- a/sklearnex/ensemble/_forest.py +++ b/sklearnex/ensemble/_forest.py @@ -40,6 +40,7 @@ ) from sklearn.tree._tree import Tree from sklearn.utils import check_random_state +from sklearn.utils._array_api import get_namespace from sklearn.utils._param_validation import Interval from sklearn.utils.multiclass import check_classification_targets, type_of_target from sklearn.utils.validation import check_array, check_is_fitted @@ -60,7 +61,7 @@ from .._device_offload import dispatch, support_input_format, wrap_output_data from .._utils import PatchingConditionsChain, register_hyperparameters from ..base import oneDALEstimator -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.class_weight import _compute_class_weight from ..utils.validation import _check_sample_weight, assert_all_finite, validate_data diff --git a/sklearnex/linear_model/_base_linear_model.py b/sklearnex/linear_model/_base_linear_model.py index c1c2036cd2..0067f5fab6 100644 --- a/sklearnex/linear_model/_base_linear_model.py +++ b/sklearnex/linear_model/_base_linear_model.py @@ -15,7 +15,7 @@ # ============================================================================== from abc import ABC, abstractmethod -from ..utils._array_api import get_namespace +from sklearn.utils._array_api import get_namespace # Adds common methods to create oneDAL objects after a fallback diff --git a/sklearnex/linear_model/incremental_linear.py b/sklearnex/linear_model/incremental_linear.py index d225be9016..50cc54ef12 100644 --- a/sklearnex/linear_model/incremental_linear.py +++ b/sklearnex/linear_model/incremental_linear.py @@ -21,6 +21,7 @@ from sklearn.linear_model import LinearRegression as _sklearn_LinearRegression from sklearn.metrics import r2_score from sklearn.utils import gen_batches +from sklearn.utils._array_api import get_namespace from sklearn.utils._param_validation import Interval from sklearn.utils.validation import check_is_fitted @@ -37,7 +38,7 @@ register_hyperparameters, ) from ..base import oneDALEstimator -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import validate_data if sklearn_check_version("1.9"): diff --git a/sklearnex/linear_model/incremental_ridge.py b/sklearnex/linear_model/incremental_ridge.py index aade8a3d0c..2302d00b08 100644 --- a/sklearnex/linear_model/incremental_ridge.py +++ b/sklearnex/linear_model/incremental_ridge.py @@ -21,6 +21,7 @@ from sklearn.linear_model import Ridge as _sklearn_Ridge from sklearn.metrics import r2_score from sklearn.utils import gen_batches +from sklearn.utils._array_api import get_namespace from sklearn.utils._param_validation import Interval from sklearn.utils.validation import check_is_fitted @@ -31,7 +32,7 @@ from .._device_offload import dispatch, wrap_output_data from .._utils import PatchingConditionsChain, _add_inc_serialization_note from ..base import oneDALEstimator -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import validate_data if sklearn_check_version("1.9"): diff --git a/sklearnex/linear_model/linear.py b/sklearnex/linear_model/linear.py index c2d9284f82..ca053e4be7 100644 --- a/sklearnex/linear_model/linear.py +++ b/sklearnex/linear_model/linear.py @@ -18,6 +18,7 @@ from sklearn.linear_model import LinearRegression as _sklearn_LinearRegression from sklearn.metrics import r2_score +from sklearn.utils._array_api import get_namespace from sklearn.utils.validation import check_is_fitted from daal4py.sklearn._n_jobs_support import control_n_jobs @@ -29,7 +30,7 @@ from .._device_offload import dispatch, wrap_output_data from .._utils import PatchingConditionsChain, get_patch_message, register_hyperparameters from ..base import oneDALEstimator -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import validate_data from ._base_linear_model import _BaseLinearModel diff --git a/sklearnex/linear_model/logistic_regression.py b/sklearnex/linear_model/logistic_regression.py index 5403d8c972..680143f6db 100644 --- a/sklearnex/linear_model/logistic_regression.py +++ b/sklearnex/linear_model/logistic_regression.py @@ -28,6 +28,7 @@ from scipy.sparse import issparse from sklearn.linear_model import LogisticRegression as _sklearn_LogisticRegression from sklearn.metrics import accuracy_score + from sklearn.utils._array_api import get_namespace from sklearn.utils.multiclass import type_of_target from sklearn.utils.validation import _num_samples, check_is_fitted @@ -39,7 +40,7 @@ from .._config import get_config from .._device_offload import dispatch, wrap_output_data from .._utils import PatchingConditionsChain, get_patch_message - from ..utils._array_api import enable_array_api, get_namespace + from ..utils._array_api import enable_array_api from ..utils.validation import validate_data if sklearn_check_version("1.9"): diff --git a/sklearnex/linear_model/ridge.py b/sklearnex/linear_model/ridge.py index 988b94d803..86ebc08ca4 100644 --- a/sklearnex/linear_model/ridge.py +++ b/sklearnex/linear_model/ridge.py @@ -24,6 +24,7 @@ import numpy as np from sklearn.linear_model import Ridge as _sklearn_Ridge from sklearn.metrics import r2_score + from sklearn.utils._array_api import get_namespace from sklearn.utils.validation import check_is_fitted from daal4py.sklearn._n_jobs_support import control_n_jobs @@ -34,7 +35,7 @@ from .._device_offload import dispatch, wrap_output_data from .._utils import PatchingConditionsChain from ..base import oneDALEstimator - from ..utils._array_api import enable_array_api, get_namespace + from ..utils._array_api import enable_array_api from ..utils.validation import validate_data from ._base_linear_model import _BaseLinearModel diff --git a/sklearnex/neighbors/_lof.py b/sklearnex/neighbors/_lof.py index 9c1cf3dc37..d5c0372f8c 100644 --- a/sklearnex/neighbors/_lof.py +++ b/sklearnex/neighbors/_lof.py @@ -19,6 +19,7 @@ import numpy as np from sklearn.neighbors import LocalOutlierFactor as _sklearn_LocalOutlierFactor +from sklearn.utils._array_api import get_namespace from sklearn.utils.metaestimators import available_if from sklearn.utils.validation import check_is_fitted @@ -29,7 +30,7 @@ from sklearnex.neighbors.common import KNeighborsDispatchingBase from sklearnex.neighbors.knn_unsupervised import NearestNeighbors -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api @enable_array_api diff --git a/sklearnex/neighbors/common.py b/sklearnex/neighbors/common.py index 5a1b9676ff..9a1fca31bd 100644 --- a/sklearnex/neighbors/common.py +++ b/sklearnex/neighbors/common.py @@ -23,6 +23,7 @@ from sklearn.neighbors._base import VALID_METRICS, KNeighborsMixin from sklearn.neighbors._base import NeighborsBase as _sklearn_NeighborsBase from sklearn.neighbors._kd_tree import KDTree +from sklearn.utils._array_api import get_namespace from sklearn.utils.validation import check_array, check_is_fitted from daal4py.sklearn._utils import is_sparse, sklearn_check_version @@ -37,7 +38,6 @@ from .._utils import PatchingConditionsChain from ..base import oneDALEstimator -from ..utils._array_api import get_namespace class KNeighborsDispatchingBase(oneDALEstimator): diff --git a/sklearnex/neighbors/knn_classification.py b/sklearnex/neighbors/knn_classification.py index 0df2a260cc..c70e32b76d 100755 --- a/sklearnex/neighbors/knn_classification.py +++ b/sklearnex/neighbors/knn_classification.py @@ -18,6 +18,7 @@ from sklearn.neighbors._classification import ( KNeighborsClassifier as _sklearn_KNeighborsClassifier, ) +from sklearn.utils._array_api import get_namespace from sklearn.utils.multiclass import check_classification_targets from sklearn.utils.validation import check_is_fitted @@ -29,7 +30,7 @@ from onedal.utils._array_api import _is_numpy_namespace from .._device_offload import dispatch, wrap_output_data -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import validate_data from .common import KNeighborsDispatchingBase diff --git a/sklearnex/neighbors/knn_regression.py b/sklearnex/neighbors/knn_regression.py index fa2240ccd4..19f362009f 100755 --- a/sklearnex/neighbors/knn_regression.py +++ b/sklearnex/neighbors/knn_regression.py @@ -18,6 +18,7 @@ from sklearn.neighbors._regression import ( KNeighborsRegressor as _sklearn_KNeighborsRegressor, ) +from sklearn.utils._array_api import get_namespace from sklearn.utils.validation import check_is_fitted from daal4py.sklearn._n_jobs_support import control_n_jobs @@ -27,7 +28,7 @@ from onedal.utils._array_api import _is_numpy_namespace from .._device_offload import dispatch, wrap_output_data -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import validate_data from .common import KNeighborsDispatchingBase diff --git a/sklearnex/neighbors/knn_unsupervised.py b/sklearnex/neighbors/knn_unsupervised.py index dd5b056e3f..c490a29877 100755 --- a/sklearnex/neighbors/knn_unsupervised.py +++ b/sklearnex/neighbors/knn_unsupervised.py @@ -15,6 +15,7 @@ # =============================================================================== from sklearn.neighbors._unsupervised import NearestNeighbors as _sklearn_NearestNeighbors +from sklearn.utils._array_api import get_namespace from sklearn.utils.validation import _deprecate_positional_args, check_is_fitted from daal4py.sklearn._n_jobs_support import control_n_jobs @@ -25,7 +26,7 @@ from onedal.utils._array_api import _is_numpy_namespace from .._device_offload import dispatch, wrap_output_data -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ..utils.validation import validate_data from .common import KNeighborsDispatchingBase diff --git a/sklearnex/preview/covariance/covariance.py b/sklearnex/preview/covariance/covariance.py index 208effc5a9..634033cf67 100644 --- a/sklearnex/preview/covariance/covariance.py +++ b/sklearnex/preview/covariance/covariance.py @@ -17,9 +17,9 @@ import warnings from functools import partial -import numpy as np from sklearn.base import clone from sklearn.covariance import EmpiricalCovariance as _sklearn_EmpiricalCovariance +from sklearn.utils._array_api import get_namespace from sklearn.utils.validation import check_array, check_is_fitted from daal4py.sklearn._n_jobs_support import control_n_jobs @@ -30,15 +30,10 @@ from onedal.utils.validation import _num_features from sklearnex import config_context -from ..._device_offload import ( - dispatch, - support_input_format, - support_sycl_format, - wrap_output_data, -) +from ..._device_offload import dispatch, support_input_format, wrap_output_data from ..._utils import PatchingConditionsChain, register_hyperparameters from ...base import oneDALEstimator -from ...utils._array_api import _pinvh, enable_array_api, get_namespace, log_likelihood +from ...utils._array_api import _pinvh, enable_array_api, log_likelihood from ...utils.validation import assert_all_finite, validate_data if sklearn_check_version("1.9"): @@ -142,14 +137,11 @@ def fit(self, X, y=None): return self @wrap_output_data - @support_sycl_format def score(self, X_test, y=None): check_is_fitted(self) if sklearn_check_version("1.9"): check_same_namespace(X_test, self, attribute="covariance_", method="score") - # Only covariance evaluated for get_namespace due to dpnp - # support without array_api_dispatch xp, _ = get_namespace(X_test, self.covariance_) X = validate_data( @@ -165,7 +157,7 @@ def score(self, X_test, y=None): est = clone(self) est.set_params(assume_centered=True) - # test_cov is a numpy array, but calculated on device + # ensure test_cov shares X's namespace and device before log_likelihood test_cov = est.fit(X - self.location_).covariance_ if not _is_numpy_namespace(xp): test_cov = xp.asarray(test_cov, device=X.device) @@ -174,20 +166,15 @@ def score(self, X_test, y=None): return res @wrap_output_data - @support_sycl_format def error_norm(self, comp_cov, norm="frobenius", scaling=True, squared=True): # equivalent to the sklearn implementation but written for array API # in the case of numpy-like inputs it will use sklearn's version instead. # This can be deprecated if/when sklearn makes the equivalent array API enabled. - # This includes a validate_data call and an unusual call to get_namespace in - # order to also support dpnp without array_api_dispatch. check_is_fitted(self) if sklearn_check_version("1.9"): check_same_namespace( comp_cov, self, attribute="covariance_", method="error_norm" ) - # Only covariance evaluated for get_namespace due to dpnp - # support without array_api_dispatch xp, _ = get_namespace(comp_cov, self.covariance_) c_cov = validate_data( self, @@ -223,7 +210,6 @@ def error_norm(self, comp_cov, norm="frobenius", scaling=True, squared=True): return result # expose sklearnex pairwise_distances if mahalanobis distance eventually supported - @support_sycl_format def mahalanobis(self, X): # This must be done as ```support_input_format``` is insufficient for array API # support when attributes are non-numpy. @@ -236,12 +222,6 @@ def mahalanobis(self, X): # do not check dtype, done in pairwise_distances X_in = validate_data(self, X, reset=False) - if not _is_numpy_namespace(xp) and isinstance(X_in, np.ndarray): - # corrects issues with respect to dpnp support without array_api_dispatch - X_in = X - loc = xp.asarray(loc, device=X.device) - precision = xp.asarray(precision, device=X.device) - with config_context(assume_finite=True): try: dist = _mahalanobis(X_in, loc, VI=precision) diff --git a/sklearnex/preview/decomposition/incremental_pca.py b/sklearnex/preview/decomposition/incremental_pca.py index d20b631ef4..5d2165454a 100644 --- a/sklearnex/preview/decomposition/incremental_pca.py +++ b/sklearnex/preview/decomposition/incremental_pca.py @@ -16,6 +16,7 @@ from sklearn.decomposition import IncrementalPCA as _sklearn_IncrementalPCA from sklearn.utils import check_array, gen_batches +from sklearn.utils._array_api import get_namespace from sklearn.utils._param_validation import StrOptions from sklearn.utils.validation import check_is_fitted @@ -26,7 +27,7 @@ from ..._device_offload import dispatch, wrap_output_data from ..._utils import PatchingConditionsChain, _add_inc_serialization_note from ...base import oneDALEstimator -from ...utils._array_api import enable_array_api, get_namespace +from ...utils._array_api import enable_array_api from ...utils.validation import validate_data if sklearn_check_version("1.9"): diff --git a/sklearnex/preview/preprocessing/_data.py b/sklearnex/preview/preprocessing/_data.py index 483f750aac..4dba257784 100644 --- a/sklearnex/preview/preprocessing/_data.py +++ b/sklearnex/preview/preprocessing/_data.py @@ -18,6 +18,7 @@ from sklearn.preprocessing import MaxAbsScaler as _sklearn_MaxAbsScaler from sklearn.preprocessing._data import _handle_zeros_in_scale +from sklearn.utils._array_api import get_namespace from sklearn.utils.validation import check_array, check_is_fitted from daal4py.sklearn._n_jobs_support import control_n_jobs @@ -26,10 +27,10 @@ IncrementalBasicStatistics as onedal_IncrementalBasicStatistics, ) -from ..._device_offload import dispatch, support_sycl_format, wrap_output_data +from ..._device_offload import dispatch from ..._utils import PatchingConditionsChain from ...base import oneDALEstimator -from ...utils._array_api import enable_array_api, get_namespace +from ...utils._array_api import enable_array_api from ...utils.validation import assert_all_finite, validate_data __check_kwargs = { @@ -190,9 +191,8 @@ def fit(self, X, y=None): ) return self - # Transform relies completely on standard scikit-learn functionality and does not need to - # be overridden using oneDAL capabilities as the scale vectors are appropriately populated. - transform = support_sycl_format(_sklearn_MaxAbsScaler.transform) + # transform is inherited unchanged from scikit-learn: it relies only on the fitted + # scale vectors and array API operations, so no oneDAL override is needed. # Ensure access to the derived properties without manually calling _onedal_finalize_fit # explicitly from the user. We wrap properties that require a finalized state. diff --git a/sklearnex/preview/preprocessing/tests/test_data.py b/sklearnex/preview/preprocessing/tests/test_data.py index 7ae59c5d15..1f5f120f41 100644 --- a/sklearnex/preview/preprocessing/tests/test_data.py +++ b/sklearnex/preview/preprocessing/tests/test_data.py @@ -14,6 +14,8 @@ # limitations under the License. # ============================================================================== +from contextlib import nullcontext + import numpy as np import pytest from numpy.testing import assert_allclose @@ -53,17 +55,23 @@ def test_max_abs_scaler_dense_fit_transform(dataframe, queue): X_df = _convert_to_dataframe(X, sycl_queue=queue, target_df=dataframe) + # non-numpy array API inputs (e.g. dpnp) require array_api_dispatch + array_api = dataframe not in ("numpy", "pandas") + context = config_context(array_api_dispatch=True) if array_api else nullcontext() + # Scikit-learn Baseline scaler_sk = _sklearn_MaxAbsScaler() X_trans_sk = scaler_sk.fit_transform(X) # Sklearnex scaler_ex = MaxAbsScaler() - X_trans_ex = scaler_ex.fit_transform(X_df) + with context: + X_trans_ex = scaler_ex.fit_transform(X_df) X_trans_ex_np = _as_numpy(X_trans_ex) - assert_allclose(scaler_ex.scale_, scaler_sk.scale_) - assert_allclose(scaler_ex.max_abs_, scaler_sk.max_abs_) + # under array_api_dispatch, fitted attributes match the input namespace (e.g. dpnp) + assert_allclose(_as_numpy(scaler_ex.scale_), scaler_sk.scale_) + assert_allclose(_as_numpy(scaler_ex.max_abs_), scaler_sk.max_abs_) assert_allclose(X_trans_ex_np, X_trans_sk) @@ -82,19 +90,25 @@ def test_max_abs_scaler_dense_partial_fit(dataframe, queue): scaler_sk.partial_fit(batch) X_trans_sk = scaler_sk.transform(X) + # non-numpy array API inputs (e.g. dpnp) require array_api_dispatch + array_api = dataframe not in ("numpy", "pandas") + context = config_context(array_api_dispatch=True) if array_api else nullcontext() + # Sklearnex execution scaler_ex = MaxAbsScaler() - for batch in [X1, X2, X3]: - batch_df = _convert_to_dataframe(batch, sycl_queue=queue, target_df=dataframe) - scaler_ex.partial_fit(batch_df) - X_df = _convert_to_dataframe(X, sycl_queue=queue, target_df=dataframe) - X_trans_ex = scaler_ex.transform(X_df) + with context: + for batch in [X1, X2, X3]: + batch_df = _convert_to_dataframe(batch, sycl_queue=queue, target_df=dataframe) + scaler_ex.partial_fit(batch_df) + + X_trans_ex = scaler_ex.transform(X_df) X_trans_ex_np = _as_numpy(X_trans_ex) assert scaler_ex.n_samples_seen_ == scaler_sk.n_samples_seen_ - assert_allclose(scaler_ex.scale_, scaler_sk.scale_) - assert_allclose(scaler_ex.max_abs_, scaler_sk.max_abs_) + # under array_api_dispatch, fitted attributes match the input namespace (e.g. dpnp) + assert_allclose(_as_numpy(scaler_ex.scale_), scaler_sk.scale_) + assert_allclose(_as_numpy(scaler_ex.max_abs_), scaler_sk.max_abs_) assert_allclose(X_trans_ex_np, X_trans_sk) @@ -116,8 +130,12 @@ def test_max_abs_scaler_array_api_dispatch(dataframe, queue): assert hasattr(est, "scale_") assert hasattr(est, "max_abs_") - est.scale_ = np.ones(est.scale_.shape) - X_trans_modified = est.transform(X_df) + # scale_ must share the input namespace/device (dpnp) so transform's + # in-place division does not mix numpy and dpnp arrays under dispatch + xp = X_df.__array_namespace__() + est.scale_ = xp.ones(est.scale_.shape, device=X_df.device) + with config_context(array_api_dispatch=True): + X_trans_modified = est.transform(X_df) X_np = _as_numpy(X_df) X_trans_modified_np = _as_numpy(X_trans_modified) diff --git a/sklearnex/spmd/basic_statistics/tests/test_basic_statistics_spmd.py b/sklearnex/spmd/basic_statistics/tests/test_basic_statistics_spmd.py index 48d83e63df..f8355d3c72 100644 --- a/sklearnex/spmd/basic_statistics/tests/test_basic_statistics_spmd.py +++ b/sklearnex/spmd/basic_statistics/tests/test_basic_statistics_spmd.py @@ -84,11 +84,8 @@ def test_basic_stats_spmd_gold(dataframe, queue): "dataframe,queue", get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi -def test_basic_stats_spmd_synthetic( - n_samples, n_features, dataframe, queue, dtype, array_api_dispatch -): +def test_basic_stats_spmd_synthetic(n_samples, n_features, dataframe, queue, dtype): # Import spmd and batch algo from onedal.basic_statistics import BasicStatistics as BasicStatistics_Batch from sklearnex.spmd.basic_statistics import BasicStatistics as BasicStatistics_SPMD @@ -102,7 +99,7 @@ def test_basic_stats_spmd_synthetic( # Ensure results of batch algo match spmd # Configure array API dispatch status for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = BasicStatistics_SPMD().fit(local_dpt_data) batch_result = BasicStatistics_Batch().fit(data) diff --git a/sklearnex/spmd/basic_statistics/tests/test_incremental_basic_statistics_spmd.py b/sklearnex/spmd/basic_statistics/tests/test_incremental_basic_statistics_spmd.py index 0e16e1c481..e75531a741 100644 --- a/sklearnex/spmd/basic_statistics/tests/test_incremental_basic_statistics_spmd.py +++ b/sklearnex/spmd/basic_statistics/tests/test_incremental_basic_statistics_spmd.py @@ -254,7 +254,6 @@ def test_incremental_basic_statistics_single_option_partial_fit_spmd_gold( @pytest.mark.parametrize("n_samples", [100, 10000]) @pytest.mark.parametrize("n_features", [10, 100]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_incremental_basic_statistics_partial_fit_spmd_synthetic( dataframe, @@ -264,7 +263,6 @@ def test_incremental_basic_statistics_partial_fit_spmd_synthetic( n_samples, n_features, dtype, - array_api_dispatch, ): # Import spmd and batch algo from sklearnex.basic_statistics import IncrementalBasicStatistics @@ -305,7 +303,7 @@ def test_incremental_basic_statistics_partial_fit_spmd_synthetic( split_weights[i], sycl_queue=queue, target_df=dataframe ) # Configure array API dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): incbs_spmd.partial_fit( local_dpt_data, sample_weight=local_dpt_weights if weighted else None ) diff --git a/sklearnex/spmd/cluster/tests/test_dbscan_spmd.py b/sklearnex/spmd/cluster/tests/test_dbscan_spmd.py index 71b7341d67..4c534a3028 100644 --- a/sklearnex/spmd/cluster/tests/test_dbscan_spmd.py +++ b/sklearnex/spmd/cluster/tests/test_dbscan_spmd.py @@ -70,7 +70,6 @@ def test_dbscan_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_dbscan_spmd_synthetic( n_samples, @@ -80,7 +79,6 @@ def test_dbscan_spmd_synthetic( dataframe, queue, dtype, - array_api_dispatch, ): n_features, eps = n_features_and_eps # Import spmd and batch algo @@ -97,7 +95,7 @@ def test_dbscan_spmd_synthetic( # Ensure labels from fit of batch algo matches spmd # Configure array API dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model = DBSCAN_SPMD(eps=eps, min_samples=min_samples).fit(local_dpt_data) batch_model = DBSCAN_Batch(eps=eps, min_samples=min_samples).fit(data) diff --git a/sklearnex/spmd/cluster/tests/test_kmeans_spmd.py b/sklearnex/spmd/cluster/tests/test_kmeans_spmd.py index 0b576d27c0..055d5d52a7 100644 --- a/sklearnex/spmd/cluster/tests/test_kmeans_spmd.py +++ b/sklearnex/spmd/cluster/tests/test_kmeans_spmd.py @@ -110,10 +110,9 @@ def test_kmeans_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_kmeans_spmd_synthetic( - n_samples, n_features, n_clusters, dataframe, queue, dtype, array_api_dispatch + n_samples, n_features, n_clusters, dataframe, queue, dtype ): # Import spmd and batch algo from sklearnex.cluster import KMeans as KMeans_Batch @@ -133,7 +132,7 @@ def test_kmeans_spmd_synthetic( # Validate KMeans init # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model_init = KMeans_SPMD( n_clusters=n_clusters, max_iter=1, random_state=0 ).fit(local_dpt_X_train) @@ -148,7 +147,7 @@ def test_kmeans_spmd_synthetic( n_clusters=n_clusters, init=spmd_model_init.cluster_centers_, random_state=0 ) # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model.fit(local_dpt_X_train) batch_model = KMeans_Batch( n_clusters=n_clusters, @@ -172,7 +171,7 @@ def test_kmeans_spmd_synthetic( # Ensure predictions of batch algo match spmd # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = spmd_model.predict(local_dpt_X_test) batch_result = batch_model.predict(X_test) diff --git a/sklearnex/spmd/covariance/tests/test_covariance_spmd.py b/sklearnex/spmd/covariance/tests/test_covariance_spmd.py index 3a049a0571..9dbc3b8897 100644 --- a/sklearnex/spmd/covariance/tests/test_covariance_spmd.py +++ b/sklearnex/spmd/covariance/tests/test_covariance_spmd.py @@ -86,10 +86,9 @@ def test_covariance_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_covariance_spmd_synthetic( - n_samples, n_features, assume_centered, dataframe, queue, dtype, array_api_dispatch + n_samples, n_features, assume_centered, dataframe, queue, dtype ): # Import spmd and batch algo from sklearnex.preview.covariance import ( @@ -105,7 +104,7 @@ def test_covariance_spmd_synthetic( ) # Ensure results of batch algo match spmd - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = EmpiricalCovariance_SPMD(assume_centered=assume_centered).fit( local_dpt_data ) diff --git a/sklearnex/spmd/covariance/tests/test_incremental_covariance_spmd.py b/sklearnex/spmd/covariance/tests/test_incremental_covariance_spmd.py index f3348c7fbf..b7a935a700 100644 --- a/sklearnex/spmd/covariance/tests/test_incremental_covariance_spmd.py +++ b/sklearnex/spmd/covariance/tests/test_incremental_covariance_spmd.py @@ -151,7 +151,6 @@ def test_incremental_covariance_partial_fit_spmd_gold( "dataframe,queue", get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_incremental_covariance_partial_fit_spmd_synthetic( n_samples, @@ -161,7 +160,6 @@ def test_incremental_covariance_partial_fit_spmd_synthetic( dataframe, queue, dtype, - array_api_dispatch, ): # Import spmd and batch algo from sklearnex.covariance import IncrementalEmpiricalCovariance @@ -185,7 +183,7 @@ def test_incremental_covariance_partial_fit_spmd_synthetic( split_local_data[i], sycl_queue=queue, target_df=dataframe ) # Configure array API dispatch status for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): inccov_spmd.partial_fit(local_dpt_data) inccov.fit(dpt_data) diff --git a/sklearnex/spmd/decomposition/tests/test_incremental_pca_spmd.py b/sklearnex/spmd/decomposition/tests/test_incremental_pca_spmd.py index bd298d55db..bfce960bdf 100644 --- a/sklearnex/spmd/decomposition/tests/test_incremental_pca_spmd.py +++ b/sklearnex/spmd/decomposition/tests/test_incremental_pca_spmd.py @@ -219,7 +219,6 @@ def test_incremental_pca_fit_spmd_random( @pytest.mark.parametrize("num_samples", [200, 400]) @pytest.mark.parametrize("num_features", [10, 20]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_incremental_pca_partial_fit_spmd_random( dataframe, @@ -230,7 +229,6 @@ def test_incremental_pca_partial_fit_spmd_random( num_samples, num_features, dtype, - array_api_dispatch, ): # Import spmd and non-SPMD algo from sklearnex.preview.decomposition import IncrementalPCA @@ -256,20 +254,23 @@ def test_incremental_pca_partial_fit_spmd_random( ) dpt_X = _convert_to_dataframe(X_split[i], sycl_queue=queue, target_df=dataframe) # Configure array API dispatch status for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): incpca_spmd.partial_fit(local_dpt_X) incpca.partial_fit(dpt_X) - for attribute in attributes_to_compare: - assert_allclose( - _as_numpy(getattr(incpca, attribute)), - _as_numpy(getattr(incpca_spmd, attribute)), - atol=tol, - err_msg=f"{attribute} is incorrect", - ) + # Accessing an spmd attribute triggers the deferred finalize, which uses the + # array namespace of the stored (dpnp) data and must run under dispatch. + with config_context(array_api_dispatch=True): + for attribute in attributes_to_compare: + assert_allclose( + _as_numpy(getattr(incpca, attribute)), + _as_numpy(getattr(incpca_spmd, attribute)), + atol=tol, + err_msg=f"{attribute} is incorrect", + ) # Configure array API dispatch status for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): y_trans_spmd = incpca_spmd.transform(dpt_X_test) y_trans = incpca.transform(dpt_X_test) diff --git a/sklearnex/spmd/decomposition/tests/test_pca_spmd.py b/sklearnex/spmd/decomposition/tests/test_pca_spmd.py index 8a899fcf73..d5eb5f3252 100644 --- a/sklearnex/spmd/decomposition/tests/test_pca_spmd.py +++ b/sklearnex/spmd/decomposition/tests/test_pca_spmd.py @@ -93,7 +93,6 @@ def test_pca_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_pca_spmd_synthetic( n_samples, @@ -103,7 +102,6 @@ def test_pca_spmd_synthetic( dataframe, queue, dtype, - array_api_dispatch, ): # TODO: Resolve issues with batch fallback and lack of support for n_rows_rank < n_cols if n_components == "mle" or n_components == 3: @@ -123,7 +121,7 @@ def test_pca_spmd_synthetic( ) # Ensure results of batch algo match spmd - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = PCA_SPMD(n_components=n_components, whiten=whiten).fit( local_dpt_data ) diff --git a/sklearnex/spmd/ensemble/tests/test_forest_spmd.py b/sklearnex/spmd/ensemble/tests/test_forest_spmd.py index e4ede074c5..22283abe4f 100644 --- a/sklearnex/spmd/ensemble/tests/test_forest_spmd.py +++ b/sklearnex/spmd/ensemble/tests/test_forest_spmd.py @@ -110,7 +110,6 @@ def test_rfcls_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_rfcls_spmd_synthetic( n_samples, @@ -121,7 +120,6 @@ def test_rfcls_spmd_synthetic( dataframe, queue, dtype, - array_api_dispatch, ): n_features, n_classes = n_features_and_classes # Import spmd and batch algo @@ -153,13 +151,13 @@ def test_rfcls_spmd_synthetic( random_state=0, ) # Configure array API dispatch status for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model.fit(local_dpt_X_train, local_dpt_y_train) batch_model = RandomForestClassifier_Batch( n_estimators=n_estimators, max_depth=max_depth, random_state=0 ).fit(X_train, y_train) # Configure array API dispatch status for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = spmd_model.predict(local_dpt_X_test) batch_result = batch_model.predict(X_test) @@ -245,7 +243,6 @@ def test_rfreg_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_rfreg_spmd_synthetic( n_samples, @@ -256,7 +253,6 @@ def test_rfreg_spmd_synthetic( dataframe, queue, dtype, - array_api_dispatch, ): # Import spmd and batch algo from sklearnex.ensemble import RandomForestRegressor as RandomForestRegressor_Batch @@ -280,7 +276,7 @@ def test_rfreg_spmd_synthetic( ) # Ensure predictions of batch algo match spmd - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model = RandomForestRegressor_SPMD( n_estimators=n_estimators, max_depth=max_depth, @@ -290,7 +286,7 @@ def test_rfreg_spmd_synthetic( batch_model = RandomForestRegressor_Batch( n_estimators=n_estimators, max_depth=max_depth, random_state=0 ).fit(X_train, y_train) - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = spmd_model.predict(local_dpt_X_test) batch_result = batch_model.predict(X_test) diff --git a/sklearnex/spmd/linear_model/tests/test_incremental_linear_spmd.py b/sklearnex/spmd/linear_model/tests/test_incremental_linear_spmd.py index b38bb655df..9ae6eed3e5 100644 --- a/sklearnex/spmd/linear_model/tests/test_incremental_linear_spmd.py +++ b/sklearnex/spmd/linear_model/tests/test_incremental_linear_spmd.py @@ -269,7 +269,6 @@ def test_incremental_linear_regression_fit_spmd_random( @pytest.mark.parametrize("num_features", [5, 10]) @pytest.mark.parametrize("macro_block", [None, 1024]) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_incremental_linear_regression_partial_fit_spmd_random( dataframe, @@ -280,7 +279,6 @@ def test_incremental_linear_regression_partial_fit_spmd_random( num_features, macro_block, dtype, - array_api_dispatch, ): # Import spmd and non-SPMD algo from sklearnex.linear_model import IncrementalLinearRegression @@ -328,7 +326,7 @@ def test_incremental_linear_regression_partial_fit_spmd_random( dpt_y = _convert_to_dataframe(y_split[i], sycl_queue=queue, target_df=dataframe) # Configure array API dispatch status for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): inclin_spmd.partial_fit(local_dpt_X, local_dpt_y) inclin.partial_fit(dpt_X, dpt_y) @@ -338,7 +336,7 @@ def test_incremental_linear_regression_partial_fit_spmd_random( _as_numpy(inclin.intercept_), _as_numpy(inclin_spmd.intercept_), atol=tol ) - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): y_pred_spmd = inclin_spmd.predict(dpt_X_test) y_pred = inclin.predict(dpt_X_test) diff --git a/sklearnex/spmd/linear_model/tests/test_linear_regression_spmd.py b/sklearnex/spmd/linear_model/tests/test_linear_regression_spmd.py index 3eb147af81..0402fc65e3 100644 --- a/sklearnex/spmd/linear_model/tests/test_linear_regression_spmd.py +++ b/sklearnex/spmd/linear_model/tests/test_linear_regression_spmd.py @@ -105,11 +105,8 @@ def test_linear_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi -def test_linear_spmd_synthetic( - n_samples, n_features, dataframe, queue, dtype, array_api_dispatch -): +def test_linear_spmd_synthetic(n_samples, n_features, dataframe, queue, dtype): # Import spmd and batch algo from sklearnex.linear_model import LinearRegression as LinearRegression_Batch from sklearnex.spmd.linear_model import LinearRegression as LinearRegression_SPMD @@ -138,7 +135,7 @@ def test_linear_spmd_synthetic( # ensure trained model of batch algo matches spmd # Configure array API dispatch status for spmd estimator spmd_model = LinearRegression_SPMD() - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model.fit(local_dpt_X_train, local_dpt_y_train) batch_model = LinearRegression_Batch().fit(X_train, y_train) @@ -155,7 +152,7 @@ def test_linear_spmd_synthetic( # ensure predictions of batch algo match spmd # Configure raw input status for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = spmd_model.predict(local_dpt_X_test) batch_result = batch_model.predict(X_test) diff --git a/sklearnex/spmd/linear_model/tests/test_logistic_regression_spmd.py b/sklearnex/spmd/linear_model/tests/test_logistic_regression_spmd.py index ebd63e6d0c..42fe7a42e8 100644 --- a/sklearnex/spmd/linear_model/tests/test_logistic_regression_spmd.py +++ b/sklearnex/spmd/linear_model/tests/test_logistic_regression_spmd.py @@ -115,11 +115,8 @@ def test_logistic_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi -def test_logistic_spmd_synthetic( - n_samples, n_features, C, tol, dataframe, queue, dtype, array_api_dispatch -): +def test_logistic_spmd_synthetic(n_samples, n_features, C, tol, dataframe, queue, dtype): # TODO: Resolve numerical issues when n_rows_rank < n_cols if n_samples <= n_features: pytest.skip("Numerical issues when rank rows < columns") @@ -149,7 +146,7 @@ def test_logistic_spmd_synthetic( # Ensure trained model of batch algo matches spmd spmd_model = LogisticRegression_SPMD(random_state=0, solver="newton-cg", C=C, tol=tol) # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model.fit(local_dpt_X_train, local_dpt_y_train) batch_model = LogisticRegression_Batch( random_state=0, solver="newton-cg", C=C, tol=tol @@ -169,7 +166,7 @@ def test_logistic_spmd_synthetic( # Ensure predictions of batch algo match spmd # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = spmd_model.predict(local_dpt_X_test) batch_result = batch_model.predict(dpt_X_test) diff --git a/sklearnex/spmd/neighbors/neighbors.py b/sklearnex/spmd/neighbors/neighbors.py index 9c7741b3de..a8ee0a7521 100644 --- a/sklearnex/spmd/neighbors/neighbors.py +++ b/sklearnex/spmd/neighbors/neighbors.py @@ -14,6 +14,8 @@ # limitations under the License. # ============================================================================== +from sklearn.utils._array_api import get_namespace + from onedal.spmd.neighbors import KNeighborsClassifier as onedal_KNeighborsClassifier from onedal.spmd.neighbors import KNeighborsRegressor as onedal_KNeighborsRegressor from onedal.spmd.neighbors import NearestNeighbors as onedal_NearestNeighbors @@ -22,7 +24,6 @@ from ...neighbors import KNeighborsClassifier as base_KNeighborsClassifier from ...neighbors import KNeighborsRegressor as base_KNeighborsRegressor from ...neighbors import NearestNeighbors as base_NearestNeighbors -from ...utils._array_api import get_namespace class KNeighborsClassifier(base_KNeighborsClassifier): diff --git a/sklearnex/spmd/neighbors/tests/test_neighbors_spmd.py b/sklearnex/spmd/neighbors/tests/test_neighbors_spmd.py index 2f35d98782..13647fee0b 100644 --- a/sklearnex/spmd/neighbors/tests/test_neighbors_spmd.py +++ b/sklearnex/spmd/neighbors/tests/test_neighbors_spmd.py @@ -112,7 +112,6 @@ def test_knncls_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_knncls_spmd_synthetic( n_samples, @@ -122,7 +121,6 @@ def test_knncls_spmd_synthetic( dataframe, queue, dtype, - array_api_dispatch, metric="euclidean", ): n_features, n_classes = n_features_and_classes @@ -150,7 +148,7 @@ def test_knncls_spmd_synthetic( n_neighbors=n_neighbors, weights=weights, metric=metric, algorithm="brute" ) # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model.fit(local_dpt_X_train, local_dpt_y_train) batch_model = KNeighborsClassifier_Batch( n_neighbors=n_neighbors, weights=weights, metric=metric, algorithm="brute" @@ -158,7 +156,7 @@ def test_knncls_spmd_synthetic( spmd_dists, spmd_indcs = spmd_model.kneighbors(local_dpt_X_test) batch_dists, batch_indcs = batch_model.kneighbors(X_test) # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = spmd_model.predict(local_dpt_X_test) batch_result = batch_model.predict(X_test) @@ -250,7 +248,6 @@ def test_knnreg_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_knnreg_spmd_synthetic( n_samples, @@ -261,7 +258,6 @@ def test_knnreg_spmd_synthetic( dataframe, queue, dtype, - array_api_dispatch, ): # Import spmd and batch algo from sklearnex.neighbors import KNeighborsRegressor as KNeighborsRegressor_Batch @@ -287,7 +283,7 @@ def test_knnreg_spmd_synthetic( n_neighbors=n_neighbors, weights=weights, metric=metric, algorithm="brute" ) # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model.fit(local_dpt_X_train, local_dpt_y_train) batch_model = KNeighborsRegressor_Batch( n_neighbors=n_neighbors, weights=weights, metric=metric, algorithm="brute" @@ -295,7 +291,7 @@ def test_knnreg_spmd_synthetic( spmd_dists, spmd_indcs = spmd_model.kneighbors(local_dpt_X_test) batch_dists, batch_indcs = batch_model.kneighbors(X_test) # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_result = spmd_model.predict(local_dpt_X_test) batch_result = batch_model.predict(X_test) @@ -352,14 +348,12 @@ def test_knnsearch_spmd_gold(dataframe, queue): get_dataframes_and_queues(dataframe_filter_="dpnp", device_filter_="gpu"), ) @pytest.mark.parametrize("dtype", [np.float32, np.float64]) -@pytest.mark.parametrize("array_api_dispatch", [True, False]) @pytest.mark.mpi def test_knnsearch_spmd_synthetic( dimensions, dataframe, queue, dtype, - array_api_dispatch, ): if dimensions["n"] > 10000 and dtype == np.float32: pytest.skip("Skipping large float32 test due to expected precision issues") @@ -377,7 +371,7 @@ def test_knnsearch_spmd_synthetic( # Ensure search results of batch algo match spmd # Configure array_api_dispatch for spmd estimator - with config_context(array_api_dispatch=array_api_dispatch): + with config_context(array_api_dispatch=True): spmd_model = NearestNeighbors_SPMD( n_neighbors=dimensions["k"], algorithm="brute" ).fit(local_dpt_X_train) diff --git a/sklearnex/svm/_base.py b/sklearnex/svm/_base.py index 5978ac2aff..14ce6e1a9d 100644 --- a/sklearnex/svm/_base.py +++ b/sklearnex/svm/_base.py @@ -27,6 +27,7 @@ from sklearn.svm._base import BaseLibSVM as _sklearn_BaseLibSVM from sklearn.svm._base import BaseSVC as _sklearn_BaseSVC from sklearn.utils import indexable +from sklearn.utils._array_api import get_namespace from sklearn.utils._response import _get_response_values from sklearn.utils.metaestimators import available_if from sklearn.utils.multiclass import check_classification_targets @@ -46,7 +47,6 @@ from .._device_offload import dispatch, wrap_output_data from .._utils import PatchingConditionsChain from ..base import oneDALEstimator -from ..utils._array_api import get_namespace from ..utils.class_weight import _compute_class_weight from ..utils.validation import _check_sample_weight, validate_data diff --git a/sklearnex/svm/_classes.py b/sklearnex/svm/_classes.py index 1f4fae6e23..e2f9acd0d0 100644 --- a/sklearnex/svm/_classes.py +++ b/sklearnex/svm/_classes.py @@ -20,6 +20,7 @@ from sklearn.svm import SVR as _sklearn_SVR from sklearn.svm import NuSVC as _sklearn_NuSVC from sklearn.svm import NuSVR as _sklearn_NuSVR +from sklearn.utils._array_api import get_namespace from sklearn.utils.multiclass import type_of_target from sklearn.utils.validation import _deprecate_positional_args @@ -32,7 +33,7 @@ from .._device_offload import dispatch from .._utils import PatchingConditionsChain -from ..utils._array_api import enable_array_api, get_namespace +from ..utils._array_api import enable_array_api from ._base import BaseSVC, BaseSVR # array API support limited to sklearn 1.5 for regressors due to an incorrect array API diff --git a/sklearnex/tests/test_patching.py b/sklearnex/tests/test_patching.py index 12a2cb282b..d6faed0e03 100755 --- a/sklearnex/tests/test_patching.py +++ b/sklearnex/tests/test_patching.py @@ -32,6 +32,7 @@ from sklearn.base import BaseEstimator, is_clusterer, is_regressor from sklearn.svm._base import BaseLibSVM from sklearn.utils import get_tags +from sklearn.utils._array_api import get_namespace from daal4py.sklearn._utils import ( _package_check_version, @@ -58,7 +59,6 @@ gen_dataset, gen_models_info, ) -from sklearnex.utils._array_api import get_namespace @pytest.mark.parametrize("dtype", DTYPES) @@ -127,6 +127,19 @@ def test_roc_auc_score_patching(caplog, dataframe, queue, dtype): ), f"sklearnex patching issue in roc_auc_score with log: \n{caplog.text}" +# (estimator, method) pairs whose special-instance config is not array API viable in +# stock sklearn itself: they raise a TypeError under array_api_dispatch, so the failure +# is a sklearn conformance gap rather than a sklearnex regression and is tolerated in +# test_special_estimator_patching_array_api. Any other failure there is treated as real. +_SKLEARN_ARRAY_API_GAPS = { + ("SVC", "predict_log_proba"), + ("NuSVC", "predict_log_proba"), + ("DummyRegressor", "predict"), + ("DummyRegressor", "score"), + ("LocalOutlierFactor", "predict"), +} + + def _check_estimator_patching(caplog, dataframe, queue, dtype, est, method): # This should be modified as more array_api frameworks are tested and for # upcoming changes in dpnp @@ -480,9 +493,45 @@ def _check_set_output_transform(est, method, X, estimator_name): @pytest.mark.parametrize("dtype", DTYPES) -@pytest.mark.parametrize("dataframe, queue", get_dataframes_and_queues()) +@pytest.mark.parametrize( + "dataframe, queue", get_dataframes_and_queues("numpy,pandas", "cpu") +) @pytest.mark.parametrize("estimator, method", gen_models_info(PATCHED_MODELS)) def test_standard_estimator_patching(caplog, dataframe, queue, dtype, estimator, method): + # numpy/pandas inputs run without array_api_dispatch; the array API frameworks + # are covered by test_standard_estimator_patching_array_api. + est = PATCHED_MODELS[estimator]() + + if "NearestNeighbors" in estimator and "radius" in method: + pytest.skip(f"RadiusNeighbors estimator not implemented in sklearnex") + + if estimator == "TSNE" and method == "fit_transform": + pytest.skip("TSNE.fit_transform is too slow for common testing") + elif estimator == "IncrementalLinearRegression" and np.issubdtype(dtype, np.integer): + pytest.skip( + "IncrementalLinearRegression fails on oneDAL side with int types because dataset is filled by zeroes" + ) + elif method and not hasattr(est, method) and not check_is_dynamic_method(est, method): + pytest.skip(f"sklearn available_if prevents testing {est}.{method}") + + # numpy/pandas outputs are not required to preserve the input container type + # (sklearnex returns numpy), so only patching and set_output are verified here. + result, X, y = _check_estimator_patching(caplog, dataframe, queue, dtype, est, method) + _check_set_output_transform(est, method, X, estimator) + + +@pytest.mark.skipif( + not _package_check_version("2.1", np.__version__), + reason="Array API functionality requires more recent version of NumPy.", +) +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("dataframe, queue", get_dataframes_and_queues("dpnp,array_api")) +@pytest.mark.parametrize("estimator, method", gen_models_info(PATCHED_MODELS)) +def test_standard_estimator_patching_array_api( + with_array_api, caplog, dataframe, queue, dtype, estimator, method +): + # array API inputs (dpnp, array_api_strict) run with array_api_dispatch enabled + # via the with_array_api fixture, so fit and all methods use array API natively. kwargs = {} if ( estimator == "LogisticRegression" @@ -516,118 +565,80 @@ def test_standard_estimator_patching(caplog, dataframe, queue, dtype, estimator, elif method and not hasattr(est, method) and not check_is_dynamic_method(est, method): pytest.skip(f"sklearn available_if prevents testing {est}.{method}") - if ( - (dataframe == "array_api" or queue) - and estimator == "LogisticRegressionCV" - and not get_tags(est).array_api_support - ): + if estimator == "LogisticRegressionCV" and not get_tags(est).array_api_support: pytest.skip("Array API and/or GPU inputs not supported in estimator") - if ( - ( - dataframe == "array_api" - or ( - dataframe in ["dpnp"] - and (not queue or not getattr(queue.sycl_device, "is_gpu", False)) - ) - ) - and estimator == "LogisticRegression" - and not sklearn_check_version("1.9") - ): - # In case array api inputs on CPU are provided on CPU for LogisticRegression sklearnex will fallback to sklearn - # Array API support for LogisticRegression in sklearn is only available starting from 1.9 + if estimator == "LogisticRegression" and not sklearn_check_version("1.9"): + # Array API support for LogisticRegression in sklearn is only available + # starting from 1.9; before that sklearnex falls back to sklearn. pytest.skip( - "Array API inputs on CPU are not supported for LogisticRegression in sklearn <1.9" + "Array API inputs are not supported for LogisticRegression in sklearn <1.9" ) - if dataframe == "array_api": - # as array_api dispatching is experimental, sklearn support isn't guaranteed. - # the infrastructure from sklearn that sklearnex depends on is also susceptible - # to failure. In this case compare to sklearn for the same failure. By design - # the patching of sklearn should act similarly. Technically this is conformance. - if not _package_check_version("2.0", np.__version__): - # numpy < 2.0 does not support keyword arguments in from_dlpack() - pytest.skip( - "numpy < 2.0 does not fully support the array API dlpack protocol." - ) - tags = get_tags(est) - array_api_check = ( - hasattr(tags, "array_api_support") and tags.array_api_support - ) or (hasattr(tags, "onedal_array_api") and tags.onedal_array_api) - if not array_api_check: - pytest.skip( - "Array API support not implemented in either scikit-learn or scikit-learn-intelex" - ) - - with config_context(array_api_dispatch=True): - try: - result, X, y = _check_estimator_patching( - caplog, dataframe, queue, dtype, est, method - ) - except Exception as e: - # if we are borrowing from sklearn and it fails, then this is something - # failing on sklearn-side. It is only allowed to fail if the underlying sklearn - # function doesn't support array_api with the set parameters and array_api - # support isn't promised by oneDAL - if estimator not in UNPATCHED_MODELS or getattr( - PATCHED_MODELS[estimator], method - ) != getattr(UNPATCHED_MODELS[estimator], method, None): - raise e - else: - # Check return type conformance when no exception - # occurred. Output arrays should match the input array type. - _check_output_type(result, y, method, estimator, caplog, X=X, est=est) - _check_fitted_attributes(est, X, estimator, caplog, queue=queue) - _check_set_output_transform(est, method, X, estimator) + tags = get_tags(est) + array_api_check = (hasattr(tags, "array_api_support") and tags.array_api_support) or ( + hasattr(tags, "onedal_array_api") and tags.onedal_array_api + ) + if not array_api_check: + pytest.skip( + "Array API support not implemented in either scikit-learn or scikit-learn-intelex" + ) - else: - if dataframe == "dpnp": - # Note: this tries to check for GPU support by checking for array API - # support. If some class can run on GPU but doesn't support array API, - # an exception should be made here. - tags = get_tags(est) - if not (hasattr(tags, "onedal_array_api") and tags.onedal_array_api): - pytest.skip("No GPU support for estimator") + try: result, X, y = _check_estimator_patching( caplog, dataframe, queue, dtype, est, method ) - # KNN/LOF store _fit_X as dpnp even without dispatch, so pickle - # fails (dpnp SYCL queues are not serializable) - if dataframe == "dpnp" and estimator not in [ - "KNeighborsClassifier", - "KNeighborsRegressor", - "NearestNeighbors", - "LocalOutlierFactor", - ]: - - pickle.loads(pickle.dumps(est)) - # Without array_api_dispatch, dpnp inputs go through - # support_input_format (converts to numpy and back) for fit and - # support_sycl_format (converts to numpy but NOT back) for some - # methods like score_samples/mahalanobis. This creates a type - # mismatch: fitted attrs may be numpy while method outputs are - # dpnp or vice versa. With array_api_dispatch enabled, all paths - # use array API consistently, so we re-fit and re-call with - # dispatch on to verify output types correctly. - if dataframe not in ("numpy", "pandas"): - # Skip second pass if estimator doesn't support GPU for this data - if queue is not None and getattr(queue.sycl_device, "is_gpu", False): - X_np, y_np = _as_numpy(X), _as_numpy(y) - if not est._onedal_gpu_supported("fit", X_np, y_np, None).get_status(): - _check_set_output_transform(est, method, X, estimator) - return - with config_context(array_api_dispatch=True): - result2, X2, y2 = _check_estimator_patching( - caplog, dataframe, queue, dtype, est, method - ) - _check_output_type(result2, y2, method, estimator, caplog, X=X2, est=est) - _check_fitted_attributes(est, X2, estimator, caplog, queue=queue) + except Exception as e: + # if we are borrowing from sklearn and it fails, then this is something + # failing on sklearn-side. It is only allowed to fail if the underlying sklearn + # function doesn't support array_api with the set parameters and array_api + # support isn't promised by oneDAL + if estimator not in UNPATCHED_MODELS or getattr( + PATCHED_MODELS[estimator], method + ) != getattr(UNPATCHED_MODELS[estimator], method, None): + raise e + else: + # Estimators without GPU fit support (e.g. neighbors) host-transfer and + # return numpy on GPU inputs, so output-type conformance does not apply. + if queue is not None and getattr(queue.sycl_device, "is_gpu", False): + X_np, y_np = _as_numpy(X), _as_numpy(y) + if not est._onedal_gpu_supported("fit", X_np, y_np, None).get_status(): + _check_set_output_transform(est, method, X, estimator) + return + # Check return type conformance when no exception occurred. Output arrays + # should match the input array type. + _check_output_type(result, y, method, estimator, caplog, X=X, est=est) + _check_fitted_attributes(est, X, estimator, caplog, queue=queue) _check_set_output_transform(est, method, X, estimator) @pytest.mark.parametrize("dtype", DTYPES) -@pytest.mark.parametrize("dataframe, queue", get_dataframes_and_queues()) +@pytest.mark.parametrize( + "dataframe, queue", get_dataframes_and_queues("numpy,pandas", "cpu") +) @pytest.mark.parametrize("estimator, method", gen_models_info(SPECIAL_INSTANCES)) def test_special_estimator_patching(caplog, dataframe, queue, dtype, estimator, method): + # numpy/pandas inputs run without array_api_dispatch; the array API frameworks + # are covered by test_special_estimator_patching_array_api. + est = SPECIAL_INSTANCES[estimator] + + if "NearestNeighbors" in estimator and "radius" in method: + pytest.skip(f"RadiusNeighbors estimator not implemented in sklearnex") + + _check_estimator_patching(caplog, dataframe, queue, dtype, est, method) + + +@pytest.mark.skipif( + not _package_check_version("2.1", np.__version__), + reason="Array API functionality requires more recent version of NumPy.", +) +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("dataframe, queue", get_dataframes_and_queues("dpnp,array_api")) +@pytest.mark.parametrize("estimator, method", gen_models_info(SPECIAL_INSTANCES)) +def test_special_estimator_patching_array_api( + with_array_api, caplog, dataframe, queue, dtype, estimator, method +): + # array API inputs (dpnp, array_api_strict) run with array_api_dispatch enabled + # via the with_array_api fixture. est = SPECIAL_INSTANCES[estimator] if queue: @@ -640,7 +651,15 @@ def test_special_estimator_patching(caplog, dataframe, queue, dtype, estimator, if "NearestNeighbors" in estimator and "radius" in method: pytest.skip(f"RadiusNeighbors estimator not implemented in sklearnex") - _check_estimator_patching(caplog, dataframe, queue, dtype, est, method) + try: + _check_estimator_patching(caplog, dataframe, queue, dtype, est, method) + except Exception as e: + # These special-instance methods are not array API viable in stock sklearn + # itself (they raise a TypeError under array_api_dispatch), so the failure is + # a sklearn conformance gap rather than a sklearnex regression. Any other + # failure is real and re-raised. + if (type(est).__name__, method) not in _SKLEARN_ARRAY_API_GAPS: + raise e @pytest.mark.parametrize("estimator", UNPATCHED_MODELS.keys()) diff --git a/sklearnex/utils/_array_api.py b/sklearnex/utils/_array_api.py index fadd16da33..967df3587b 100644 --- a/sklearnex/utils/_array_api.py +++ b/sklearnex/utils/_array_api.py @@ -22,77 +22,14 @@ import scipy.linalg as linalg from sklearn.covariance import log_likelihood as _sklearn_log_likelihood -from sklearn.utils._array_api import get_namespace as sklearn_get_namespace +from sklearn.utils._array_api import get_namespace from daal4py.sklearn._utils import sklearn_check_version -from onedal.utils._array_api import _get_sycl_namespace, _is_numpy_namespace +from onedal.utils._array_api import _is_numpy_namespace -from .._config import get_config from ..base import Tags, oneDALEstimator -def get_namespace(*arrays): - """Get namespace of arrays. - - Introspect `arrays` arguments and return their common Array API - compatible namespace object, if any. NumPy 1.22 and later can - construct such containers using the `numpy.array_api` namespace - for instance. - - This function will return the namespace of SYCL-related arrays - which define the __sycl_usm_array_interface__ attribute - regardless of array_api support, the configuration of - array_api_dispatch, or scikit-learn version. - - See: https://numpy.org/neps/nep-0047-array-api-standard.html - - If `arrays` are regular numpy arrays, an instance of the - `_NumPyApiWrapper` compatibility wrapper is returned instead. - - Namespace support is not enabled by default. To enabled it - call: - - sklearn.set_config(array_api_dispatch=True) - - or: - - with sklearn.config_context(array_api_dispatch=True): - # your code here - - Otherwise an instance of the `_NumPyApiWrapper` - compatibility wrapper is always returned irrespective of - the fact that arrays implement the `__array_namespace__` - protocol or not. - - Parameters - ---------- - *arrays : array objects - Array objects. - - Returns - ------- - namespace : module - Namespace shared by array objects. - - is_array_api : bool - True of the arrays are containers that implement the Array API spec. - """ - - # check required because _get_sycl_namespace only verifies that *arrays - # are of the same sycl namespace, not of the same array namespace. - # When array_api_dispatch is enabled, then sklearn's version is required - # for the additional array namespace check. This is now possible with - # dpnp as it supports `__array_namespace__`. - if not get_config().get("array_api_dispatch", False): - sycl_type, xp, is_array_api_compliant = _get_sycl_namespace(*arrays) - if sycl_type: - return xp, is_array_api_compliant - - # sklearn contains a specially patched numpy wrapper that should be - # reused which is yielded from sklearn's get_namespace. - return sklearn_get_namespace(*arrays) - - def _enable_array_api(original_class: type[oneDALEstimator]) -> type[oneDALEstimator]: def __sklearn_tags__(self) -> Tags: sktags = super(original_class, self).__sklearn_tags__() diff --git a/sklearnex/utils/class_weight.py b/sklearnex/utils/class_weight.py index 241665ae8a..4ff282707d 100644 --- a/sklearnex/utils/class_weight.py +++ b/sklearnex/utils/class_weight.py @@ -15,10 +15,10 @@ # ============================================================================== from sklearn.preprocessing import LabelEncoder as _sklearn_LabelEncoder +from sklearn.utils._array_api import get_namespace from daal4py.sklearn._utils import sklearn_check_version -from ._array_api import get_namespace from .validation import _check_sample_weight if sklearn_check_version("1.9"): diff --git a/sklearnex/utils/validation.py b/sklearnex/utils/validation.py index 5c967a9fa4..9b2cc782dd 100755 --- a/sklearnex/utils/validation.py +++ b/sklearnex/utils/validation.py @@ -19,6 +19,7 @@ from collections.abc import Sequence import scipy.sparse as sp +from sklearn.utils._array_api import get_namespace from sklearn.utils.validation import _assert_all_finite as _sklearn_assert_all_finite from sklearn.utils.validation import ( _num_samples, @@ -39,7 +40,6 @@ from sklearn.utils.validation import validate_data as _sklearn_validate_data from .._config import get_config as _get_config -from ._array_api import get_namespace if daal_check_version((2024, "P", 700)): from onedal.utils.validation import check_all_finite