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 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)))