Skip to content

Commit f07b25f

Browse files
JhonatanFelixJhonatan Ramos Felixpre-commit-ci[bot]flying-sheep
authored
fix: njit fallback for broken runtime with torch (#164)
Co-authored-by: Jhonatan Ramos Felix <jhonatan@ip-82-146-123-104.reverse.destiny.be> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Philipp A. <flying-sheep@web.de>
1 parent d5176d6 commit f07b25f

4 files changed

Lines changed: 448 additions & 4 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ testpaths = [ "./tests", "fast_array_utils" ]
163163
doctest_subpackage_requires = [
164164
"src/fast_array_utils/conv/scipy/* = scipy",
165165
"src/fast_array_utils/conv/scipy/_to_dense.py = numba",
166+
"src/fast_array_utils/numba/* = numba",
166167
"src/fast_array_utils/stats/* = numba",
167168
"src/fast_array_utils/_plugins/dask.py = dask",
168169
"src/fast_array_utils/_plugins/numba_sparse.py = numba;scipy",
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,15 @@ def njit[**P, R](fn: Callable[P, R] | None = None, /) -> Callable[P, R] | Callab
9292
"""Jit-compile a function using numba.
9393
9494
On call, this function dispatches to a parallel or serial numba function,
95-
depending on if it has been called from a thread pool.
95+
depending on the current threading environment.
9696
"""
9797
# See https://github.com/numbagg/numbagg/pull/201/files#r1409374809
9898

9999
def decorator(f: Callable[P, R], /) -> Callable[P, R]:
100100
import numba
101101

102+
from ._parallel_runtime import _needs_parallel_runtime_probe, _parallel_numba_runtime_is_safe
103+
102104
assert isinstance(f, FunctionType)
103105

104106
# use distinct names so numba doesn’t reuse the wrong version’s cache
@@ -109,11 +111,17 @@ def decorator(f: Callable[P, R], /) -> Callable[P, R]:
109111

110112
@wraps(f)
111113
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
112-
parallel = not _is_in_unsafe_thread_pool()
113-
if not parallel: # pragma: no cover
114+
msg = None
115+
if _is_in_unsafe_thread_pool(): # pragma: no cover
114116
msg = f"Detected unsupported threading environment. Trying to run {f.__name__} in serial mode. In case of problems, install `tbb`."
117+
elif _needs_parallel_runtime_probe() and not _parallel_numba_runtime_is_safe():
118+
msg = (
119+
f"Detected an unsupported numba parallel runtime. Running {f.__name__} in serial mode as a workaround. "
120+
"Set `NUMBA_THREADING_LAYER=workqueue` or install `tbb` to avoid this fallback."
121+
)
122+
if not (run_parallel := msg is None):
115123
warnings.warn(msg, UserWarning, stacklevel=2)
116-
return fns[parallel](*args, **kwargs)
124+
return fns[run_parallel](*args, **kwargs)
117125

118126
return wrapper
119127

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
3+
from __future__ import annotations
4+
5+
import os
6+
import platform
7+
import subprocess
8+
import sys
9+
from functools import cache
10+
from typing import TYPE_CHECKING
11+
12+
import numba
13+
14+
from . import LAYERS
15+
16+
17+
if TYPE_CHECKING:
18+
from . import TheadingCategory, ThreadingLayer
19+
20+
21+
__all__ = ["_needs_parallel_runtime_probe", "_parallel_numba_runtime_is_safe"]
22+
23+
24+
type _ParallelRuntimeProbeKey = tuple[str, ThreadingLayer | TheadingCategory, tuple[ThreadingLayer, ...], tuple[str, ...]]
25+
26+
27+
_PARALLEL_RUNTIME_PROBE_SENTINEL = "FAST_ARRAY_UTILS_NUMBA_PROBE_OK"
28+
_PARALLEL_RUNTIME_PROBE_TIMEOUT = 20
29+
_PARALLEL_RUNTIME_PROBE_MODULE_WHITELIST = ("torch",)
30+
_PARALLEL_RUNTIME_PROBE_CODE = f"""
31+
import numba
32+
import numpy as np
33+
34+
@numba.njit(parallel=True, cache=False)
35+
def _probe(values):
36+
total = 0.0
37+
for i in numba.prange(values.shape[0]):
38+
total += values[i]
39+
return total
40+
41+
values = np.arange(32, dtype=np.float64)
42+
assert _probe(values) == np.sum(values)
43+
print({_PARALLEL_RUNTIME_PROBE_SENTINEL!r})
44+
"""
45+
46+
47+
def _is_apple_silicon() -> bool:
48+
return sys.platform == "darwin" and platform.machine() == "arm64"
49+
50+
51+
def _needs_parallel_runtime_probe() -> bool:
52+
if not _is_apple_silicon() or "torch" not in sys.modules:
53+
return False
54+
55+
match numba.config.THREADING_LAYER:
56+
case "omp":
57+
return True
58+
case "tbb" | "workqueue":
59+
return False
60+
case "default" | "safe" | "threadsafe" | "forksafe" as category:
61+
return "omp" in LAYERS[category]
62+
63+
64+
def _loaded_relevant_parallel_runtime_probe_modules() -> tuple[str, ...]:
65+
return tuple(module for module in _PARALLEL_RUNTIME_PROBE_MODULE_WHITELIST if module in sys.modules)
66+
67+
68+
def _parallel_runtime_probe_code(modules: tuple[str, ...]) -> str:
69+
return "\n".join(f"import {module}" for module in modules) + _PARALLEL_RUNTIME_PROBE_CODE
70+
71+
72+
def _parallel_runtime_probe_key() -> _ParallelRuntimeProbeKey:
73+
return (
74+
sys.executable,
75+
numba.config.THREADING_LAYER,
76+
tuple(numba.config.THREADING_LAYER_PRIORITY),
77+
_loaded_relevant_parallel_runtime_probe_modules(),
78+
)
79+
80+
81+
def _build_parallel_runtime_probe_env(key: _ParallelRuntimeProbeKey | None = None) -> dict[str, str]:
82+
_, layer_or_category, priority, _ = _parallel_runtime_probe_key() if key is None else key
83+
env = dict(os.environ)
84+
env["NUMBA_THREADING_LAYER"] = layer_or_category
85+
env["NUMBA_THREADING_LAYER_PRIORITY"] = " ".join(priority)
86+
return env
87+
88+
89+
@cache
90+
def _parallel_numba_runtime_is_safe_cached(key: _ParallelRuntimeProbeKey) -> bool:
91+
try:
92+
# The probe command is built from `sys.executable` plus a generated script
93+
# that only imports modules from a fixed whitelist.
94+
result = subprocess.run( # noqa: S603
95+
[key[0], "-c", _parallel_runtime_probe_code(key[3])],
96+
capture_output=True,
97+
check=False,
98+
env=_build_parallel_runtime_probe_env(key),
99+
text=True,
100+
timeout=_PARALLEL_RUNTIME_PROBE_TIMEOUT,
101+
)
102+
except Exception: # noqa: BLE001
103+
return False
104+
return result.returncode == 0 and _PARALLEL_RUNTIME_PROBE_SENTINEL in result.stdout
105+
106+
107+
def _parallel_numba_runtime_is_safe() -> bool:
108+
return _parallel_numba_runtime_is_safe_cached(_parallel_runtime_probe_key())

0 commit comments

Comments
 (0)