Skip to content

Commit a288474

Browse files
committed
Isolate CUmemLocation construction in a versioned helper
Build CUmemLocation via field assignment in to_cumemlocation() so cuda.core compiles against both the 13.3 and 13.4 layouts. The localized arm is an optional helper argument that exists only when CUDA_VERSION >= 13040.
1 parent 819c586 commit a288474

7 files changed

Lines changed: 152 additions & 45 deletions

File tree

‎cuda_core/build_hooks.py‎

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,23 @@ def _get_cuda_path() -> str:
7575
return cuda_path
7676

7777

78+
@functools.cache
79+
def _read_cuda_version_int() -> int | None:
80+
"""Return the numeric CUDA_VERSION macro from cuda.h, or None if unavailable."""
81+
cuda_path = _get_cuda_path()
82+
cuda_h = os.path.join(cuda_path, "include", "cuda.h")
83+
try:
84+
with open(cuda_h, encoding="utf-8") as f:
85+
for line in f:
86+
m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line)
87+
if m:
88+
# CUDA_VERSION is e.g. 12020 for 12.2, 13040 for 13.4.
89+
return int(m.group(1))
90+
except OSError:
91+
pass
92+
return None
93+
94+
7895
@functools.cache
7996
def _determine_cuda_major_version() -> str:
8097
"""Determine the CUDA major version for building cuda.core.
@@ -97,20 +114,11 @@ def _determine_cuda_major_version() -> str:
97114
return cuda_major
98115

99116
# Derive from the CUDA headers (the authoritative source for what we compile against).
100-
cuda_path = _get_cuda_path()
101-
cuda_h = os.path.join(cuda_path, "include", "cuda.h")
102-
try:
103-
with open(cuda_h, encoding="utf-8") as f:
104-
for line in f:
105-
m = re.match(r"^#\s*define\s+CUDA_VERSION\s+(\d+)\s*$", line)
106-
if m:
107-
v = int(m.group(1))
108-
# CUDA_VERSION is e.g. 12020 for 12.2.
109-
cuda_major = str(v // 1000)
110-
print("CUDA MAJOR VERSION:", cuda_major)
111-
return cuda_major
112-
except OSError:
113-
pass
117+
version = _read_cuda_version_int()
118+
if version is not None:
119+
cuda_major = str(version // 1000)
120+
print("CUDA MAJOR VERSION:", cuda_major)
121+
return cuda_major
114122

115123
# CUDA_PATH or CUDA_HOME is required for the build, so we should not reach here
116124
# in normal circumstances. Raise an error to make the issue clear.
@@ -121,6 +129,22 @@ def _determine_cuda_major_version() -> str:
121129
)
122130

123131

132+
@functools.cache
133+
def _cuda_core_has_localized_location() -> bool:
134+
"""Whether CUmemLocation exposes the ``localized`` union arm (CUDA 13.4+).
135+
136+
CUDA 13.4 adds ``CUmemLocation.localized`` and
137+
``CU_MEM_LOCATION_TYPE_DEVICE_LOCALITY_DOMAIN``. This flag is independent of
138+
``CUDA_CORE_BUILD_MAJOR`` so 13.3 and 13.4 can share a major version while
139+
still compiling different ``to_cumemlocation`` signatures.
140+
"""
141+
override = os.environ.get("CUDA_CORE_HAS_LOCALIZED_LOCATION")
142+
if override is not None:
143+
return bool(int(override))
144+
version = _read_cuda_version_int()
145+
return version is not None and version >= 13040
146+
147+
124148
# used later by setup()
125149
_extensions = None
126150

@@ -220,7 +244,10 @@ def get_sources(mod_name):
220244
)
221245

222246
nthreads = int(os.environ.get("CUDA_PYTHON_PARALLEL_LEVEL", os.cpu_count() // 2))
223-
compile_time_env = {"CUDA_CORE_BUILD_MAJOR": int(_determine_cuda_major_version())}
247+
compile_time_env = {
248+
"CUDA_CORE_BUILD_MAJOR": int(_determine_cuda_major_version()),
249+
"CUDA_CORE_HAS_LOCALIZED_LOCATION": int(_cuda_core_has_localized_location()),
250+
}
224251
compiler_directives = {"embedsignature": True, "warn.deprecated.IF": False, "freethreading_compatible": True}
225252
_CythonOptions.warning_errors = True
226253
if COMPILE_FOR_COVERAGE:

‎cuda_core/cuda/core/_memory/_device_memory_resource.pyx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from __future__ import annotations
66

77
from cuda.bindings cimport cydriver
8+
from cuda.core._memory._location cimport to_cumemlocation
89
from cuda.core._memory._memory_pool cimport (
910
_MemPool, MP_init_create_pool, MP_raise_release_threshold,
1011
)
@@ -321,10 +322,7 @@ cpdef str DMR_mempool_get_access(DeviceMemoryResource dmr, int device_id):
321322

322323
cdef int dev_id = Device(device_id).device_id
323324
cdef cydriver.CUmemAccess_flags flags
324-
cdef cydriver.CUmemLocation location = cydriver.CUmemLocation(
325-
type=cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE,
326-
id=dev_id,
327-
)
325+
cdef cydriver.CUmemLocation location = to_cumemlocation("device", dev_id)
328326

329327
with nogil:
330328
HANDLE_RETURN(cydriver.cuMemPoolGetAccess(&flags, as_cu(dmr._h_pool), &location))

‎cuda_core/cuda/core/_memory/_location.pxd‎

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,62 @@
99
# cimport it without either module depending on the other. ``CUmemLocation``
1010
# is only populated on a CUDA 13 build; the CUDA 12 stub exists so callers
1111
# compiled there still resolve the symbol.
12+
#
13+
# Construction uses field assignment rather than Cython struct literals so
14+
# the same source compiles against both the CUDA 13.3 two-member declaration
15+
# and the CUDA 13.4 declaration that adds the ``localized`` union arm.
16+
# ``CUDA_CORE_HAS_LOCALIZED_LOCATION`` selects a helper signature with an
17+
# optional ``localized`` argument (13.4+) or without it (13.3). Passing
18+
# ``localized=...`` is therefore a Cython compile-time error on 13.3.
1219

1320
from cuda.bindings cimport cydriver
1421

1522

1623
IF CUDA_CORE_BUILD_MAJOR >= 13:
17-
cdef inline cydriver.CUmemLocation to_cumemlocation(str kind, int loc_id):
24+
cdef inline void _fill_id_location(
25+
cydriver.CUmemLocation* cu_loc, str kind, int loc_id
26+
) except *:
1827
if kind == "device":
19-
return cydriver.CUmemLocation(
20-
type=cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE,
21-
id=loc_id)
28+
cu_loc.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE
29+
cu_loc.id = loc_id
2230
elif kind == "host":
23-
return cydriver.CUmemLocation(
24-
type=cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST,
25-
id=0)
31+
cu_loc.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST
32+
cu_loc.id = 0
2633
elif kind == "host_numa":
27-
return cydriver.CUmemLocation(
28-
type=cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST_NUMA,
29-
id=loc_id)
34+
cu_loc.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST_NUMA
35+
cu_loc.id = loc_id
3036
elif kind == "host_numa_current":
31-
return cydriver.CUmemLocation(
32-
type=cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST_NUMA_CURRENT,
33-
id=0)
37+
cu_loc.type = cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST_NUMA_CURRENT
38+
cu_loc.id = 0
3439
else:
3540
raise ValueError(f"unknown location kind: {kind!r}")
41+
42+
IF CUDA_CORE_HAS_LOCALIZED_LOCATION:
43+
cdef inline cydriver.CUmemLocation to_cumemlocation(
44+
str kind, int loc_id=0, tuple localized=None
45+
):
46+
cdef cydriver.CUmemLocation cu_loc
47+
if kind == "device_locality_domain":
48+
if localized is None:
49+
raise ValueError(
50+
"kind='device_locality_domain' requires "
51+
"localized=(device_id, locality_domain_id)"
52+
)
53+
cu_loc.type = (
54+
cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE_LOCALITY_DOMAIN
55+
)
56+
cu_loc.localized.deviceId = localized[0]
57+
cu_loc.localized.localityDomainId = localized[1]
58+
return cu_loc
59+
_fill_id_location(&cu_loc, kind, loc_id)
60+
return cu_loc
61+
ELSE:
62+
cdef inline cydriver.CUmemLocation to_cumemlocation(str kind, int loc_id=0):
63+
cdef cydriver.CUmemLocation cu_loc
64+
_fill_id_location(&cu_loc, kind, loc_id)
65+
return cu_loc
3666
ELSE:
37-
cdef inline cydriver.CUmemLocation to_cumemlocation(str kind, int loc_id):
67+
cdef inline cydriver.CUmemLocation to_cumemlocation(str kind, int loc_id=0):
3868
raise NotImplementedError(
3969
"CUmemLocation requires cuda.core built against CUDA 13 headers"
4070
)

‎cuda_core/cuda/core/_memory/_managed_memory_ops.pyx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,7 @@ cdef void _do_single_advise(Buffer buf, object advice_value, object loc, bint al
193193
# Driver ignores location for read_mostly / unset_preferred_location
194194
# advice values but still validates the CUmemLocation; pass a
195195
# host placeholder.
196-
cu_loc = cydriver.CUmemLocation(
197-
type=cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_HOST,
198-
id=0)
196+
cu_loc = to_cumemlocation("host", 0)
199197
else:
200198
cu_loc = to_cumemlocation(loc.kind, loc.id)
201199
with nogil:

‎cuda_core/cuda/core/_memory/_peer_access_utils.pyx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any
1010

1111
from cuda.bindings cimport cydriver
1212
from cuda.core._memory._device_memory_resource cimport DeviceMemoryResource
13+
from cuda.core._memory._location cimport to_cumemlocation
1314
from cuda.core._resource_handles cimport as_cu
1415
from cuda.core._utils.cuda_utils cimport HANDLE_RETURN
1516
from cpython.mem cimport PyMem_Malloc, PyMem_Free
@@ -113,10 +114,7 @@ cdef inline tuple _query_peer_access_ids(DeviceMemoryResource mr):
113114
cdef inline bint _peer_access_includes(DeviceMemoryResource mr, int dev_id):
114115
"""Return True if peer access from ``dev_id`` is currently granted."""
115116
cdef cydriver.CUmemAccess_flags flags
116-
cdef cydriver.CUmemLocation location = cydriver.CUmemLocation(
117-
type=cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE,
118-
id=dev_id,
119-
)
117+
cdef cydriver.CUmemLocation location = to_cumemlocation("device", dev_id)
120118
with nogil:
121119
HANDLE_RETURN(cydriver.cuMemPoolGetAccess(&flags, as_cu(mr._h_pool), &location))
122120
return flags == cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE

‎cuda_core/cuda/core/graph/_graph_node.pyx‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ from cuda.core._event cimport Event
2121
from cuda.core._kernel_arg_handler cimport ParamHolder
2222
from cuda.core._launch_config cimport LaunchConfig
2323
from cuda.core._memory._buffer cimport Buffer
24+
from cuda.core._memory._location cimport to_cumemlocation
2425
from cuda.core._module cimport Kernel
2526
from cuda.core.graph._graph_definition cimport GraphCondition, GraphDefinition
2627
from cuda.core.graph._subclasses cimport (
@@ -835,11 +836,8 @@ cdef inline AllocNode GN_alloc(GraphNode self, size_t size, object device,
835836
peer_id = getattr(peer_dev, 'device_id', peer_dev)
836837
peer_ids.append(peer_id)
837838
access_descs.push_back(cydriver.CUmemAccessDesc_st(
838-
cydriver.CUmemLocation_st(
839-
cydriver.CUmemLocationType.CU_MEM_LOCATION_TYPE_DEVICE,
840-
peer_id
841-
),
842-
cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE
839+
to_cumemlocation("device", peer_id),
840+
cydriver.CUmemAccess_flags.CU_MEM_ACCESS_FLAGS_PROT_READWRITE,
843841
))
844842
845843
cdef str memory_type_str = "device" if memory_type is None else str(memory_type)

‎cuda_core/tests/test_build_hooks.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ def _check_version_detection(
9999
cuda_h.write_text(f"#define CUDA_VERSION {cuda_version}\n")
100100

101101
build_hooks._get_cuda_path.cache_clear()
102+
build_hooks._read_cuda_version_int.cache_clear()
102103
build_hooks._determine_cuda_major_version.cache_clear()
104+
build_hooks._cuda_core_has_localized_location.cache_clear()
103105
get_cuda_path_or_home.cache_clear()
104106

105107
mock_env = {
@@ -124,7 +126,9 @@ class TestGetCudaMajorVersion:
124126
def test_env_var_override(self, version):
125127
"""CUDA_CORE_BUILD_MAJOR env var override works with various versions."""
126128
build_hooks._get_cuda_path.cache_clear()
129+
build_hooks._read_cuda_version_int.cache_clear()
127130
build_hooks._determine_cuda_major_version.cache_clear()
131+
build_hooks._cuda_core_has_localized_location.cache_clear()
128132
get_cuda_path_or_home.cache_clear()
129133
with mock.patch.dict(os.environ, {"CUDA_CORE_BUILD_MAJOR": version}, clear=False):
130134
result = build_hooks._determine_cuda_major_version()
@@ -158,10 +162,64 @@ def test_env_var_takes_priority_over_headers(self):
158162
def test_missing_cuda_path_raises_error(self):
159163
"""RuntimeError is raised when CUDA_PATH/CUDA_HOME not set and no env var override."""
160164
build_hooks._get_cuda_path.cache_clear()
165+
build_hooks._read_cuda_version_int.cache_clear()
161166
build_hooks._determine_cuda_major_version.cache_clear()
167+
build_hooks._cuda_core_has_localized_location.cache_clear()
162168
get_cuda_path_or_home.cache_clear()
163169
with (
164170
mock.patch.dict(os.environ, {}, clear=True),
165171
pytest.raises(RuntimeError, match="CUDA_PATH or CUDA_HOME"),
166172
):
167173
build_hooks._determine_cuda_major_version()
174+
175+
176+
def _check_localized_location_detection(cuda_version, expected, *, env_override=None):
177+
"""Test localized-arm detection with a mock cuda.h."""
178+
with tempfile.TemporaryDirectory() as tmpdir:
179+
include_dir = Path(tmpdir) / "include"
180+
include_dir.mkdir()
181+
(include_dir / "cuda.h").write_text(f"#define CUDA_VERSION {cuda_version}\n")
182+
183+
build_hooks._get_cuda_path.cache_clear()
184+
build_hooks._read_cuda_version_int.cache_clear()
185+
build_hooks._cuda_core_has_localized_location.cache_clear()
186+
get_cuda_path_or_home.cache_clear()
187+
188+
mock_env = {"CUDA_PATH": tmpdir}
189+
if env_override is not None:
190+
mock_env["CUDA_CORE_HAS_LOCALIZED_LOCATION"] = env_override
191+
192+
with mock.patch.dict(os.environ, mock_env, clear=True):
193+
assert build_hooks._cuda_core_has_localized_location() is expected
194+
195+
196+
class TestHasLocalizedLocation:
197+
"""Tests for _cuda_core_has_localized_location()."""
198+
199+
@pytest.mark.agent_authored(model="grok-4.6")
200+
@pytest.mark.parametrize(
201+
("cuda_version", "expected"),
202+
[
203+
(12080, False),
204+
(13000, False),
205+
(13030, False),
206+
(13040, True),
207+
(14000, True),
208+
],
209+
ids=["12.8", "13.0", "13.3", "13.4", "14.0"],
210+
)
211+
def test_cuda_headers_parsing(self, cuda_version, expected):
212+
"""CUDA_VERSION 13040+ enables the localized CUmemLocation arm."""
213+
_check_localized_location_detection(cuda_version, expected)
214+
215+
@pytest.mark.agent_authored(model="grok-4.6")
216+
@pytest.mark.parametrize(
217+
("override", "expected"),
218+
[
219+
("0", False),
220+
("1", True),
221+
],
222+
)
223+
def test_env_var_override(self, override, expected):
224+
"""CUDA_CORE_HAS_LOCALIZED_LOCATION overrides the header-derived value."""
225+
_check_localized_location_detection(13030, expected, env_override=override)

0 commit comments

Comments
 (0)