Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions cuda_core/build_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _determine_cuda_major_version() -> str:

# Derive from the CUDA headers (the authoritative source for what we compile against).
cuda_path = _get_cuda_path()
cuda_h = os.path.join(cuda_path, "include", "cuda.h")
cuda_h = Path(cuda_path, "include", "cuda.h")
try:
with open(cuda_h, encoding="utf-8") as f:
for line in f:
Expand Down Expand Up @@ -162,10 +162,11 @@ def _build_cuda_core(debug=False):
# It seems setuptools' wildcard support has problems for namespace packages,
# so we explicitly spell out all Extension instances.
def module_names():
root_path = os.path.sep.join(["cuda", "core", ""])
for filename in glob.glob(f"{root_path}/**/*.pyx", recursive=True):
mod = filename[len(root_path) : -4]
if sys.platform == "win32" and mod.replace(os.path.sep, "/") in _posix_only_modules:
root_path = Path("cuda", "core")
for filename in glob.glob(str(root_path / "**" / "*.pyx"), recursive=True):
# Module names are always spelled POSIX-style, on every platform.
mod = Path(filename).relative_to(root_path).with_suffix("").as_posix()
if sys.platform == "win32" and mod in _posix_only_modules:
continue
yield mod

Expand All @@ -176,12 +177,12 @@ def get_sources(mod_name):
# Add module-specific .cpp file from _cpp/ directory if it exists
# Example: _resource_handles.pyx finds _cpp/resource_handles.cpp.
cpp_file = f"cuda/core/_cpp/{mod_name.lstrip('_')}.cpp"
if os.path.exists(cpp_file):
if Path(cpp_file).exists():
sources.append(cpp_file)

return sources

all_include_dirs = [os.path.join(cuda_path, "include")]
all_include_dirs = [str(Path(cuda_path, "include"))]
extra_compile_args = []
extra_link_args = []
extra_cythonize_kwargs = {}
Expand All @@ -205,7 +206,7 @@ def get_sources(mod_name):

ext_modules = tuple(
Extension(
f"cuda.core.{mod.replace(os.path.sep, '.')}",
f"cuda.core.{mod.replace('/', '.')}",
sources=get_sources(mod),
include_dirs=[
"cuda/core/_include",
Expand Down Expand Up @@ -239,7 +240,7 @@ def get_sources(mod_name):
return


def _add_cython_include_paths_to_pth(wheel_path: str) -> None:
def _add_cython_include_paths_to_pth(wheel_path: Path) -> None:
"""
Modify the .pth file in an editable install wheel to add Cython include paths.

Expand Down Expand Up @@ -270,7 +271,7 @@ def _add_cython_include_paths_to_pth(wheel_path: str) -> None:
# Create a temporary directory for wheel manipulation
with tempfile.TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
wheel_file = Path(wheel_path)
wheel_file = wheel_path

# Extract the wheel
extract_dir = tmpdir_path / "extracted"
Expand Down Expand Up @@ -325,7 +326,7 @@ def build_editable(wheel_directory, config_settings=None, metadata_directory=Non
wheel_name = _build_meta.build_editable(wheel_directory, config_settings, metadata_directory)

# Patch the .pth file to add Cython include paths
wheel_path = os.path.join(wheel_directory, wheel_name)
wheel_path = Path(wheel_directory, wheel_name)
_add_cython_include_paths_to_pth(wheel_path)

return wheel_name
Expand Down
44 changes: 31 additions & 13 deletions cuda_core/cuda/core/_program.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This module provides :class:`Program` for compiling source code into
from __future__ import annotations

from dataclasses import dataclass
from os import PathLike

from cuda.bindings import nvrtc
from cuda.core._linker import LinkerHandleT
Expand Down Expand Up @@ -141,6 +142,12 @@ class Program:
class ProgramOptions:
"""Customizable options for configuring :class:`Program`.

Every path-valued option (``include_path``, ``pre_include``, ``create_pch``,
``use_pch``, ``pch_dir``, ``fdevice_time_trace``) accepts either a :class:`str`
or any :class:`os.PathLike`, and stores it as a :class:`pathlib.Path`. Callers
building paths with :mod:`pathlib` therefore never need to convert back to
``str``.

Attributes
----------
name : str, optional
Expand Down Expand Up @@ -207,11 +214,11 @@ class ProgramOptions:
undefine_macro : Union[str, list[str]], optional
Cancel any previous definition of a macro, or list of macros.
Default: None
include_path : Union[str, list[str]], optional
include_path : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
Add the directory or directories to the list of directories to be searched for headers.
Default: None
pre_include : Union[str, list[str]], optional
Preinclude one or more headers during preprocessing. Can be either a string or a list of strings.
pre_include : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
Preinclude one or more headers during preprocessing. Can be either a single path or a list of paths.
Default: None
no_source_include : bool, optional
Disable the default behavior of adding the directory of each input source to the include path.
Expand Down Expand Up @@ -270,7 +277,7 @@ class ProgramOptions:
no_cache : bool, optional
Disable compiler caching.
Default: False
fdevice_time_trace : str, optional
fdevice_time_trace : Union[str, os.PathLike], optional
Generate time trace JSON for profiling compilation (NVRTC only).
Default: None
device_float128 : bool, optional
Expand All @@ -285,13 +292,13 @@ class ProgramOptions:
pch : bool, optional
Use default precompiled header (NVRTC only, CUDA 12.8+).
Default: False
create_pch : str, optional
create_pch : Union[str, os.PathLike], optional
Create precompiled header file (NVRTC only, CUDA 12.8+).
Default: None
use_pch : str, optional
use_pch : Union[str, os.PathLike], optional
Use specific precompiled header file (NVRTC only, CUDA 12.8+).
Default: None
pch_dir : str, optional
pch_dir : Union[str, os.PathLike], optional
PCH directory location (NVRTC only, CUDA 12.8+).
Default: None
pch_verbose : bool, optional
Expand Down Expand Up @@ -333,8 +340,8 @@ class ProgramOptions:
gen_opt_lto: bool | None = None
define_macro: str | tuple[str, str] | list[str | tuple[str, str]] | tuple[str | tuple[str, str], ...] | None = None
undefine_macro: str | list[str] | tuple[str] | None = None
include_path: str | list[str] | tuple[str] | None = None
pre_include: str | list[str] | tuple[str] | None = None
include_path: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
pre_include: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
no_source_include: bool | None = None
std: str | None = None
builtin_move_forward: bool | None = None
Expand All @@ -354,14 +361,14 @@ class ProgramOptions:
fdevice_syntax_only: bool | None = None
minimal: bool | None = None
no_cache: bool | None = None
fdevice_time_trace: str | None = None
fdevice_time_trace: str | PathLike[str] | None = None
device_float128: bool | None = None
frandom_seed: str | None = None
ofast_compile: str | None = None
pch: bool | None = None
create_pch: str | None = None
use_pch: str | None = None
pch_dir: str | None = None
create_pch: str | PathLike[str] | None = None
use_pch: str | PathLike[str] | None = None
pch_dir: str | PathLike[str] | None = None
pch_verbose: bool | None = None
pch_messages: bool | None = None
instantiate_templates_in_pch: bool | None = None
Expand Down Expand Up @@ -418,12 +425,23 @@ class ProgramOptions:
"""Convert extra_sources to bytes format for NVVM."""
__all__ = ['Program', 'ProgramOptions']
ProgramHandleT = nvrtc.nvrtcProgram | int | LinkerHandleT
_PATH_OPTION_FIELDS = ('include_path', 'pre_include', 'create_pch', 'use_pch', 'pch_dir', 'fdevice_time_trace')
_nvvm_module = None
_nvvm_import_attempted = False

def _can_load_generated_ptx() -> bool:
"""Check if the driver can load PTX generated by the current NVRTC version."""

def _coerce_path_option(value):
"""Normalize a path-valued :class:`ProgramOptions` field.

``str`` / :class:`os.PathLike` becomes :class:`pathlib.Path`; a ``list``
or ``tuple`` has its ``str`` / :class:`os.PathLike` items converted while
keeping the container type. Anything else is returned unchanged, so the
"silently ignored at compile time" behavior of non-path values (``False``,
``range(...)``, ...) is unaffected.
"""

def _program_compile_uncached(program, target_type, name_expressions, logs):
"""Run ``Program_compile`` without the cache wrapper.

Expand Down
82 changes: 67 additions & 15 deletions cuda_core/cuda/core/_program.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This module provides :class:`Program` for compiling source code into
from __future__ import annotations

from dataclasses import dataclass
from os import PathLike
from pathlib import Path
import threading
from typing import TYPE_CHECKING
from warnings import warn
Expand Down Expand Up @@ -294,6 +296,12 @@ cdef class Program:
class ProgramOptions:
"""Customizable options for configuring :class:`Program`.

Every path-valued option (``include_path``, ``pre_include``, ``create_pch``,
``use_pch``, ``pch_dir``, ``fdevice_time_trace``) accepts either a :class:`str`
or any :class:`os.PathLike`, and stores it as a :class:`pathlib.Path`. Callers
building paths with :mod:`pathlib` therefore never need to convert back to
``str``.

Attributes
----------
name : str, optional
Expand Down Expand Up @@ -360,11 +368,11 @@ class ProgramOptions:
undefine_macro : Union[str, list[str]], optional
Cancel any previous definition of a macro, or list of macros.
Default: None
include_path : Union[str, list[str]], optional
include_path : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
Add the directory or directories to the list of directories to be searched for headers.
Default: None
pre_include : Union[str, list[str]], optional
Preinclude one or more headers during preprocessing. Can be either a string or a list of strings.
pre_include : Union[str, os.PathLike, list[Union[str, os.PathLike]]], optional
Preinclude one or more headers during preprocessing. Can be either a single path or a list of paths.
Default: None
no_source_include : bool, optional
Disable the default behavior of adding the directory of each input source to the include path.
Expand Down Expand Up @@ -423,7 +431,7 @@ class ProgramOptions:
no_cache : bool, optional
Disable compiler caching.
Default: False
fdevice_time_trace : str, optional
fdevice_time_trace : Union[str, os.PathLike], optional
Generate time trace JSON for profiling compilation (NVRTC only).
Default: None
device_float128 : bool, optional
Expand All @@ -438,13 +446,13 @@ class ProgramOptions:
pch : bool, optional
Use default precompiled header (NVRTC only, CUDA 12.8+).
Default: False
create_pch : str, optional
create_pch : Union[str, os.PathLike], optional
Create precompiled header file (NVRTC only, CUDA 12.8+).
Default: None
use_pch : str, optional
use_pch : Union[str, os.PathLike], optional
Use specific precompiled header file (NVRTC only, CUDA 12.8+).
Default: None
pch_dir : str, optional
pch_dir : Union[str, os.PathLike], optional
PCH directory location (NVRTC only, CUDA 12.8+).
Default: None
pch_verbose : bool, optional
Expand Down Expand Up @@ -487,8 +495,8 @@ class ProgramOptions:
gen_opt_lto: bool | None = None
define_macro: str | tuple[str, str] | list[str | tuple[str, str]] | tuple[str | tuple[str, str], ...] | None = None
undefine_macro: str | list[str] | tuple[str] | None = None
include_path: str | list[str] | tuple[str] | None = None
pre_include: str | list[str] | tuple[str] | None = None
include_path: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
pre_include: str | PathLike[str] | list[str | PathLike[str]] | tuple[str | PathLike[str]] | None = None
no_source_include: bool | None = None
std: str | None = None
builtin_move_forward: bool | None = None
Expand All @@ -508,14 +516,14 @@ class ProgramOptions:
fdevice_syntax_only: bool | None = None
minimal: bool | None = None
no_cache: bool | None = None
fdevice_time_trace: str | None = None
fdevice_time_trace: str | PathLike[str] | None = None
device_float128: bool | None = None
frandom_seed: str | None = None
ofast_compile: str | None = None
pch: bool | None = None
create_pch: str | None = None
use_pch: str | None = None
pch_dir: str | None = None
create_pch: str | PathLike[str] | None = None
use_pch: str | PathLike[str] | None = None
pch_dir: str | PathLike[str] | None = None
pch_verbose: bool | None = None
pch_messages: bool | None = None
instantiate_templates_in_pch: bool | None = None
Expand All @@ -528,6 +536,12 @@ class ProgramOptions:
if self.name is None:
self.name = "default_program"
self._name = self.name.encode()
# Path-valued options accept str or os.PathLike; normalize to Path so
# callers never have to convert back to str just to build options.
for _field in _PATH_OPTION_FIELDS:
_value = getattr(self, _field)
if _value is not None:
setattr(self, _field, _coerce_path_option(_value))
# Set arch to default if not provided
if self.arch is None:
self.arch = f"sm_{Device().arch}"
Expand Down Expand Up @@ -632,6 +646,42 @@ class ProgramOptions:
# =============================================================================


# ``ProgramOptions`` fields that name a filesystem path. ``str`` and
# ``os.PathLike`` values for these are normalized to :class:`pathlib.Path` in
# ``ProgramOptions.__post_init__`` so everything downstream sees one type.
#
# ``name`` is not here: NVRTC uses it as the source *filename*, but it is a
# plain label (``ProgramOptions.__post_init__`` encodes it, and the program
# cache inspects it for a directory component), so it stays ``str``. ``time``
# is not here either: NVRTC treats it as an output filename, but the same
# field is forwarded to ``LinkerOptions.time`` (a bool flag) for PTX inputs.
_PATH_OPTION_FIELDS = (
"include_path",
"pre_include",
"create_pch",
"use_pch",
"pch_dir",
"fdevice_time_trace",
)


def _coerce_path_option(value):
"""Normalize a path-valued :class:`ProgramOptions` field.

``str`` / :class:`os.PathLike` becomes :class:`pathlib.Path`; a ``list``
or ``tuple`` has its ``str`` / :class:`os.PathLike` items converted while
keeping the container type. Anything else is returned unchanged, so the
"silently ignored at compile time" behavior of non-path values (``False``,
``range(...)``, ...) is unaffected.
"""
if isinstance(value, (str, PathLike)):
return Path(value)
if isinstance(value, (list, tuple)):
coerced = [Path(v) if isinstance(v, (str, PathLike)) else v for v in value]
return tuple(coerced) if isinstance(value, tuple) else coerced
return value


def _program_compile_uncached(program, target_type, name_expressions, logs):
"""Run ``Program_compile`` without the cache wrapper.

Expand Down Expand Up @@ -1129,13 +1179,15 @@ cdef inline list _prepare_nvrtc_options_impl(object opts):
for macro in opts.undefine_macro:
options.append(f"--undefine-macro={macro}")
if opts.include_path is not None:
if isinstance(opts.include_path, str):
# ``__post_init__`` turns str/PathLike into Path, but the dataclass is
# mutable, so accept either form here.
if isinstance(opts.include_path, (str, PathLike)):
options.append(f"--include-path={opts.include_path}")
elif is_sequence(opts.include_path):
for path in opts.include_path:
options.append(f"--include-path={path}")
if opts.pre_include is not None:
if isinstance(opts.pre_include, str):
if isinstance(opts.pre_include, (str, PathLike)):
options.append(f"--pre-include={opts.pre_include}")
elif is_sequence(opts.pre_include):
for header in opts.pre_include:
Expand Down
15 changes: 9 additions & 6 deletions cuda_core/cuda/core/utils/_program_cache/_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import abc
import collections.abc
import hashlib
import os
from typing import Any, Callable, Sequence

# Mutual-dependency contract: this module imports ProgramOptions from
Expand Down Expand Up @@ -269,7 +270,8 @@ def _option_is_set(options: ProgramOptions, name: str) -> bool:

- Boolean flags (``pch``): truthy only.
- str-or-sequence fields (``include_path``, ``pre_include``): ``str``
(including empty) or a non-empty ``collections.abc.Sequence`` (list,
(including empty), ``os.PathLike`` (``ProgramOptions`` normalizes both
to ``pathlib.Path``), or a non-empty ``collections.abc.Sequence`` (list,
tuple, range, user subclass, ...); everything else (``False``, ``int``,
empty sequence, ``None``) is ignored by the compiler and must not
trigger a cache-time guard.
Expand All @@ -284,11 +286,12 @@ def _option_is_set(options: ProgramOptions, name: str) -> bool:
if name in _BOOLEAN_OPTION_FIELDS:
return bool(value)
if name in _STR_OR_SEQUENCE_OPTION_FIELDS:
# Mirror ``_prepare_nvrtc_options_impl``: it checks ``isinstance(v, str)``
# first, then ``is_sequence(v)`` (which is ``isinstance(v, Sequence)``).
# We therefore accept any ``collections.abc.Sequence`` (range, deque,
# user subclass, etc.), not just list/tuple.
if isinstance(value, str):
# Mirror ``_prepare_nvrtc_options_impl``: it checks
# ``isinstance(v, (str, os.PathLike))`` first, then ``is_sequence(v)``
# (which is ``isinstance(v, Sequence)``). We therefore accept any
# ``collections.abc.Sequence`` (range, deque, user subclass, etc.),
# not just list/tuple.
if isinstance(value, (str, os.PathLike)):
return True
if isinstance(value, collections.abc.Sequence):
return len(value) > 0
Expand Down
Loading
Loading