shap-cuda 0.54.1.dev0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shap_cuda-0.54.1.dev0/CMakeLists.txt +80 -0
- shap_cuda-0.54.1.dev0/PKG-INFO +130 -0
- shap_cuda-0.54.1.dev0/README.md +106 -0
- shap_cuda-0.54.1.dev0/_build_backend.py +38 -0
- shap_cuda-0.54.1.dev0/pyproject.toml +86 -0
- shap_cuda-0.54.1.dev0/shap_cuda/__init__.py +8 -0
- shap_cuda-0.54.1.dev0/shap_cuda/_gpu_tree.py +201 -0
- shap_cuda-0.54.1.dev0/shap_cuda/_version.py +24 -0
- shap_cuda-0.54.1.dev0/shap_cuda/cext/_cext_gpu.cc +192 -0
- shap_cuda-0.54.1.dev0/shap_cuda/cext/_cext_gpu.cu +432 -0
- shap_cuda-0.54.1.dev0/shap_cuda/cext/gpu_treeshap.h +1546 -0
- shap_cuda-0.54.1.dev0/shap_cuda/cext/tree_shap.h +1247 -0
- shap_cuda-0.54.1.dev0/tests/test_gpu_tree.py +380 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.24...3.27)
|
|
2
|
+
project(shap_cuda_extensions LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
if (NOT SKBUILD)
|
|
5
|
+
message(FATAL_ERROR "\
|
|
6
|
+
This CMakeLists.txt is meant to be used with scikit-build.
|
|
7
|
+
Please use 'python -m pip install .', not 'cmake'.")
|
|
8
|
+
endif()
|
|
9
|
+
|
|
10
|
+
if (CMAKE_VERSION VERSION_LESS 3.18)
|
|
11
|
+
set(DEV_MODULE Development)
|
|
12
|
+
else()
|
|
13
|
+
set(DEV_MODULE Development.Module)
|
|
14
|
+
endif()
|
|
15
|
+
|
|
16
|
+
find_package(Python 3.12
|
|
17
|
+
COMPONENTS Interpreter ${DEV_MODULE} REQUIRED
|
|
18
|
+
OPTIONAL_COMPONENTS Development.SABIModule)
|
|
19
|
+
|
|
20
|
+
if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
|
21
|
+
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE)
|
|
22
|
+
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
|
|
23
|
+
endif()
|
|
24
|
+
|
|
25
|
+
# Optional: CUDA may not be present (e.g. a docs build). In that case skip
|
|
26
|
+
# _cext_gpu and install the pure-Python package only - GPUTreeExplainer's own
|
|
27
|
+
# try/except around importing it degrades to a clear runtime error instead of
|
|
28
|
+
# making the whole install fail.
|
|
29
|
+
enable_language(CUDA OPTIONAL)
|
|
30
|
+
|
|
31
|
+
if(CMAKE_CUDA_COMPILER)
|
|
32
|
+
find_package(CUDAToolkit)
|
|
33
|
+
endif()
|
|
34
|
+
|
|
35
|
+
if(CMAKE_CUDA_COMPILER AND CUDAToolkit_FOUND)
|
|
36
|
+
message(STATUS "CUDA toolkit found, building the _cext_gpu extension.")
|
|
37
|
+
|
|
38
|
+
if(NOT "${SKBUILD_SABI_VERSION}" STREQUAL "")
|
|
39
|
+
set(USE_SABI USE_SABI ${SKBUILD_SABI_VERSION})
|
|
40
|
+
endif()
|
|
41
|
+
|
|
42
|
+
python_add_library(
|
|
43
|
+
_cext_gpu MODULE
|
|
44
|
+
|
|
45
|
+
${USE_SABI} WITH_SOABI
|
|
46
|
+
|
|
47
|
+
# The .cu file is compiled by nvcc, the .cc file by the host compiler
|
|
48
|
+
shap_cuda/cext/_cext_gpu.cu
|
|
49
|
+
shap_cuda/cext/_cext_gpu.cc
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
execute_process(
|
|
53
|
+
COMMAND "${PYTHON_EXECUTABLE}"
|
|
54
|
+
-c "import numpy; print(numpy.get_include())"
|
|
55
|
+
OUTPUT_VARIABLE NUMPY_INCLUDE_DIR
|
|
56
|
+
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
target_include_directories(_cext_gpu PUBLIC ${NUMPY_INCLUDE_DIR})
|
|
60
|
+
target_link_libraries(_cext_gpu PRIVATE CUDA::cudart)
|
|
61
|
+
|
|
62
|
+
set_target_properties(_cext_gpu PROPERTIES
|
|
63
|
+
CUDA_ARCHITECTURES "60;70;75;80"
|
|
64
|
+
CUDA_STANDARD 14
|
|
65
|
+
CUDA_EXTENSIONS ON
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
target_compile_options(_cext_gpu PRIVATE
|
|
69
|
+
$<$<COMPILE_LANGUAGE:CUDA>:
|
|
70
|
+
--expt-extended-lambda
|
|
71
|
+
--expt-relaxed-constexpr
|
|
72
|
+
>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
install(TARGETS _cext_gpu LIBRARY DESTINATION shap_cuda)
|
|
76
|
+
else()
|
|
77
|
+
message(STATUS "CUDA toolkit not found, skipping the _cext_gpu extension. "
|
|
78
|
+
"shap-cuda will install as Python-only; GPUTreeExplainer will "
|
|
79
|
+
"raise a clear error when actually used.")
|
|
80
|
+
endif()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: shap-cuda
|
|
3
|
+
Version: 0.54.1.dev0
|
|
4
|
+
Summary: CUDA-accelerated GPUTreeExplainer backend for shap.
|
|
5
|
+
Author-Email: Scott Lundberg <slund1@cs.washington.edu>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Classifier: Operating System :: POSIX
|
|
8
|
+
Classifier: Operating System :: Unix
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Intended Audience :: Information Technology
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Development Status :: 4 - Beta
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Project-URL: Repository, http://github.com/shap/shap
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Requires-Dist: shap
|
|
21
|
+
Requires-Dist: numpy<2.5,>=2
|
|
22
|
+
Requires-Dist: packaging>20.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# shap-cuda
|
|
26
|
+
|
|
27
|
+
CUDA-accelerated `GPUTreeExplainer` backend for [shap](https://github.com/shap/shap).
|
|
28
|
+
|
|
29
|
+
Installing this package registers `GPUTreeExplainer` with `shap` via the
|
|
30
|
+
`shap.tree_backends` entry-point group; use it through `shap.GPUTreeExplainer`,
|
|
31
|
+
not by importing `shap_cuda` directly.
|
|
32
|
+
|
|
33
|
+
## Build locally from source
|
|
34
|
+
|
|
35
|
+
Build both `shap` and `shap-cuda` from the same checkout: the backend uses SHAP
|
|
36
|
+
internals and checks that their release versions match. The commands below are
|
|
37
|
+
for Linux and run from the repository root, not this backend directory.
|
|
38
|
+
|
|
39
|
+
### Prerequisites
|
|
40
|
+
|
|
41
|
+
- Python 3.12 or newer; Python 3.13 is used in the examples below.
|
|
42
|
+
- A C++ compiler and Python development headers (on Ubuntu/Debian, install
|
|
43
|
+
`build-essential` and the development package for your Python version).
|
|
44
|
+
- The NVIDIA CUDA toolkit, including `nvcc`. CI builds with CUDA 12.8; the
|
|
45
|
+
backend currently targets GPU architectures `60`, `70`, `75`, and `80`, so
|
|
46
|
+
use a toolkit that supports those targets.
|
|
47
|
+
- An NVIDIA GPU and a compatible NVIDIA driver to run explanations. Building
|
|
48
|
+
the extension does not require a physical GPU.
|
|
49
|
+
|
|
50
|
+
Check that the toolkit and driver are available:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
nvcc --version
|
|
54
|
+
nvidia-smi
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the toolkit is installed outside your `PATH`, point the build at it. Adjust
|
|
58
|
+
this example to your installation:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export PATH=/usr/local/cuda-12.8/bin:$PATH
|
|
62
|
+
export CUDACXX=/usr/local/cuda-12.8/bin/nvcc
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The build backend installs CMake (3.24 or newer) and its other Python build
|
|
66
|
+
dependencies in an isolated environment as needed. The separate CUDA backend
|
|
67
|
+
does not require the old `SHAP_ENABLE_CUDA` environment variable.
|
|
68
|
+
|
|
69
|
+
### Install with uv
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uv venv --python 3.13
|
|
73
|
+
source .venv/bin/activate
|
|
74
|
+
uv sync --extra cuda
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The repository's uv workspace resolves the `cuda` extra to the local
|
|
78
|
+
`backends/shap-cuda` package and builds both packages as editable installs.
|
|
79
|
+
|
|
80
|
+
### Install with pip
|
|
81
|
+
|
|
82
|
+
Alternatively, create and activate a virtual environment, then explicitly
|
|
83
|
+
install both local packages:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python3.13 -m venv .venv
|
|
87
|
+
source .venv/bin/activate
|
|
88
|
+
python -m pip install --upgrade pip
|
|
89
|
+
python -m pip install -v -e . -e ./backends/shap-cuda
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Using `pip install -e '.[cuda]'` alone can resolve `shap-cuda` from a package
|
|
93
|
+
index instead of this checkout; pip does not use uv's workspace configuration.
|
|
94
|
+
|
|
95
|
+
### Verify and use the backend
|
|
96
|
+
|
|
97
|
+
Confirm that the compiled extension loads and that SHAP discovers the backend:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python -c "import shap; import shap_cuda._cext_gpu; print(shap.GPUTreeExplainer)"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Importing the extension directly here is a build check. For explanations, use
|
|
104
|
+
the public SHAP API:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import numpy as np
|
|
108
|
+
import shap
|
|
109
|
+
from sklearn.tree import DecisionTreeRegressor
|
|
110
|
+
|
|
111
|
+
X = np.array([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])
|
|
112
|
+
model = DecisionTreeRegressor(max_depth=2).fit(X, X[:, 0] + X[:, 1])
|
|
113
|
+
explainer = shap.GPUTreeExplainer(model)
|
|
114
|
+
values = explainer.shap_values(X)
|
|
115
|
+
print(values)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This example also checks that GPU execution works. Importing the package alone
|
|
119
|
+
does not prove that the CUDA extension was built. In verbose build output,
|
|
120
|
+
look for `CUDA toolkit found, building the _cext_gpu extension.` If the
|
|
121
|
+
extension is missing or the build cannot find `nvcc`, check `CUDACXX` and the
|
|
122
|
+
toolkit installation, then rebuild:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# uv
|
|
126
|
+
uv sync --extra cuda --reinstall-package shap-cuda
|
|
127
|
+
|
|
128
|
+
# Or pip; --no-deps keeps the existing local SHAP installation.
|
|
129
|
+
python -m pip install -v --no-cache-dir --force-reinstall --no-deps -e ./backends/shap-cuda
|
|
130
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# shap-cuda
|
|
2
|
+
|
|
3
|
+
CUDA-accelerated `GPUTreeExplainer` backend for [shap](https://github.com/shap/shap).
|
|
4
|
+
|
|
5
|
+
Installing this package registers `GPUTreeExplainer` with `shap` via the
|
|
6
|
+
`shap.tree_backends` entry-point group; use it through `shap.GPUTreeExplainer`,
|
|
7
|
+
not by importing `shap_cuda` directly.
|
|
8
|
+
|
|
9
|
+
## Build locally from source
|
|
10
|
+
|
|
11
|
+
Build both `shap` and `shap-cuda` from the same checkout: the backend uses SHAP
|
|
12
|
+
internals and checks that their release versions match. The commands below are
|
|
13
|
+
for Linux and run from the repository root, not this backend directory.
|
|
14
|
+
|
|
15
|
+
### Prerequisites
|
|
16
|
+
|
|
17
|
+
- Python 3.12 or newer; Python 3.13 is used in the examples below.
|
|
18
|
+
- A C++ compiler and Python development headers (on Ubuntu/Debian, install
|
|
19
|
+
`build-essential` and the development package for your Python version).
|
|
20
|
+
- The NVIDIA CUDA toolkit, including `nvcc`. CI builds with CUDA 12.8; the
|
|
21
|
+
backend currently targets GPU architectures `60`, `70`, `75`, and `80`, so
|
|
22
|
+
use a toolkit that supports those targets.
|
|
23
|
+
- An NVIDIA GPU and a compatible NVIDIA driver to run explanations. Building
|
|
24
|
+
the extension does not require a physical GPU.
|
|
25
|
+
|
|
26
|
+
Check that the toolkit and driver are available:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
nvcc --version
|
|
30
|
+
nvidia-smi
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If the toolkit is installed outside your `PATH`, point the build at it. Adjust
|
|
34
|
+
this example to your installation:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export PATH=/usr/local/cuda-12.8/bin:$PATH
|
|
38
|
+
export CUDACXX=/usr/local/cuda-12.8/bin/nvcc
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The build backend installs CMake (3.24 or newer) and its other Python build
|
|
42
|
+
dependencies in an isolated environment as needed. The separate CUDA backend
|
|
43
|
+
does not require the old `SHAP_ENABLE_CUDA` environment variable.
|
|
44
|
+
|
|
45
|
+
### Install with uv
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv venv --python 3.13
|
|
49
|
+
source .venv/bin/activate
|
|
50
|
+
uv sync --extra cuda
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The repository's uv workspace resolves the `cuda` extra to the local
|
|
54
|
+
`backends/shap-cuda` package and builds both packages as editable installs.
|
|
55
|
+
|
|
56
|
+
### Install with pip
|
|
57
|
+
|
|
58
|
+
Alternatively, create and activate a virtual environment, then explicitly
|
|
59
|
+
install both local packages:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python3.13 -m venv .venv
|
|
63
|
+
source .venv/bin/activate
|
|
64
|
+
python -m pip install --upgrade pip
|
|
65
|
+
python -m pip install -v -e . -e ./backends/shap-cuda
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Using `pip install -e '.[cuda]'` alone can resolve `shap-cuda` from a package
|
|
69
|
+
index instead of this checkout; pip does not use uv's workspace configuration.
|
|
70
|
+
|
|
71
|
+
### Verify and use the backend
|
|
72
|
+
|
|
73
|
+
Confirm that the compiled extension loads and that SHAP discovers the backend:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -c "import shap; import shap_cuda._cext_gpu; print(shap.GPUTreeExplainer)"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Importing the extension directly here is a build check. For explanations, use
|
|
80
|
+
the public SHAP API:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import numpy as np
|
|
84
|
+
import shap
|
|
85
|
+
from sklearn.tree import DecisionTreeRegressor
|
|
86
|
+
|
|
87
|
+
X = np.array([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])
|
|
88
|
+
model = DecisionTreeRegressor(max_depth=2).fit(X, X[:, 0] + X[:, 1])
|
|
89
|
+
explainer = shap.GPUTreeExplainer(model)
|
|
90
|
+
values = explainer.shap_values(X)
|
|
91
|
+
print(values)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
This example also checks that GPU execution works. Importing the package alone
|
|
95
|
+
does not prove that the CUDA extension was built. In verbose build output,
|
|
96
|
+
look for `CUDA toolkit found, building the _cext_gpu extension.` If the
|
|
97
|
+
extension is missing or the build cannot find `nvcc`, check `CUDACXX` and the
|
|
98
|
+
toolkit installation, then rebuild:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# uv
|
|
102
|
+
uv sync --extra cuda --reinstall-package shap-cuda
|
|
103
|
+
|
|
104
|
+
# Or pip; --no-deps keeps the existing local SHAP installation.
|
|
105
|
+
python -m pip install -v --no-cache-dir --force-reinstall --no-deps -e ./backends/shap-cuda
|
|
106
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""PEP 517 build backend wrapping scikit-build-core.
|
|
2
|
+
|
|
3
|
+
shap_cuda/cext/tree_shap.h is a symlink into the sibling shap/cext/ dir (the
|
|
4
|
+
CPU and GPU extensions compile against the same header). scikit-build-core's
|
|
5
|
+
sdist packaging preserves symlinks as-is, which produces a tarball that's
|
|
6
|
+
broken once extracted outside this monorepo. build_sdist here swaps the
|
|
7
|
+
symlink for its resolved file content before delegating, then restores it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from scikit_build_core import build as _skbuild
|
|
15
|
+
|
|
16
|
+
_SYMLINKED_HEADERS: list[Path] = [Path(__file__).parent / "shap_cuda" / "cext" / "tree_shap.h"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def __getattr__(name: str) -> Any:
|
|
20
|
+
# Every PEP 517 hook other than build_sdist delegates to scikit-build-core unmodified.
|
|
21
|
+
return getattr(_skbuild, name)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_sdist(sdist_directory: str, config_settings: dict[str, Any] | None = None) -> str:
|
|
25
|
+
materialized: list[tuple[Path, Path]] = []
|
|
26
|
+
try:
|
|
27
|
+
for header in _SYMLINKED_HEADERS:
|
|
28
|
+
if header.is_symlink():
|
|
29
|
+
resolved = header.resolve()
|
|
30
|
+
relative_target = Path("../../../../shap/cext") / header.name
|
|
31
|
+
header.unlink()
|
|
32
|
+
shutil.copyfile(resolved, header)
|
|
33
|
+
materialized.append((header, relative_target))
|
|
34
|
+
return _skbuild.build_sdist(sdist_directory, config_settings)
|
|
35
|
+
finally:
|
|
36
|
+
for header, relative_target in materialized:
|
|
37
|
+
header.unlink()
|
|
38
|
+
header.symlink_to(relative_target)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"scikit-build-core>=0.10.0",
|
|
4
|
+
"setuptools-scm>=8.0",
|
|
5
|
+
"numpy>=2.0",
|
|
6
|
+
"packaging>20.9",
|
|
7
|
+
]
|
|
8
|
+
build-backend = "_build_backend"
|
|
9
|
+
backend-path = ["."]
|
|
10
|
+
|
|
11
|
+
[project]
|
|
12
|
+
name = "shap-cuda"
|
|
13
|
+
description = "CUDA-accelerated GPUTreeExplainer backend for shap."
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
license = {text = "MIT License"}
|
|
16
|
+
authors = [
|
|
17
|
+
{name = "Scott Lundberg", email = "slund1@cs.washington.edu"},
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.12"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"shap",
|
|
22
|
+
"numpy>=2,<2.5",
|
|
23
|
+
"packaging>20.9",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Operating System :: POSIX",
|
|
27
|
+
"Operating System :: Unix",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Programming Language :: Python :: 3.14",
|
|
31
|
+
"Intended Audience :: Information Technology",
|
|
32
|
+
"Intended Audience :: Science/Research",
|
|
33
|
+
"Topic :: Scientific/Engineering",
|
|
34
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
35
|
+
"Development Status :: 4 - Beta",
|
|
36
|
+
"License :: OSI Approved :: MIT License",
|
|
37
|
+
]
|
|
38
|
+
dynamic = ["version"]
|
|
39
|
+
|
|
40
|
+
[project.entry-points."shap.tree_backends"]
|
|
41
|
+
cuda = "shap_cuda:GPUTreeExplainer"
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Repository = 'http://github.com/shap/shap'
|
|
45
|
+
|
|
46
|
+
[dependency-groups]
|
|
47
|
+
test = ["pytest", "pytest-mpl", "pytest-cov", "xgboost", "lightgbm", "matplotlib", "ipython"]
|
|
48
|
+
|
|
49
|
+
[tool.uv.sources]
|
|
50
|
+
shap = {workspace = true}
|
|
51
|
+
|
|
52
|
+
[tool.uv]
|
|
53
|
+
cache-keys = [
|
|
54
|
+
{ file = "shap_cuda/cext/*.cc" },
|
|
55
|
+
{ file = "shap_cuda/cext/*.cu" },
|
|
56
|
+
{ file = "shap_cuda/cext/*.h" },
|
|
57
|
+
{ file = "CMakeLists.txt" },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.setuptools_scm]
|
|
61
|
+
root = "../.."
|
|
62
|
+
version_file = "shap_cuda/_version.py"
|
|
63
|
+
local_scheme = "no-local-version"
|
|
64
|
+
|
|
65
|
+
[tool.scikit-build]
|
|
66
|
+
wheel.packages = ["shap_cuda"]
|
|
67
|
+
|
|
68
|
+
metadata.version.provider = "scikit_build_core.metadata.setuptools_scm"
|
|
69
|
+
sdist.include = ["shap_cuda/_version.py"]
|
|
70
|
+
|
|
71
|
+
minimum-version = "build-system.requires"
|
|
72
|
+
build-dir = "build/{wheel_tag}"
|
|
73
|
+
|
|
74
|
+
# Exclude C/C++/CUDA source files from the wheel (compiled .so is sufficient)
|
|
75
|
+
wheel.exclude = ["**/*.cc", "**/*.cpp", "**/*.h", "**/*.cu"]
|
|
76
|
+
|
|
77
|
+
wheel.py-api = "cp312"
|
|
78
|
+
|
|
79
|
+
[tool.ruff]
|
|
80
|
+
extend = "../../pyproject.toml"
|
|
81
|
+
|
|
82
|
+
[tool.pytest.ini_options]
|
|
83
|
+
addopts = "--mpl -m 'not xslow'"
|
|
84
|
+
mpl-default-style = "default"
|
|
85
|
+
testpaths = ["tests"]
|
|
86
|
+
markers = ["xslow: mark test as extremely slow (not run unless explicitly requested)"]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""GPU accelerated tree explanations"""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import shap
|
|
5
|
+
from packaging.version import parse as _parse_version
|
|
6
|
+
from shap.explainers._tree import (
|
|
7
|
+
TreeExplainer,
|
|
8
|
+
_xgboost_cat_unsupported,
|
|
9
|
+
feature_perturbation_codes,
|
|
10
|
+
output_transform_codes,
|
|
11
|
+
)
|
|
12
|
+
from shap.utils import assert_import, record_import_error
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from ._version import version as _shap_cuda_version
|
|
16
|
+
except ImportError:
|
|
17
|
+
_shap_cuda_version = "0.0.0-not-built"
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from . import _cext_gpu # type: ignore
|
|
21
|
+
except ImportError as e:
|
|
22
|
+
record_import_error("cext_gpu", "cuda extension was not built during install!", e)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _check_versions_match() -> None:
|
|
26
|
+
shap_release = _parse_version(shap.__version__).release
|
|
27
|
+
shap_cuda_release = _parse_version(_shap_cuda_version).release
|
|
28
|
+
if shap_release != shap_cuda_release:
|
|
29
|
+
raise RuntimeError(
|
|
30
|
+
f"shap=={shap.__version__} and shap-cuda=={_shap_cuda_version} are from different "
|
|
31
|
+
"releases and are not guaranteed to be compatible. Reinstall matching versions, "
|
|
32
|
+
f"e.g. `pip install shap=={shap.__version__} shap-cuda=={shap.__version__}`."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GPUTreeExplainer(TreeExplainer):
|
|
37
|
+
"""Experimental GPU accelerated version of TreeExplainer. Currently requires source build with
|
|
38
|
+
cuda available and 'CUDA_PATH' environment variable defined.
|
|
39
|
+
|
|
40
|
+
Examples
|
|
41
|
+
--------
|
|
42
|
+
See `GPUTree explainer examples <https://shap.readthedocs.io/en/latest/example_notebooks/api_examples/explainers/GPUTree.html>`_
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, *args, **kwargs):
|
|
47
|
+
_check_versions_match()
|
|
48
|
+
super().__init__(*args, **kwargs)
|
|
49
|
+
|
|
50
|
+
def shap_values(self, X, y=None, tree_limit=None, approximate=False, check_additivity=True, from_call=False):
|
|
51
|
+
"""Estimate the SHAP values for a set of samples.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
X : numpy.array, pandas.DataFrame or catboost.Pool (for catboost)
|
|
56
|
+
A matrix of samples (# samples x # features) on which to explain the model's output.
|
|
57
|
+
|
|
58
|
+
y : numpy.array
|
|
59
|
+
An array of label values for each sample. Used when explaining loss functions.
|
|
60
|
+
|
|
61
|
+
tree_limit : None (default) or int
|
|
62
|
+
Limit the number of trees used by the model. By default None means no use the limit
|
|
63
|
+
of the
|
|
64
|
+
original model, and -1 means no limit.
|
|
65
|
+
|
|
66
|
+
approximate : bool
|
|
67
|
+
Not supported.
|
|
68
|
+
|
|
69
|
+
check_additivity : bool
|
|
70
|
+
Run a validation check that the sum of the SHAP values equals the output of the
|
|
71
|
+
model. This
|
|
72
|
+
check takes only a small amount of time, and will catch potential unforeseen errors.
|
|
73
|
+
Note that this check only runs right now when explaining the margin of the model.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
array or list
|
|
78
|
+
For models with a single output this returns a matrix of SHAP values
|
|
79
|
+
(# samples x # features). Each row sums to the difference between the model output
|
|
80
|
+
for that
|
|
81
|
+
sample and the expected value of the model output (which is stored in the expected_value
|
|
82
|
+
attribute of the explainer when it is constant). For models with vector outputs this
|
|
83
|
+
returns
|
|
84
|
+
a list of such matrices, one for each output.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
assert not approximate, "approximate not supported"
|
|
88
|
+
|
|
89
|
+
X, y, X_missing, flat_output, tree_limit, check_additivity = self._validate_inputs(
|
|
90
|
+
X, y, tree_limit, check_additivity
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
model = self.model
|
|
94
|
+
_xgboost_cat_unsupported(model)
|
|
95
|
+
transform = model.get_transform()
|
|
96
|
+
|
|
97
|
+
# run the core algorithm using the C extension
|
|
98
|
+
assert_import("cext_gpu")
|
|
99
|
+
phi = np.zeros((X.shape[0], X.shape[1] + 1, self.model.num_outputs))
|
|
100
|
+
_cext_gpu.dense_tree_shap(
|
|
101
|
+
self.model.children_left,
|
|
102
|
+
self.model.children_right,
|
|
103
|
+
self.model.children_default,
|
|
104
|
+
self.model.features,
|
|
105
|
+
self.model.thresholds,
|
|
106
|
+
self.model.threshold_types,
|
|
107
|
+
self.model.values,
|
|
108
|
+
self.model.node_sample_weight,
|
|
109
|
+
self.model.max_depth,
|
|
110
|
+
X,
|
|
111
|
+
X_missing,
|
|
112
|
+
y,
|
|
113
|
+
self.data,
|
|
114
|
+
self.data_missing,
|
|
115
|
+
tree_limit,
|
|
116
|
+
self.model.base_offset,
|
|
117
|
+
phi,
|
|
118
|
+
feature_perturbation_codes[self.feature_perturbation],
|
|
119
|
+
output_transform_codes[transform],
|
|
120
|
+
False,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
out = self._get_shap_output(phi, flat_output)
|
|
124
|
+
if check_additivity and self.model.model_output == "raw":
|
|
125
|
+
self.assert_additivity(out, self.model.predict(X)) # type: ignore[arg-type]
|
|
126
|
+
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
def shap_interaction_values(self, X, y=None, tree_limit=None):
|
|
130
|
+
"""Estimate the SHAP interaction values for a set of samples.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
X : numpy.array, pandas.DataFrame or catboost.Pool (for catboost)
|
|
135
|
+
A matrix of samples (# samples x # features) on which to explain the model's output.
|
|
136
|
+
|
|
137
|
+
y : numpy.array
|
|
138
|
+
An array of label values for each sample. Used when explaining loss functions (not
|
|
139
|
+
yet supported).
|
|
140
|
+
|
|
141
|
+
tree_limit : None (default) or int
|
|
142
|
+
Limit the number of trees used by the model. By default None means no use the limit
|
|
143
|
+
of the
|
|
144
|
+
original model, and -1 means no limit.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
array or list
|
|
149
|
+
For models with a single output this returns a tensor of SHAP values
|
|
150
|
+
(# samples x # features x # features). The matrix (# features x # features) for each
|
|
151
|
+
sample sums
|
|
152
|
+
to the difference between the model output for that sample and the expected value of
|
|
153
|
+
the model output
|
|
154
|
+
(which is stored in the expected_value attribute of the explainer). Each row of this
|
|
155
|
+
matrix sums to the
|
|
156
|
+
SHAP value for that feature for that sample. The diagonal entries of the matrix
|
|
157
|
+
represent the
|
|
158
|
+
"main effect" of that feature on the prediction and the symmetric off-diagonal
|
|
159
|
+
entries represent the
|
|
160
|
+
interaction effects between all pairs of features for that sample. For models with
|
|
161
|
+
vector outputs
|
|
162
|
+
this returns a list of tensors, one for each output.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
if self.model.model_output != "raw":
|
|
166
|
+
raise ValueError('Only model_output = "raw" is supported for SHAP interaction values right now!')
|
|
167
|
+
if self.feature_perturbation == "interventional":
|
|
168
|
+
raise ValueError(
|
|
169
|
+
'feature_perturbation="interventional" is not yet supported for interaction values. '
|
|
170
|
+
'Use feature_perturbation="tree_path_dependent" instead.'
|
|
171
|
+
)
|
|
172
|
+
transform = "identity"
|
|
173
|
+
|
|
174
|
+
X, y, X_missing, flat_output, tree_limit, _ = self._validate_inputs(X, y, tree_limit, False)
|
|
175
|
+
# run the core algorithm using the C extension
|
|
176
|
+
assert_import("cext_gpu")
|
|
177
|
+
phi = np.zeros((X.shape[0], X.shape[1] + 1, X.shape[1] + 1, self.model.num_outputs))
|
|
178
|
+
_cext_gpu.dense_tree_shap(
|
|
179
|
+
self.model.children_left,
|
|
180
|
+
self.model.children_right,
|
|
181
|
+
self.model.children_default,
|
|
182
|
+
self.model.features,
|
|
183
|
+
self.model.thresholds,
|
|
184
|
+
self.model.threshold_types,
|
|
185
|
+
self.model.values,
|
|
186
|
+
self.model.node_sample_weight,
|
|
187
|
+
self.model.max_depth,
|
|
188
|
+
X,
|
|
189
|
+
X_missing,
|
|
190
|
+
y,
|
|
191
|
+
self.data,
|
|
192
|
+
self.data_missing,
|
|
193
|
+
tree_limit,
|
|
194
|
+
self.model.base_offset,
|
|
195
|
+
phi,
|
|
196
|
+
feature_perturbation_codes[self.feature_perturbation],
|
|
197
|
+
output_transform_codes[transform],
|
|
198
|
+
True,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return self._get_shap_interactions_output(phi, flat_output)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.54.1.dev0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 54, 1, 'dev0')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'g7d48b3044'
|