11# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22# SPDX-License-Identifier: Apache-2.0
33
4+ import functools
45import multiprocessing
56import os
67import pathlib
@@ -91,6 +92,75 @@ def xfail_if_mempool_oom(err_or_exc, api_name=None, device=0):
9192 sys .path .insert (0 , test_helpers_root )
9293
9394
95+ def pytest_configure (config ):
96+ # When using `parallel-threads` set up mini-plugin to ensure each thread has a CUDA context
97+ parallel_threads = getattr (config .option , "parallel_threads" , 0 )
98+ if parallel_threads == "auto" or int (parallel_threads ) > 1 :
99+ config .pluginmanager .register (_CudaCoreParallelPlugin (), name = "_cuda_core_parallel_plugin" )
100+
101+
102+ @contextmanager
103+ def _init_cuda_context ():
104+ # TODO: rename this to e.g. init_context
105+ device = Device (0 )
106+ device .set_current ()
107+
108+ # Set option to avoid spin-waiting on synchronization.
109+ if int (os .environ .get ("CUDA_CORE_TEST_BLOCKING_SYNC" , 0 )) != 0 :
110+ handle_return (
111+ driver .cuDevicePrimaryCtxSetFlags (device .device_id , driver .CUctx_flags .CU_CTX_SCHED_BLOCKING_SYNC )
112+ )
113+
114+ try :
115+ yield device
116+ finally :
117+ _ = _device_unset_current ()
118+
119+
120+ def _wrap_worker_cuda_test (func ):
121+ if getattr (func , "_cuda_core_worker_cuda_wrapped" , False ):
122+ return func
123+
124+ @functools .wraps (func )
125+ def wrapper (* args , ** kwargs ):
126+ kwargs = dict (kwargs ) # copy before mutating
127+ with _init_cuda_context () as device :
128+ if "init_cuda" in kwargs :
129+ kwargs ["init_cuda" ] = device
130+ if "mempool_device_x2" in kwargs :
131+ kwargs ["mempool_device_x2" ] = _mempool_device_impl (2 )
132+ if "mempool_device_x3" in kwargs :
133+ kwargs ["mempool_device_x3" ] = _mempool_device_impl (3 )
134+ return func (* args , ** kwargs )
135+
136+ wrapper ._cuda_core_worker_cuda_wrapped = True
137+ return wrapper
138+
139+
140+ def _item_uses_init_cuda (item ):
141+ return "init_cuda" in getattr (item , "fixturenames" , ())
142+
143+
144+ class _CudaCoreParallelPlugin :
145+ """A mini pytest plugin used only for pytest-run-parallel testing.
146+ pytest-run-parallel spawns new threads for each test and we need to
147+ initialize and pass the correct CUDA context for each these.
148+
149+ This plugin looks for context specific fixtures and replaces them
150+ new context specific fixtures may have to be added.
151+
152+ This plugin approach is not ideal, it would be nicer to introduce hooks
153+ into pytest-run-parallel. Once that issue is closed this would be good
154+ to refactor: https://github.com/Quansight-Labs/pytest-run-parallel/issues/189
155+ """
156+
157+ @pytest .hookimpl (tryfirst = True )
158+ def pytest_collection_modifyitems (self , config , items ):
159+ for item in items :
160+ if _item_uses_init_cuda (item ):
161+ item .obj = _wrap_worker_cuda_test (item .obj )
162+
163+
94164def skip_if_pinned_memory_unsupported (device ):
95165 try :
96166 if not device .properties .host_memory_pools_supported :
@@ -194,18 +264,8 @@ def session_setup():
194264
195265@pytest .fixture
196266def init_cuda ():
197- # TODO: rename this to e.g. init_context
198- device = Device (0 )
199- device .set_current ()
200-
201- # Set option to avoid spin-waiting on synchronization.
202- if int (os .environ .get ("CUDA_CORE_TEST_BLOCKING_SYNC" , 0 )) != 0 :
203- handle_return (
204- driver .cuDevicePrimaryCtxSetFlags (device .device_id , driver .CUctx_flags .CU_CTX_SCHED_BLOCKING_SYNC )
205- )
206-
207- yield device
208- _ = _device_unset_current ()
267+ with _init_cuda_context () as device :
268+ yield device
209269
210270
211271def _device_unset_current () -> bool :
@@ -247,7 +307,7 @@ def pop_all_contexts():
247307
248308
249309@pytest .fixture
250- def ipc_device ():
310+ def ipc_device (init_cuda ):
251311 """Obtains a device suitable for IPC-enabled mempool tests, or skips.
252312
253313 The fixture also tracks every ``multiprocessing.Process`` spawned during
@@ -257,8 +317,7 @@ def ipc_device():
257317 """
258318 from helpers .child_processes import track_child_processes
259319
260- device = Device (0 )
261- device .set_current ()
320+ device = init_cuda
262321
263322 if not device .properties .memory_pools_supported :
264323 pytest .skip ("Device does not support mempool operations" )
@@ -296,10 +355,9 @@ def ipc_memory_resource(request, ipc_device):
296355
297356
298357@pytest .fixture
299- def mempool_device ():
358+ def mempool_device (init_cuda ):
300359 """Obtains a device suitable for mempool tests, or skips."""
301- device = Device (0 )
302- device .set_current ()
360+ device = init_cuda
303361
304362 if not device .properties .memory_pools_supported :
305363 pytest .skip ("Device does not support mempool operations" )
@@ -326,13 +384,13 @@ def _mempool_device_impl(num):
326384
327385
328386@pytest .fixture
329- def mempool_device_x2 ():
387+ def mempool_device_x2 (init_cuda ):
330388 """Fixture that provides two devices if available, otherwise skips test."""
331389 return _mempool_device_impl (2 )
332390
333391
334392@pytest .fixture
335- def mempool_device_x3 ():
393+ def mempool_device_x3 (init_cuda ):
336394 """Fixture that provides three devices if available, otherwise skips test."""
337395 return _mempool_device_impl (3 )
338396
0 commit comments