Skip to content

Commit ff6e773

Browse files
fix: improve error message when DPC++ backend (libonedal_dpc.so) fails to load (#2998)
* fix: improve error message when DPC++ backend fails to load When libonedal_dpc.so is missing (e.g. CPU-only oneDAL package split), the ImportError was silently swallowed and users received a cryptic internal message when attempting SYCL/queue-based inference: RuntimeError: Operations using queues require the DPC/SPMD backend This change: 1. Captures the ImportError reason in _dpc_load_error at module load time 2. Surfaces a user-actionable error message with the actual load failure and install instructions when a SYCL queue is used without DPC++ support Example new message: RuntimeError: oneDAL GPU/DPC++ support is not available in the current installation. Reason: libonedal_dpc.so.3: cannot open shared object file: ... To enable SYCL/GPU acceleration, install the GPU extras: pip install scikit-learn-intelex[gpu] or via conda: conda install -c https://software.repos.intel.com/python/conda scikit-learn-intelex * fix: wrap long string to satisfy flake8 E501 (max-line-length=90) * fix: use scikit-learn-intelex-gpu in conda install hint * docs: note Polars DataFrame support in supported input types * fix: refactor DPC++ load error into throw_if_no_dpc_available() Addresses david-cortes-intel review comments on #2998: - Add _spmd_load_error captured from SPMD backend ImportError - Replace _dpc_load_error inline check with throw_if_no_dpc_available(require_spmd) which handles both DPC++ and SPMD backends uniformly - Function raises actionable error with reason + install instructions only when the backend actually failed to load (avoids misleading message when file exists but import fails for other reasons like SYCL runtime) - Use require_spmd=self.backend.is_spmd so SPMD-only backends surface the correct error message * lint: fix docstring style for throw_if_no_dpc_available * fix: rename to _ensure_dpc_available, always raise when backend is None, remove dead raise - Rename throw_if_no_dpc_available -> _ensure_dpc_available (Pythonic convention) - Change gate from 'if error_msg' to 'if backend is None' so the function always raises with install instructions, even when the package was never installed (no ImportError captured). The Reason line is only included when an ImportError message is available. - Remove the dead 'raise RuntimeError("Operations using...")' fallthrough in _backend.py — _ensure_dpc_available unconditionally raises when backend is None - Remove private _dpc_load_error/_spmd_load_error from __all__; only expose the public helper function - Add Notes section to docstring documenting import-time capture semantics Addresses david-cortes-intel review comments. * revert: remove Polars line from input-types.rst (out of scope for this PR) * Update onedal/__init__.py Co-authored-by: david-cortes-intel <david.cortes@intel.com> * fix: only capture ImportError reason when .so file is present When the DPC++/SPMD backend package is simply not installed, ImportError gives 'No module named onedal._onedal_py_dpc' which is not actionable. Only populate _dpc_load_error/_spmd_load_error when the .so file actually exists in the package directory — meaning the library is present but failed to load (e.g. missing SYCL runtime, incompatible libsycl.so). Uses pathlib.glob() at module init time. Addresses david-cortes-intel review comment. * refactor: reuse backend binary presence check for Windows and load diagnostics --------- Co-authored-by: david-cortes-intel <david.cortes@intel.com>
1 parent 2dd961a commit ff6e773

2 files changed

Lines changed: 77 additions & 11 deletions

File tree

onedal/__init__.py

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# limitations under the License.
1616
# ==============================================================================
1717

18+
import pathlib as _pathlib
1819
import platform
1920

2021
from daal4py.sklearn._utils import daal_check_version
@@ -61,9 +62,13 @@ def __repr__(self) -> str:
6162
return f"Backend({self.backend}, is_dpc={self.is_dpc}, is_spmd={self.is_spmd})"
6263

6364

65+
def _backend_binary_present(prefix: str) -> bool:
66+
"""Return True if a backend extension binary with the given prefix exists."""
67+
return any(_pathlib.Path(__file__).parent.glob(f"{prefix}*"))
68+
69+
6470
if "Windows" in platform.system():
6571
import os
66-
import pathlib
6772
import site
6873
import sys
6974

@@ -78,12 +83,7 @@ def __repr__(self) -> str:
7883
if os.path.exists(dal_root_redist):
7984
os.add_dll_directory(dal_root_redist)
8085

81-
has_dpc_file = False
82-
for file_name in os.listdir(pathlib.Path(__file__).parent):
83-
if file_name.startswith("_onedal_py_dpc"):
84-
has_dpc_file = True
85-
break
86-
if has_dpc_file:
86+
if _backend_binary_present("_onedal_py_dpc"):
8787
for dep_root in ["CMPLR_ROOT", "MKLROOT"]:
8888
if dep_root in os.environ:
8989
dep_root_dir = os.path.join(os.environ[dep_root], "bin")
@@ -97,16 +97,30 @@ def __repr__(self) -> str:
9797
os.environ["PATH"] = path_to_libs + os.pathsep + os.environ["PATH"]
9898

9999

100+
# Preserved ImportError messages when DPC++/SPMD backends fail to load.
101+
# Used by _ensure_dpc_available() to surface actionable error messages.
102+
# Only populated when the backend .so file exists but fails to import
103+
# (e.g. missing SYCL runtime). Stays empty when the package is simply
104+
# not installed — in that case "No module named X" is not informative.
105+
_dpc_load_error: str = ""
106+
_spmd_load_error: str = ""
107+
108+
_dpc_file_present = _backend_binary_present("_onedal_py_dpc")
109+
_spmd_file_present = _backend_binary_present("_onedal_py_spmd_dpc")
110+
100111
try:
101112
# use dpc backend if available
102113
import onedal._onedal_py_dpc
103114

104115
_dpc_backend = Backend(onedal._onedal_py_dpc, is_dpc=True, is_spmd=False)
105116

106117
_host_backend = None
107-
except ImportError:
108-
# fall back to host backend
118+
except ImportError as _dpc_import_err:
119+
# fall back to host backend; preserve reason only when the .so exists
120+
# (file-not-found ImportError is not actionable for end users)
109121
_dpc_backend = None
122+
if _dpc_file_present:
123+
_dpc_load_error = str(_dpc_import_err)
110124

111125
import onedal._onedal_py_host
112126

@@ -117,8 +131,10 @@ def __repr__(self) -> str:
117131
import onedal._onedal_py_spmd_dpc
118132

119133
_spmd_backend = Backend(onedal._onedal_py_spmd_dpc, is_dpc=True, is_spmd=True)
120-
except ImportError:
134+
except ImportError as _spmd_import_err:
121135
_spmd_backend = None
136+
if _spmd_file_present:
137+
_spmd_load_error = str(_spmd_import_err)
122138

123139
# if/elif/else layout required for pylint to realize _default_backend cannot be None
124140
if _dpc_backend is not None:
@@ -128,8 +144,53 @@ def __repr__(self) -> str:
128144
else:
129145
raise ImportError("No oneDAL backend available")
130146

147+
148+
def _ensure_dpc_available(require_spmd: bool = False) -> None:
149+
"""Raise a user-actionable RuntimeError if the required DPC++/SPMD backend is unavailable.
150+
151+
This function should be called when a SYCL queue is present but the
152+
corresponding backend was not loaded. It always raises when the backend
153+
is ``None`` (whether due to a load error or because the package was never
154+
installed), and includes the original ``ImportError`` reason when available.
155+
156+
Parameters
157+
----------
158+
require_spmd : bool, default=False
159+
If True, check the SPMD backend; otherwise check the DPC++ backend.
160+
161+
Raises
162+
------
163+
RuntimeError
164+
Always raised when the requested backend is unavailable.
165+
Includes the original ImportError reason (if captured) and
166+
install instructions.
167+
168+
Notes
169+
-----
170+
Backend availability is determined at module import time. If the GPU
171+
package is installed after the interpreter has started, Python must be
172+
restarted for the change to take effect.
173+
"""
174+
backend = _spmd_backend if require_spmd else _dpc_backend
175+
if backend is not None:
176+
return # backend is available, nothing to do
177+
error_msg = _spmd_load_error if require_spmd else _dpc_load_error
178+
backend_label = "SPMD" if require_spmd else "DPC++"
179+
reason = f"\n Reason: {error_msg}" if error_msg else ""
180+
raise RuntimeError(
181+
f"oneDAL GPU/{backend_label} support is not available "
182+
f"in the current installation.{reason}\n"
183+
" To enable SYCL/GPU acceleration, install the GPU extras:\n"
184+
" pip install scikit-learn-intelex-gpu\n"
185+
" or via conda:\n"
186+
" conda install scikit-learn-intelex-gpu -c "
187+
"https://software.repos.intel.com/python/conda"
188+
)
189+
190+
131191
# Core modules to export
132192
__all__ = [
193+
"_ensure_dpc_available",
133194
"_host_backend",
134195
"_default_backend",
135196
"_dpc_backend",

onedal/common/_backend.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,12 @@ def __call__(self, *args: Any, **kwargs: Any) -> Any:
116116
queue = QM.get_global_queue()
117117

118118
if queue is not None and not (self.backend.is_dpc or self.backend.is_spmd):
119-
raise RuntimeError("Operations using queues require the DPC/SPMD backend")
119+
from onedal import _ensure_dpc_available
120+
121+
# Always raises with install instructions (and ImportError reason if
122+
# available). The old generic raise is removed — _ensure_dpc_available
123+
# unconditionally raises when the backend is None.
124+
_ensure_dpc_available(require_spmd=self.backend.is_spmd)
120125

121126
if self.backend.is_spmd and queue is None:
122127
raise RuntimeError("Executing functions from SPMD backend requires a queue")

0 commit comments

Comments
 (0)