diff --git a/README.md b/README.md
index 80c8ef57..4341c8f2 100755
--- a/README.md
+++ b/README.md
@@ -40,6 +40,12 @@ conda env create -n sklearn -f envs/conda-env-sklearn.yml
conda env create -n rapids --solver=libmamba -f envs/conda-env-rapids.yml
```
+GPU benchmarks using `sklearnex`'s `device: gpu` rely on scikit-learn's Array API support (`array_api_dispatch`), which requires `SCIPY_ARRAY_API=1` to be set in the environment before running:
+
+```bash
+export SCIPY_ARRAY_API=1
+```
+
## 🚀 How To Use Scikit-learn_bench
### Benchmarks Runner
diff --git a/configs/BENCH-CONFIG-SPEC.md b/configs/BENCH-CONFIG-SPEC.md
index c34f4743..7e23e548 100644
--- a/configs/BENCH-CONFIG-SPEC.md
+++ b/configs/BENCH-CONFIG-SPEC.md
@@ -98,7 +98,7 @@ Configs have the three highest parameter keys:
| `data`:`distributed_split` | None | None, `rank_based` | Split type used to distribute data between machines in distributed algorithm. `None` type means usage of all data without split on all machines. `rank_based` type splits the data equally between machines with split sequence based on rank id from MPI. |
|
Algorithm parameters
||||
| `algorithm`:`library` | None | | Python module containing measured entity (class or function). |
-| `algorithm`:`device` | `default` | `default`, `cpu`, `gpu` | Device selected for computation. |
+| `algorithm`:`device` | `default` | `default`, `cpu`, `gpu` | Device selected for computation. `sklearnex`+`gpu` cases enable sklearn's `array_api_dispatch` and use `dpnp` data by default (see `sklearn_context` below); requires `SCIPY_ARRAY_API=1` to be set in the environment. |
## Benchmark-Specific Parameters
diff --git a/configs/common/sklearn.json b/configs/common/sklearn.json
index 6b988ba7..0ac26ef8 100644
--- a/configs/common/sklearn.json
+++ b/configs/common/sklearn.json
@@ -6,12 +6,18 @@
{ "library": "sklearnex", "device": "cpu" }
]
},
- "sklearn-ex[cpu,gpu] implementations": {
- "algorithm": [
- { "library": "sklearn", "device": "cpu" },
- { "library": "sklearnex", "device": ["cpu", "gpu"] }
- ]
- },
+ "sklearn-ex[cpu,gpu] implementations": [
+ { "algorithm": { "library": "sklearn", "device": "cpu" } },
+ { "algorithm": { "library": "sklearnex", "device": "cpu" } },
+ {
+ "algorithm": {
+ "library": "sklearnex",
+ "device": "gpu",
+ "sklearn_context": { "array_api_dispatch": true }
+ },
+ "data": { "format": "dpnp" }
+ }
+ ],
"sklearnex spmd implementation": {
"algorithm": {
"library": "sklearnex.spmd",
diff --git a/sklbench/benchmarks/sklearn_estimator.py b/sklbench/benchmarks/sklearn_estimator.py
index 5b3032ea..ffb3174b 100644
--- a/sklbench/benchmarks/sklearn_estimator.py
+++ b/sklbench/benchmarks/sklearn_estimator.py
@@ -147,6 +147,10 @@ def get_subset_metrics_of_estimator(
and getattr(estimator_instance, "probability") == False
):
y_pred_proba = convert_to_numpy(estimator_instance.predict_proba(x))
+ # GPU/float32 predict_proba rows may drift outside roc_auc_score's
+ # sum-to-one tolerance for multiclass, so renormalize before scoring
+ if y_pred_proba.shape[1] > 2:
+ y_pred_proba = y_pred_proba / y_pred_proba.sum(axis=1, keepdims=True)
metrics.update(
{
"ROC AUC": float(
diff --git a/sklbench/datasets/transformer.py b/sklbench/datasets/transformer.py
index 1efc31e6..f3dfb375 100644
--- a/sklbench/datasets/transformer.py
+++ b/sklbench/datasets/transformer.py
@@ -26,7 +26,7 @@
from ..utils.logger import logger
-def convert_data(data, dformat: str, order: str, dtype: str, device: str = None):
+def convert_data(data, dformat: str, order: str, dtype: str, device: str = None, sycl_queue=None):
if isinstance(data, csr_matrix) and dformat != "csr_matrix":
data = data.toarray()
if dtype == "preserve":
@@ -46,6 +46,11 @@ def convert_data(data, dformat: str, order: str, dtype: str, device: str = None)
elif dformat == "dpnp":
import dpnp
+ # Pin every subset to one shared queue: sklearnex builds its internal
+ # arrays (e.g. the take() indices in KNN predict) on the device's default
+ # queue, and array_api_dispatch requires all arrays share one queue object.
+ if sycl_queue is not None:
+ return dpnp.asarray(data, dtype=dtype, order=order, sycl_queue=sycl_queue)
return dpnp.array(data, dtype=dtype, order=order, device=device)
elif dformat == "dpctl":
warnings.warn(
@@ -143,6 +148,14 @@ def split_and_transform_data(bench_case, data, data_description):
device = get_bench_case_value(bench_case, "algorithm:device", None)
common_data_format = get_bench_case_value(bench_case, "data:format", "pandas")
+
+ # Resolve one queue for the device up front so all dpnp subsets share it;
+ # dpnp.array(device=...) per subset can otherwise land on distinct queues.
+ sycl_queue = None
+ if common_data_format == "dpnp" and device is not None:
+ import dpnp
+
+ sycl_queue = dpnp.array([], device=device).sycl_queue
common_data_order = get_bench_case_value(bench_case, "data:order", "F")
common_data_dtype = get_bench_case_value(bench_case, "data:dtype", "float32")
@@ -177,7 +190,7 @@ def split_and_transform_data(bench_case, data, data_description):
data_dtype = required_label_dtype
converted_data = convert_data(
- subset_content, data_format, data_order, data_dtype, device
+ subset_content, data_format, data_order, data_dtype, device, sycl_queue
)
data_dict[subset_name] = converted_data
if not is_label: