Skip to content

Commit bf8dc41

Browse files
authored
cuda.core: add LaunchConfig.programmatic_stream_serialization for programmatic dependent launch (#2456)
* feat(cuda.core): expose PDL via LaunchConfig.programmatic_stream_serialization Allow users to set CU_LAUNCH_ATTRIBUTE_PROGRAMMATIC_STREAM_SERIALIZATION through LaunchConfig, matching the is_cooperative attribute pattern (#1334). * test(cuda.core): verify PDL overlap for primary/secondary launch Add an end-to-end Hopper+ test that launches primary and secondary kernels on the same stream with programmatic_stream_serialization, and asserts overlap only when the PDL attribute is enabled (#1334). * test(cuda.core): simplify PDL secondary kernel and log success Drop unused secondary sync/sleep from the overlap test, clarify the primary clock window comment, and print a short success line for CI. * add pre-commit passed * revise to xfail
1 parent 182a7f6 commit bf8dc41

5 files changed

Lines changed: 149 additions & 4 deletions

File tree

cuda_core/cuda/core/_launch_config.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ cdef class LaunchConfig:
1515
public tuple block
1616
public int shmem_size
1717
public bint is_cooperative
18+
public bint programmatic_stream_serialization
1819

1920
vector[cydriver.CUlaunchAttribute] _attrs
2021
object __weakref__

cuda_core/cuda/core/_launch_config.pyi

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,13 @@ class LaunchConfig:
3535
(Default to size 0)
3636
is_cooperative : bool, optional
3737
Whether this config can be used to launch a cooperative kernel.
38+
programmatic_stream_serialization : bool, optional
39+
Whether to allow programmatic stream serialization (PDL). When True,
40+
the kernel may overlap with a previous kernel in the same stream that
41+
signals completion via programmatic means.
3842
"""
3943

40-
def __init__(self, grid: int | tuple[int, ...] | None=None, cluster: int | tuple[int, ...] | None=None, block: int | tuple[int, ...] | None=None, shmem_size: int | None=None, is_cooperative: bool=False) -> None:
44+
def __init__(self, grid: int | tuple[int, ...] | None=None, cluster: int | tuple[int, ...] | None=None, block: int | tuple[int, ...] | None=None, shmem_size: int | None=None, is_cooperative: bool=False, programmatic_stream_serialization: bool=False) -> None:
4145
"""Initialize LaunchConfig with validation.
4246
4347
Parameters
@@ -52,6 +56,8 @@ class LaunchConfig:
5256
Dynamic shared memory size in bytes (default: 0)
5357
is_cooperative : bool, optional
5458
Whether to launch as cooperative kernel (default: False)
59+
programmatic_stream_serialization : bool, optional
60+
Whether to allow programmatic stream serialization / PDL (default: False)
5561
"""
5662

5763
def _identity(self) -> tuple[Any, ...]:
@@ -65,7 +71,7 @@ class LaunchConfig:
6571

6672
def __hash__(self) -> int:
6773
...
68-
_LAUNCH_CONFIG_ATTRS = ('grid', 'cluster', 'block', 'shmem_size', 'is_cooperative')
74+
_LAUNCH_CONFIG_ATTRS = ('grid', 'cluster', 'block', 'shmem_size', 'is_cooperative', 'programmatic_stream_serialization')
6975
__all__ = ['LaunchConfig']
7076

7177
def _to_native_launch_config(config: LaunchConfig) -> object:

cuda_core/cuda/core/_launch_config.pyx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ from cuda.core._utils.cuda_utils import (
1313
driver,
1414
)
1515

16-
_LAUNCH_CONFIG_ATTRS = ('grid', 'cluster', 'block', 'shmem_size', 'is_cooperative')
16+
_LAUNCH_CONFIG_ATTRS = (
17+
'grid',
18+
'cluster',
19+
'block',
20+
'shmem_size',
21+
'is_cooperative',
22+
'programmatic_stream_serialization',
23+
)
1724

1825
__all__ = ['LaunchConfig']
1926

@@ -48,6 +55,10 @@ cdef class LaunchConfig:
4855
(Default to size 0)
4956
is_cooperative : bool, optional
5057
Whether this config can be used to launch a cooperative kernel.
58+
programmatic_stream_serialization : bool, optional
59+
Whether to allow programmatic stream serialization (PDL). When True,
60+
the kernel may overlap with a previous kernel in the same stream that
61+
signals completion via programmatic means.
5162
"""
5263

5364
# TODO: expand LaunchConfig to include other attributes
@@ -60,6 +71,7 @@ cdef class LaunchConfig:
6071
block: int | tuple[int, ...] | None = None,
6172
shmem_size: int | None = None,
6273
is_cooperative: bool = False,
74+
programmatic_stream_serialization: bool = False,
6375
) -> None:
6476
"""Initialize LaunchConfig with validation.
6577

@@ -75,6 +87,8 @@ cdef class LaunchConfig:
7587
Dynamic shared memory size in bytes (default: 0)
7688
is_cooperative : bool, optional
7789
Whether to launch as cooperative kernel (default: False)
90+
programmatic_stream_serialization : bool, optional
91+
Whether to allow programmatic stream serialization / PDL (default: False)
7892
"""
7993
# Convert and validate grid and block dimensions
8094
self.grid = cast_to_3_tuple("LaunchConfig.grid", grid)
@@ -101,6 +115,7 @@ cdef class LaunchConfig:
101115
self.shmem_size = shmem_size
102116

103117
self.is_cooperative = is_cooperative
118+
self.programmatic_stream_serialization = programmatic_stream_serialization
104119

105120
if self.is_cooperative and not Device().properties.cooperative_launch:
106121
raise CUDAError("cooperative kernels are not supported on this device")
@@ -149,6 +164,11 @@ cdef class LaunchConfig:
149164
attr.value.cooperative = 1
150165
self._attrs.push_back(attr)
151166

167+
if self.programmatic_stream_serialization:
168+
attr.id = cydriver.CUlaunchAttributeID.CU_LAUNCH_ATTRIBUTE_PROGRAMMATIC_STREAM_SERIALIZATION
169+
attr.value.programmaticStreamSerializationAllowed = 1
170+
self._attrs.push_back(attr)
171+
152172
drv_cfg.numAttrs = self._attrs.size()
153173
drv_cfg.attrs = self._attrs.data()
154174

@@ -204,6 +224,12 @@ cpdef object _to_native_launch_config(LaunchConfig config):
204224
attr.value.cooperative = 1
205225
attrs.append(attr)
206226

227+
if config.programmatic_stream_serialization:
228+
attr = driver.CUlaunchAttribute()
229+
attr.id = driver.CUlaunchAttributeID.CU_LAUNCH_ATTRIBUTE_PROGRAMMATIC_STREAM_SERIALIZATION
230+
attr.value.programmaticStreamSerializationAllowed = 1
231+
attrs.append(attr)
232+
207233
drv_cfg.numAttrs = len(attrs)
208234
drv_cfg.attrs = attrs
209235

cuda_core/tests/test_launcher.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,117 @@ class _FakeDev:
183183
assert attr.value.cooperative == 1, f"Expected cooperative=1, got {attr.value.cooperative}"
184184

185185

186+
def test_to_native_launch_config_pdl():
187+
"""LaunchConfig(programmatic_stream_serialization=True) maps to the PDL launch attribute."""
188+
from cuda.bindings import driver
189+
from cuda.core._launch_config import _to_native_launch_config
190+
191+
config = LaunchConfig(grid=2, block=4, programmatic_stream_serialization=True)
192+
native = _to_native_launch_config(config)
193+
assert native.gridDimX == 2
194+
assert native.blockDimX == 4
195+
assert native.numAttrs == 1
196+
attr = native.attrs[0]
197+
assert attr.id == driver.CUlaunchAttributeID.CU_LAUNCH_ATTRIBUTE_PROGRAMMATIC_STREAM_SERIALIZATION, (
198+
f"Expected CU_LAUNCH_ATTRIBUTE_PROGRAMMATIC_STREAM_SERIALIZATION, got {attr.id}"
199+
)
200+
assert attr.value.programmaticStreamSerializationAllowed == 1, (
201+
f"Expected programmaticStreamSerializationAllowed=1, got {attr.value.programmaticStreamSerializationAllowed}"
202+
)
203+
204+
205+
@skipif_need_cuda_headers
206+
def test_pdl_primary_secondary_overlap_same_stream():
207+
"""Primary + secondary PDL launch on one stream can overlap on Hopper+.
208+
209+
Secondary is launched with ``programmatic_stream_serialization=True``. After
210+
the primary triggers completion, it spins until it observes a flag written by
211+
the secondary's independent preamble — proving both grids were resident at
212+
once. Without PDL, the secondary cannot start until the primary exits.
213+
214+
Note concurrency is opportunistic, so a missing overlap execution is reported as
215+
an expected failure.
216+
"""
217+
dev = Device()
218+
if dev.compute_capability < (9, 0):
219+
pytest.skip("Programmatic Dependent Launch requires compute capability >= 9.0")
220+
dev.set_current()
221+
stream = dev.create_stream(options={"nonblocking": True})
222+
223+
# clock64 budgets are in GPU cycles; keep the post-trigger window long enough
224+
# for the secondary to boot, but short enough for a unit test.
225+
code = r"""
226+
#include <cuda_device_runtime_api.h>
227+
228+
extern "C" __global__ void primary_kernel(int* secondary_started, int* overlapped) {
229+
cudaTriggerProgrammaticLaunchCompletion();
230+
231+
const long long deadline = clock64() + 100000000LL; // ~50ms @ ~2GHz
232+
if (threadIdx.x == 0 && blockIdx.x == 0) {
233+
while (clock64() < deadline) {
234+
if (atomicAdd(secondary_started, 0) != 0) {
235+
atomicExch(overlapped, 1);
236+
return;
237+
}
238+
__nanosleep(1000);
239+
}
240+
}
241+
}
242+
243+
extern "C" __global__ void secondary_kernel(int* secondary_started) {
244+
if (threadIdx.x == 0 && blockIdx.x == 0) {
245+
atomicExch(secondary_started, 1);
246+
}
247+
}
248+
"""
249+
250+
arch = "".join(f"{i}" for i in dev.compute_capability)
251+
pro_opts = ProgramOptions(std="c++17", arch=f"sm_{arch}", include_path=helpers.CUDA_INCLUDE_PATH)
252+
prog = Program(code, code_type="c++", options=pro_opts)
253+
mod = prog.compile("cubin")
254+
primary = mod.get_kernel("primary_kernel")
255+
secondary = mod.get_kernel("secondary_kernel")
256+
257+
mr = LegacyPinnedMemoryResource()
258+
secondary_started = np.from_dlpack(mr.allocate(4)).view(np.int32)
259+
overlapped = np.from_dlpack(mr.allocate(4)).view(np.int32)
260+
261+
primary_cfg = LaunchConfig(grid=1, block=1)
262+
secondary_cfg = LaunchConfig(grid=1, block=1, programmatic_stream_serialization=True)
263+
secondary_serial_cfg = LaunchConfig(grid=1, block=1)
264+
265+
def _run(secondary_launch_cfg: LaunchConfig) -> int:
266+
secondary_started[0] = 0
267+
overlapped[0] = 0
268+
launch(stream, primary_cfg, primary, secondary_started.ctypes.data, overlapped.ctypes.data)
269+
launch(stream, secondary_launch_cfg, secondary, secondary_started.ctypes.data)
270+
stream.sync()
271+
return int(overlapped[0])
272+
273+
# Without the PDL attribute, same-stream kernels stay serialized.
274+
assert _run(secondary_serial_cfg) == 0, "Expected no overlap when programmatic_stream_serialization is False"
275+
276+
# PDL overlap is opportunistic; retry a few times on a quiet GPU.
277+
saw_overlap = False
278+
for _ in range(5):
279+
if _run(secondary_cfg) == 1:
280+
saw_overlap = True
281+
break
282+
283+
if not saw_overlap:
284+
# Overlap is never guaranteed by the driver, so a miss is reported as an
285+
# expected failure rather than turning a busy GPU into a red CI run.
286+
pytest.xfail(
287+
"PDL (Programmatic Dependent Launch) overlap was not observed. "
288+
"If this keeps xfailing in CI, manually re-check on a quiet Hopper+ GPU."
289+
)
290+
291+
print(
292+
f"PDL (Programmatic Dependent Launch) overlap verified on {dev.name} compute capability {dev.compute_capability}",
293+
flush=True,
294+
)
295+
296+
186297
def test_launch_config_cluster_accepts_hopper_cc(monkeypatch):
187298
"""LaunchConfig accepts ``cluster`` when the device reports compute
188299
capability >= 9.0. Device is mocked so the cluster-cast branch runs on any

cuda_core/tests/test_object_protocols.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,8 @@ def sample_switch_node_alt(sample_graphdef):
684684
(
685685
"sample_launch_config",
686686
r"LaunchConfig\(grid=\(\d+, \d+, \d+\), cluster=.+, block=\(\d+, \d+, \d+\), "
687-
r"shmem_size=\d+, is_cooperative=(?:True|False)\)",
687+
r"shmem_size=\d+, is_cooperative=(?:True|False), "
688+
r"programmatic_stream_serialization=(?:True|False)\)",
688689
),
689690
("sample_kernel", r"<Kernel handle=0x[0-9a-f]+>"),
690691
# ObjectCode variations (by code_type)

0 commit comments

Comments
 (0)