cupmb 1.1.0__py3-none-any.whl
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.
- cupmb/__init__.py +46 -0
- cupmb/backend/__init__.py +87 -0
- cupmb/backend/cupy_backend.py +549 -0
- cupmb/backend/pycuda_backend.py +578 -0
- cupmb/bbox.py +75 -0
- cupmb/core.py +2389 -0
- cupmb/enums.py +44 -0
- cupmb/kernels/pmb.cuh +348 -0
- cupmb/kernels/pmb1D.cuh +262 -0
- cupmb/kernels/pmb2D.cuh +371 -0
- cupmb/kernels/pmb3D.cuh +476 -0
- cupmb/kernels/trove/aos.h +280 -0
- cupmb/kernels/trove/array.h +287 -0
- cupmb/kernels/trove/block.h +67 -0
- cupmb/kernels/trove/detail/dismember.h +127 -0
- cupmb/kernels/trove/detail/fallback.h +133 -0
- cupmb/kernels/trove/memory.h +170 -0
- cupmb/kernels/trove/print_array.h +48 -0
- cupmb/kernels/trove/ptr.h +80 -0
- cupmb/kernels/trove/rotate.h +117 -0
- cupmb/kernels/trove/shfl.h +106 -0
- cupmb/kernels/trove/static_gcd.h +77 -0
- cupmb/kernels/trove/static_mod_inverse.h +54 -0
- cupmb/kernels/trove/transpose.h +686 -0
- cupmb/kernels/trove/utility.h +162 -0
- cupmb/kernels/trove/warp.h +52 -0
- cupmb/parameters.py +31 -0
- cupmb/properties.py +136 -0
- cupmb/utils/IO.py +25 -0
- cupmb/utils/__init__.py +4 -0
- cupmb/utils/kernel_cache.py +129 -0
- cupmb/utils/logger.py +64 -0
- cupmb/utils/nvrtc_build.py +274 -0
- cupmb/utils/types.py +51 -0
- cupmb/utils/units.py +91 -0
- cupmb-1.1.0.dist-info/METADATA +226 -0
- cupmb-1.1.0.dist-info/RECORD +41 -0
- cupmb-1.1.0.dist-info/WHEEL +5 -0
- cupmb-1.1.0.dist-info/licenses/LICENSE +21 -0
- cupmb-1.1.0.dist-info/licenses/ext/trove/LICENSE +24 -0
- cupmb-1.1.0.dist-info/top_level.txt +1 -0
cupmb/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2021-2026 Jean-Baptiste Keck <jbkeck@hotmail.com>
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
__version__ = "1.1.0"
|
|
7
|
+
""":noindex: The version of this package as a string (single source of truth for packaging)."""
|
|
8
|
+
|
|
9
|
+
__version_info__ = tuple(__version__.split("."))
|
|
10
|
+
""":noindex: The version of this package as a tuple of strings."""
|
|
11
|
+
|
|
12
|
+
__author__ = "Jean-Baptiste Keck"
|
|
13
|
+
""":noindex: The author of this package."""
|
|
14
|
+
|
|
15
|
+
from cupmb.utils.logger import logger, log_once
|
|
16
|
+
|
|
17
|
+
log_once("info", f"Initializing {__name__} version {__version__}.", logger)
|
|
18
|
+
|
|
19
|
+
from cupmb.enums import (
|
|
20
|
+
Backend,
|
|
21
|
+
BufferType,
|
|
22
|
+
AllocationPolicy,
|
|
23
|
+
AutotuningRigor,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from cupmb.bbox import BoundingBox
|
|
27
|
+
from cupmb.parameters import ExtraParameter
|
|
28
|
+
from cupmb.properties import VertexProperty
|
|
29
|
+
|
|
30
|
+
from cupmb.core import (
|
|
31
|
+
ParallelMarchingBlocks,
|
|
32
|
+
)
|
|
33
|
+
from cupmb.utils.kernel_cache import kernel_cache_dir, clear_kernel_cache
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"Backend",
|
|
37
|
+
"BufferType",
|
|
38
|
+
"AllocationPolicy",
|
|
39
|
+
"AutotuningRigor",
|
|
40
|
+
"BoundingBox",
|
|
41
|
+
"ExtraParameter",
|
|
42
|
+
"VertexProperty",
|
|
43
|
+
"ParallelMarchingBlocks",
|
|
44
|
+
"kernel_cache_dir",
|
|
45
|
+
"clear_kernel_cache",
|
|
46
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2021-2026 Jean-Baptiste Keck <jbkeck@hotmail.com>
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from cupmb.enums import Backend
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def find_cuda_include_dirs(logger, extra_candidates=()):
|
|
11
|
+
"""
|
|
12
|
+
Determine the CUDA toolkit include directories (used to build kernels against the CUDA standard library).
|
|
13
|
+
|
|
14
|
+
The CUDA root is determined from the CUPMB_CUDA_ROOT environment variable if it is set. Otherwise, the
|
|
15
|
+
first existing directory among extra_candidates (backend specific), CUDA_PATH, CUDA_HOME and /usr/local/cuda
|
|
16
|
+
is used. For CUDA >= 13 toolkits, the include/cccl directory is returned as well when it exists.
|
|
17
|
+
"""
|
|
18
|
+
if "CUPMB_CUDA_ROOT" in os.environ:
|
|
19
|
+
cuda_root = os.environ["CUPMB_CUDA_ROOT"].strip()
|
|
20
|
+
if not os.path.isdir(cuda_root):
|
|
21
|
+
msg = f"CUPMB_CUDA_ROOT has been set to '{cuda_root}' but this directory does not exist."
|
|
22
|
+
logger.error(msg)
|
|
23
|
+
raise RuntimeError(msg)
|
|
24
|
+
else:
|
|
25
|
+
candidates = tuple(extra_candidates) + (
|
|
26
|
+
("CUDA_PATH", os.environ.get("CUDA_PATH")),
|
|
27
|
+
("CUDA_HOME", os.environ.get("CUDA_HOME")),
|
|
28
|
+
("default CUDA root", "/usr/local/cuda"),
|
|
29
|
+
)
|
|
30
|
+
cuda_root = None
|
|
31
|
+
for source, candidate in candidates:
|
|
32
|
+
if not candidate:
|
|
33
|
+
continue
|
|
34
|
+
candidate = candidate.strip()
|
|
35
|
+
if os.path.isdir(os.path.join(candidate, "include")):
|
|
36
|
+
logger.debug(f"Using CUDA root '{candidate}' from {source}.")
|
|
37
|
+
cuda_root = candidate
|
|
38
|
+
break
|
|
39
|
+
logger.debug(f"Ignoring CUDA root '{candidate}' from {source} (no include directory).")
|
|
40
|
+
if cuda_root is None:
|
|
41
|
+
msg = (
|
|
42
|
+
"Could not determine the CUDA toolkit root directory, please set the CUPMB_CUDA_ROOT "
|
|
43
|
+
"(or CUDA_PATH) environment variable to your CUDA toolkit root directory."
|
|
44
|
+
)
|
|
45
|
+
logger.error(msg)
|
|
46
|
+
raise RuntimeError(msg)
|
|
47
|
+
|
|
48
|
+
cuda_include_dir = os.path.join(cuda_root, "include")
|
|
49
|
+
if not os.path.isdir(cuda_include_dir):
|
|
50
|
+
msg = (
|
|
51
|
+
f"Could not find CUDA include directory '{cuda_include_dir}', please set the CUPMB_CUDA_ROOT "
|
|
52
|
+
"environment variable to your CUDA toolkit root directory."
|
|
53
|
+
)
|
|
54
|
+
logger.error(msg)
|
|
55
|
+
raise RuntimeError(msg)
|
|
56
|
+
|
|
57
|
+
include_dirs = [cuda_include_dir]
|
|
58
|
+
cccl_include_dir = os.path.join(cuda_include_dir, "cccl")
|
|
59
|
+
if os.path.isdir(cccl_include_dir):
|
|
60
|
+
# CUDA >= 13 toolkits ship libcu++/CUB/Thrust headers in include/cccl
|
|
61
|
+
include_dirs.append(cccl_include_dir)
|
|
62
|
+
|
|
63
|
+
return tuple(map(os.path.realpath, include_dirs))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CudaBackend:
|
|
67
|
+
def __new__(cls, backend, initialize_context=True):
|
|
68
|
+
if backend is Backend.CUPY:
|
|
69
|
+
from cupmb.backend.cupy_backend import CupyBackend
|
|
70
|
+
|
|
71
|
+
return CupyBackend
|
|
72
|
+
elif backend is Backend.PYCUDA:
|
|
73
|
+
from cupmb.backend.pycuda_backend import PyCudaBackend
|
|
74
|
+
|
|
75
|
+
if initialize_context:
|
|
76
|
+
import pycuda.autoinit # noqa: F401
|
|
77
|
+
return PyCudaBackend
|
|
78
|
+
else:
|
|
79
|
+
raise NotImplementedError(backend)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def determine_cuda_include_dirs(cls, logger):
|
|
83
|
+
return find_cuda_include_dirs(logger)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def determine_cuda_include_dir(cls, logger):
|
|
87
|
+
return cls.determine_cuda_include_dirs(logger)[0]
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2021-2026 Jean-Baptiste Keck <jbkeck@hotmail.com>
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
#
|
|
5
|
+
import tempfile, threading, functools
|
|
6
|
+
|
|
7
|
+
from cupmb.backend import CudaBackend, find_cuda_include_dirs
|
|
8
|
+
from cupmb.utils.logger import logger
|
|
9
|
+
|
|
10
|
+
logger.info("Importing CuPy backend.")
|
|
11
|
+
|
|
12
|
+
import cupy as cp
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
_compiler_lock = threading.Lock()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@functools.cache
|
|
19
|
+
def _cudart():
|
|
20
|
+
# The CUDA runtime python bindings are only required for OpenGL interoperability,
|
|
21
|
+
# import them lazily (cuda-bindings layout first, then legacy cuda-python layout).
|
|
22
|
+
try:
|
|
23
|
+
from cuda.bindings import runtime as cudart
|
|
24
|
+
except ImportError:
|
|
25
|
+
try:
|
|
26
|
+
from cuda import cudart
|
|
27
|
+
except ImportError as e:
|
|
28
|
+
msg = (
|
|
29
|
+
"CUDA OpenGL interoperability with the CuPy backend requires the cuda-python "
|
|
30
|
+
f"package (pip install cuda-python): {e}"
|
|
31
|
+
)
|
|
32
|
+
logger.error(msg)
|
|
33
|
+
raise ImportError(msg) from e
|
|
34
|
+
return cudart
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _extract_cuda_version(v):
|
|
38
|
+
return f"{v // 1000}.{(v % 1000) // 10}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
int_driver_version = cp.cuda.runtime.driverGetVersion()
|
|
42
|
+
int_cuda_version = cp.cuda.runtime.runtimeGetVersion()
|
|
43
|
+
driver_version = _extract_cuda_version(int_driver_version)
|
|
44
|
+
cuda_version = _extract_cuda_version(int_cuda_version)
|
|
45
|
+
|
|
46
|
+
if int_driver_version < 11000:
|
|
47
|
+
msg = (
|
|
48
|
+
"You are using an outdated NVIDIA driver, expected a CUDA >= 11.0 compatible driver but found version "
|
|
49
|
+
f"{driver_version} ({int_driver_version}). Please upgrade your NVidia driver to the latest stable "
|
|
50
|
+
"driver supported by your device at https://www.nvidia.fr/Download/index.aspx."
|
|
51
|
+
)
|
|
52
|
+
logger.warning(msg)
|
|
53
|
+
|
|
54
|
+
if int_cuda_version < 11000:
|
|
55
|
+
msg = (
|
|
56
|
+
"You are using an outdated CUDA version, expected at least CUDA 11.0 but found version "
|
|
57
|
+
f"{cuda_version} ({int_cuda_version}). Please download and install lastest CUDA release from "
|
|
58
|
+
"https://developer.nvidia.com/cuda-downloads and upgrade cupy."
|
|
59
|
+
)
|
|
60
|
+
logger.warning(msg)
|
|
61
|
+
|
|
62
|
+
msg = (
|
|
63
|
+
f"Loaded cuda runtime {cuda_version} ({int_cuda_version}), "
|
|
64
|
+
f"driver version is {driver_version} ({int_driver_version})."
|
|
65
|
+
)
|
|
66
|
+
logger.info(msg)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def format_cudart_err(err):
|
|
70
|
+
cudart = _cudart()
|
|
71
|
+
return (
|
|
72
|
+
f"{cudart.cudaGetErrorName(err)[1].decode('utf-8')}({int(err)}): "
|
|
73
|
+
f"{cudart.cudaGetErrorString(err)[1].decode('utf-8')}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check_cudart_err(args):
|
|
78
|
+
if isinstance(args, tuple):
|
|
79
|
+
assert len(args) >= 1
|
|
80
|
+
err = args[0]
|
|
81
|
+
if len(args) == 1:
|
|
82
|
+
ret = None
|
|
83
|
+
elif len(args) == 2:
|
|
84
|
+
ret = args[1]
|
|
85
|
+
else:
|
|
86
|
+
ret = args[1:]
|
|
87
|
+
else:
|
|
88
|
+
ret = None
|
|
89
|
+
|
|
90
|
+
cudart = _cudart()
|
|
91
|
+
assert isinstance(err, cudart.cudaError_t), type(err)
|
|
92
|
+
if err != cudart.cudaError_t.cudaSuccess:
|
|
93
|
+
raise RuntimeError(format_cudart_err(err))
|
|
94
|
+
|
|
95
|
+
return ret
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CupyDevice:
|
|
99
|
+
"""
|
|
100
|
+
CuPy device wrapper. Instances are cached per device id because querying device
|
|
101
|
+
properties and attributes is expensive (this used to dominate host overhead).
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
_instances = {}
|
|
105
|
+
_instances_lock = threading.Lock()
|
|
106
|
+
|
|
107
|
+
def __new__(cls, device_id):
|
|
108
|
+
device_id = int(device_id)
|
|
109
|
+
with cls._instances_lock:
|
|
110
|
+
instance = cls._instances.get(device_id)
|
|
111
|
+
if instance is None:
|
|
112
|
+
instance = super().__new__(cls)
|
|
113
|
+
instance._initialize(device_id)
|
|
114
|
+
cls._instances[device_id] = instance
|
|
115
|
+
return instance
|
|
116
|
+
|
|
117
|
+
def _initialize(self, device_id):
|
|
118
|
+
self._cupy_device = cp.cuda.Device(device_id)
|
|
119
|
+
self._device_props = cp.cuda.runtime.getDeviceProperties(device_id)
|
|
120
|
+
self._device_attrs = self._cupy_device.attributes
|
|
121
|
+
self._pci_bus_id = self._cupy_device.pci_bus_id
|
|
122
|
+
self._tls = threading.local()
|
|
123
|
+
|
|
124
|
+
def __enter__(self):
|
|
125
|
+
# thread-safe and reentrant equivalent of cupy.cuda.Device.__enter__
|
|
126
|
+
previous_device_id = cp.cuda.runtime.getDevice()
|
|
127
|
+
self._tls.__dict__.setdefault("stack", []).append(previous_device_id)
|
|
128
|
+
if previous_device_id != self.id:
|
|
129
|
+
cp.cuda.runtime.setDevice(self.id)
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
133
|
+
previous_device_id = self._tls.stack.pop()
|
|
134
|
+
if previous_device_id != self.id:
|
|
135
|
+
cp.cuda.runtime.setDevice(previous_device_id)
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def handle(self):
|
|
139
|
+
return self._cupy_device
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def id(self):
|
|
143
|
+
return self._cupy_device.id
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def max_grid_dim(self):
|
|
147
|
+
return (self.max_grid_dim_x, self.max_grid_dim_y, self.max_grid_dim_z)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def max_block_dim(self):
|
|
151
|
+
return (self.max_block_dim_x, self.max_block_dim_y, self.max_block_dim_z)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def max_threads_per_multiprocessor(self):
|
|
155
|
+
return self._device_props["maxThreadsPerMultiProcessor"]
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def max_blocks_per_multiprocessor(self):
|
|
159
|
+
return self._device_props["maxBlocksPerMultiProcessor"]
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def multiprocessor_count(self):
|
|
163
|
+
return self._device_props["multiProcessorCount"]
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def major(self):
|
|
167
|
+
return self._device_props["major"]
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def minor(self):
|
|
171
|
+
return self._device_props["minor"]
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def name(self):
|
|
175
|
+
return self._device_props["name"].decode("utf-8")
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def pci_bus_id(self):
|
|
179
|
+
return self._pci_bus_id
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def warp_size(self):
|
|
183
|
+
return self._device_attrs["WarpSize"]
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def max_threads_per_block(self):
|
|
187
|
+
return self._device_attrs["MaxThreadsPerBlock"]
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def max_block_dim_x(self):
|
|
191
|
+
return self._device_attrs["MaxBlockDimX"]
|
|
192
|
+
|
|
193
|
+
@property
|
|
194
|
+
def max_block_dim_y(self):
|
|
195
|
+
return self._device_attrs["MaxBlockDimY"]
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def max_block_dim_z(self):
|
|
199
|
+
return self._device_attrs["MaxBlockDimZ"]
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def max_grid_dim_x(self):
|
|
203
|
+
return self._device_attrs["MaxGridDimX"]
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def max_grid_dim_y(self):
|
|
207
|
+
return self._device_attrs["MaxGridDimY"]
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def max_grid_dim_z(self):
|
|
211
|
+
return self._device_attrs["MaxGridDimZ"]
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def max_shared_memory_per_block(self):
|
|
215
|
+
return self._device_attrs["MaxSharedMemoryPerBlock"]
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def max_shared_memory_per_block_optin(self):
|
|
219
|
+
return self._device_attrs["MaxSharedMemoryPerBlockOptin"]
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def free_memory_bytes(self):
|
|
223
|
+
return self._cupy_device.mem_info[0]
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def global_memory_bytes(self):
|
|
227
|
+
return self._device_props["totalGlobalMem"]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class CupyKernel:
|
|
231
|
+
def __init__(self, handle):
|
|
232
|
+
assert isinstance(handle, cp.RawKernel), type(handle)
|
|
233
|
+
self._cupy_kernel = handle
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def handle(self):
|
|
237
|
+
return self._cupy_kernel
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def max_threads_per_block(self):
|
|
241
|
+
return self._cupy_kernel.max_threads_per_block
|
|
242
|
+
|
|
243
|
+
def __call__(self, grid, block, args, stream, shared_mem=None, time_kernel=False):
|
|
244
|
+
shared_mem = shared_mem if (shared_mem is not None) else 0
|
|
245
|
+
if time_kernel:
|
|
246
|
+
start = stream.record()
|
|
247
|
+
with stream:
|
|
248
|
+
self._cupy_kernel(grid, block, args, shared_mem=shared_mem)
|
|
249
|
+
end = stream.record()
|
|
250
|
+
stream.synchronize()
|
|
251
|
+
return 1e-3 * cp.cuda.get_elapsed_time(start, end)
|
|
252
|
+
else:
|
|
253
|
+
with stream:
|
|
254
|
+
self._cupy_kernel(grid, block, args, shared_mem=shared_mem)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class CudaOpenGLMappedBuffer:
|
|
258
|
+
def __init__(self, gl_buffer, flags=0):
|
|
259
|
+
self._gl_buffer = int(gl_buffer)
|
|
260
|
+
self._flags = int(flags)
|
|
261
|
+
|
|
262
|
+
self._graphics_ressource = None
|
|
263
|
+
self._cuda_buffer = None
|
|
264
|
+
|
|
265
|
+
self.register()
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def gl_buffer(self):
|
|
269
|
+
return self._gl_buffer
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def cuda_buffer(self):
|
|
273
|
+
assert self.mapped
|
|
274
|
+
return self._cuda_buffer
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def graphics_ressource(self):
|
|
278
|
+
assert self.registered
|
|
279
|
+
return self._graphics_ressource
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def registered(self):
|
|
283
|
+
return self._graphics_ressource is not None
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def mapped(self):
|
|
287
|
+
return self._cuda_buffer is not None
|
|
288
|
+
|
|
289
|
+
def __enter__(self):
|
|
290
|
+
return self.map()
|
|
291
|
+
|
|
292
|
+
def __exit__(self, exc_type, exc_value, trace):
|
|
293
|
+
self.unmap()
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
def __del__(self):
|
|
297
|
+
self.unregister()
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _stream_ptr(stream):
|
|
301
|
+
return 0 if (stream is None) else stream.ptr
|
|
302
|
+
|
|
303
|
+
def register(self):
|
|
304
|
+
if not self.registered:
|
|
305
|
+
self._graphics_ressource = check_cudart_err(
|
|
306
|
+
_cudart().cudaGraphicsGLRegisterBuffer(self._gl_buffer, self._flags)
|
|
307
|
+
)
|
|
308
|
+
return self._graphics_ressource
|
|
309
|
+
|
|
310
|
+
def unregister(self):
|
|
311
|
+
if self.registered:
|
|
312
|
+
self.unmap()
|
|
313
|
+
self._graphics_ressource = check_cudart_err(
|
|
314
|
+
_cudart().cudaGraphicsUnregisterResource(self._graphics_ressource)
|
|
315
|
+
)
|
|
316
|
+
return self
|
|
317
|
+
|
|
318
|
+
def map(self, stream=None):
|
|
319
|
+
if not self.registered:
|
|
320
|
+
raise RuntimeError("Cannot map an unregistered buffer.")
|
|
321
|
+
|
|
322
|
+
if not self.mapped:
|
|
323
|
+
cudart = _cudart()
|
|
324
|
+
check_cudart_err(cudart.cudaGraphicsMapResources(1, self._graphics_ressource, self._stream_ptr(stream)))
|
|
325
|
+
|
|
326
|
+
ptr, size = check_cudart_err(cudart.cudaGraphicsResourceGetMappedPointer(self._graphics_ressource))
|
|
327
|
+
|
|
328
|
+
self._cuda_buffer = cp.cuda.MemoryPointer(cp.cuda.UnownedMemory(ptr, size, self), 0)
|
|
329
|
+
|
|
330
|
+
return self._cuda_buffer
|
|
331
|
+
|
|
332
|
+
def unmap(self, stream=None):
|
|
333
|
+
if not self.registered:
|
|
334
|
+
raise RuntimeError("Cannot unmap an unregistered buffer.")
|
|
335
|
+
|
|
336
|
+
if self.mapped:
|
|
337
|
+
self._cuda_buffer = check_cudart_err(
|
|
338
|
+
_cudart().cudaGraphicsUnmapResources(1, self._graphics_ressource, self._stream_ptr(stream))
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return self
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class CudaOpenGLMappedArray(CudaOpenGLMappedBuffer):
|
|
345
|
+
def __init__(self, dtype, shape, gl_buffer, flags=0, strides=None, order="C"):
|
|
346
|
+
super().__init__(gl_buffer, flags)
|
|
347
|
+
self._dtype = dtype
|
|
348
|
+
self._shape = shape
|
|
349
|
+
self._strides = strides
|
|
350
|
+
self._order = order
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def cuda_array(self):
|
|
354
|
+
assert self.mapped
|
|
355
|
+
return cp.ndarray(
|
|
356
|
+
shape=self._shape,
|
|
357
|
+
dtype=self._dtype,
|
|
358
|
+
strides=self._strides,
|
|
359
|
+
order=self._order,
|
|
360
|
+
memptr=self._cuda_buffer,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def map(self, *args, **kwargs):
|
|
364
|
+
super().map(*args, **kwargs)
|
|
365
|
+
return self.cuda_array
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class CupyBackend(CudaBackend):
|
|
369
|
+
ArrayBackend = cp
|
|
370
|
+
Array = cp.ndarray
|
|
371
|
+
Buffer = cp.cuda.memory.MemoryPointer
|
|
372
|
+
Stream = cp.cuda.Stream
|
|
373
|
+
Device = CupyDevice
|
|
374
|
+
Kernel = CupyKernel
|
|
375
|
+
AllocationError = cp.cuda.memory.OutOfMemoryError
|
|
376
|
+
|
|
377
|
+
CudaOpenGLMappedArray = CudaOpenGLMappedArray
|
|
378
|
+
CudaOpenGLMappedBuffer = CudaOpenGLMappedBuffer
|
|
379
|
+
|
|
380
|
+
NullMem = cp.cuda.UnownedMemory(ptr=0x0, size=0, owner=None, device_id=0)
|
|
381
|
+
NullPtr = cp.cuda.memory.MemoryPointer(NullMem, 0)
|
|
382
|
+
|
|
383
|
+
@classmethod
|
|
384
|
+
def default_stream(cls):
|
|
385
|
+
# honor 'with stream:' blocks of the caller
|
|
386
|
+
return cp.cuda.get_current_stream()
|
|
387
|
+
|
|
388
|
+
@classmethod
|
|
389
|
+
def asnumpy(cls, a, stream=None):
|
|
390
|
+
# copy is performed on (and synchronized with) the given stream
|
|
391
|
+
return cp.asnumpy(a, stream=stream, blocking=True)
|
|
392
|
+
|
|
393
|
+
@classmethod
|
|
394
|
+
def asarray(cls, a):
|
|
395
|
+
return cp.asarray(a)
|
|
396
|
+
|
|
397
|
+
@classmethod
|
|
398
|
+
def get_nbytes(cls, a):
|
|
399
|
+
if a is None:
|
|
400
|
+
return 0
|
|
401
|
+
elif isinstance(a, cls.Array):
|
|
402
|
+
return a.nbytes
|
|
403
|
+
elif isinstance(a, cls.Buffer):
|
|
404
|
+
return a.mem.size
|
|
405
|
+
else:
|
|
406
|
+
raise NotImplementedError(type(a))
|
|
407
|
+
|
|
408
|
+
@classmethod
|
|
409
|
+
def synchronize_stream(cls, stream):
|
|
410
|
+
stream.synchronize()
|
|
411
|
+
|
|
412
|
+
@classmethod
|
|
413
|
+
def get_data_pointers(cls, *args):
|
|
414
|
+
if len(args) == 1 and isinstance(args[0], tuple):
|
|
415
|
+
args = args[0]
|
|
416
|
+
return tuple(cls.NullPtr if (a is None) else a for a in args)
|
|
417
|
+
|
|
418
|
+
@classmethod
|
|
419
|
+
def array_from_pointer(cls, ptr, shape, dtype):
|
|
420
|
+
assert isinstance(ptr, cls.Buffer)
|
|
421
|
+
req_bytes = int(np.prod(shape, dtype=np.int64)) * np.dtype(dtype).itemsize
|
|
422
|
+
ptr_bytes = cls.get_nbytes(ptr)
|
|
423
|
+
if req_bytes > ptr_bytes:
|
|
424
|
+
msg = (
|
|
425
|
+
f"Buffer of {ptr_bytes} bytes is too small to hold an array of shape {shape} "
|
|
426
|
+
f"and dtype {np.dtype(dtype)}."
|
|
427
|
+
)
|
|
428
|
+
raise ValueError(msg)
|
|
429
|
+
return cls.Array(shape=shape, dtype=dtype, memptr=ptr)
|
|
430
|
+
|
|
431
|
+
@classmethod
|
|
432
|
+
def fill_zeros(cls, a, stream):
|
|
433
|
+
assert isinstance(a, cls.Array), type(a)
|
|
434
|
+
if a.nbytes == 0:
|
|
435
|
+
return
|
|
436
|
+
cp.cuda.runtime.memsetAsync(
|
|
437
|
+
a.data.ptr,
|
|
438
|
+
0,
|
|
439
|
+
a.nbytes,
|
|
440
|
+
stream.ptr,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
@classmethod
|
|
444
|
+
def determine_cuda_include_dirs(cls, logger):
|
|
445
|
+
# cupy.cuda.get_cuda_path() may return None when no CUDA toolkit could be found
|
|
446
|
+
return find_cuda_include_dirs(logger, extra_candidates=(("CuPy", cp.cuda.get_cuda_path()),))
|
|
447
|
+
|
|
448
|
+
@classmethod
|
|
449
|
+
@functools.cache
|
|
450
|
+
def compiler_version(cls):
|
|
451
|
+
major, minor = cp.cuda.nvrtc.getVersion()
|
|
452
|
+
return f"cupy-{cp.__version__}-nvrtc-{major}.{minor}"
|
|
453
|
+
|
|
454
|
+
@classmethod
|
|
455
|
+
def build_module_and_extract_kernels_from_source(
|
|
456
|
+
cls,
|
|
457
|
+
logger,
|
|
458
|
+
device_major,
|
|
459
|
+
device_minor,
|
|
460
|
+
source_code,
|
|
461
|
+
kernel_names,
|
|
462
|
+
build_options,
|
|
463
|
+
):
|
|
464
|
+
kernels, _, _ = cls.build_kernels(logger, device_major, device_minor, source_code, kernel_names, build_options)
|
|
465
|
+
return kernels
|
|
466
|
+
|
|
467
|
+
@classmethod
|
|
468
|
+
def build_kernels(
|
|
469
|
+
cls,
|
|
470
|
+
logger,
|
|
471
|
+
device_major,
|
|
472
|
+
device_minor,
|
|
473
|
+
source_code,
|
|
474
|
+
kernel_names,
|
|
475
|
+
build_options,
|
|
476
|
+
):
|
|
477
|
+
"""
|
|
478
|
+
Build the kernels, return the kernels, the compiled module (cubin or ptx) and the mangled kernel names.
|
|
479
|
+
The compiled module is None when it could not be retrieved from CuPy (it is then not cached).
|
|
480
|
+
"""
|
|
481
|
+
|
|
482
|
+
def debug_dump_kernel():
|
|
483
|
+
with tempfile.NamedTemporaryFile(mode="w", prefix="cupmb_kernel_", suffix=".cu", delete=False) as f:
|
|
484
|
+
f.write(source_code)
|
|
485
|
+
debug_dump_file = f.name
|
|
486
|
+
debug_msg = f"Build failure, kernel source code has been dumped to file://{debug_dump_file}."
|
|
487
|
+
logger.error(debug_msg)
|
|
488
|
+
|
|
489
|
+
# CuPy does not use its on disk cache when name expressions are used, and it does not expose the
|
|
490
|
+
# compiled module: intercept the NVRTC compilation to retrieve it while keeping the exact same build
|
|
491
|
+
# options (CuPy adds its own include directories and -ftz=true).
|
|
492
|
+
compiler = cp.cuda.compiler
|
|
493
|
+
compile_using_nvrtc = getattr(compiler, "compile_using_nvrtc", None)
|
|
494
|
+
compiled = []
|
|
495
|
+
|
|
496
|
+
def capture_compile_using_nvrtc(*args, **kwds):
|
|
497
|
+
result = compile_using_nvrtc(*args, **kwds)
|
|
498
|
+
compiled.append(result)
|
|
499
|
+
return result
|
|
500
|
+
|
|
501
|
+
with _compiler_lock:
|
|
502
|
+
if compile_using_nvrtc is not None:
|
|
503
|
+
compiler.compile_using_nvrtc = capture_compile_using_nvrtc
|
|
504
|
+
try:
|
|
505
|
+
try:
|
|
506
|
+
module = cp.RawModule(
|
|
507
|
+
code=source_code,
|
|
508
|
+
name_expressions=kernel_names,
|
|
509
|
+
backend="nvrtc",
|
|
510
|
+
options=build_options,
|
|
511
|
+
)
|
|
512
|
+
except Exception as e:
|
|
513
|
+
debug_dump_kernel()
|
|
514
|
+
logger.error(f"Failed to build module, build options were {build_options}.")
|
|
515
|
+
logger.error(e)
|
|
516
|
+
raise e
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
kernels = tuple(map(cls.Kernel, map(module.get_function, kernel_names)))
|
|
520
|
+
except Exception as e:
|
|
521
|
+
debug_dump_kernel()
|
|
522
|
+
logger.error(f"Failed to extract kernels, build options were {build_options}.")
|
|
523
|
+
logger.error(e)
|
|
524
|
+
raise e
|
|
525
|
+
finally:
|
|
526
|
+
if compile_using_nvrtc is not None:
|
|
527
|
+
compiler.compile_using_nvrtc = compile_using_nvrtc
|
|
528
|
+
|
|
529
|
+
binary, mangled_kernel_names = None, None
|
|
530
|
+
if len(compiled) == 1:
|
|
531
|
+
try:
|
|
532
|
+
binary, mapping = compiled[0]
|
|
533
|
+
mangled_kernel_names = tuple(mapping[kname] for kname in kernel_names)
|
|
534
|
+
if isinstance(binary, str):
|
|
535
|
+
binary = binary.encode()
|
|
536
|
+
if not isinstance(binary, bytes):
|
|
537
|
+
binary = None
|
|
538
|
+
except Exception:
|
|
539
|
+
binary = None
|
|
540
|
+
if binary is None:
|
|
541
|
+
logger.debug("Could not retrieve the compiled module from CuPy, it will not be cached.")
|
|
542
|
+
|
|
543
|
+
return kernels, binary, mangled_kernel_names
|
|
544
|
+
|
|
545
|
+
@classmethod
|
|
546
|
+
def load_kernels(cls, logger, binary_path, mangled_kernel_names):
|
|
547
|
+
"""Load kernels from a compiled module file (cubin or ptx) and the mangled kernel names."""
|
|
548
|
+
module = cp.RawModule(path=binary_path)
|
|
549
|
+
return tuple(map(cls.Kernel, map(module.get_function, mangled_kernel_names)))
|