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: