From 219be4f72ae328fb23cddd4463e3f5317027c317 Mon Sep 17 00:00:00 2001 From: Andy Jost Date: Thu, 15 Oct 2026 14:54:48 -0700 Subject: [PATCH] test(cuda.core): cover the forkserver start method for VMM IPC buffers The forkserver is a fresh interpreter and children fork from it, so it is safe after CUDA initialization, and it is the Linux default since Python 3.14. Its Popen collects file descriptors during pickling and sends them afterwards, like spawn, so the Process-argument path in VirtualMemoryBuffer.__reduce__ has to work there too. Add a Process argument test and a Queue round trip under mp.get_context("forkserver"), skipped where that start method is unavailable. Closes #3111 Co-Authored-By: Claude Fable 5.1 --- cuda_core/tests/memory_ipc/test_vmm_ipc.py | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/cuda_core/tests/memory_ipc/test_vmm_ipc.py b/cuda_core/tests/memory_ipc/test_vmm_ipc.py index b42f1eb4623..3c824ebf556 100644 --- a/cuda_core/tests/memory_ipc/test_vmm_ipc.py +++ b/cuda_core/tests/memory_ipc/test_vmm_ipc.py @@ -649,6 +649,71 @@ def child_main(device_id, to_child, from_child): from_child.put(counts) +_FORKSERVER_UNAVAILABLE = "forkserver" not in mp.get_all_start_methods() + + +def _forkserver_child(device_id, buffer, report): + """Read the parent's pattern from the imported buffer, then write a new one.""" + device = Device(device_id) + device.set_current() + report.put(_read(buffer, 0, 16)) + _fill(buffer, 0x6C) + buffer.close() + + +@pytest.mark.skipif(_FORKSERVER_UNAVAILABLE, reason="the forkserver start method is not available on this platform") +class TestForkserver: + """The conftest forces ``spawn``; a forkserver context passes descriptors the same way and must work too.""" + + @pytest.mark.agent_authored(model="claude-sonnet-5-5") + def test_process_argument(self, vmm_ipc_device): + """A buffer passed as a forkserver Process argument imports in the child, and writes go both ways.""" + device = vmm_ipc_device + buf = _resource(device).allocate(1) + _fill(buf, 0x5D) + ctx = mp.get_context("forkserver") + report = ctx.Queue() + process = ctx.Process(target=_forkserver_child, args=(device.device_id, buf, report)) + process.start() + seen = report.get(timeout=CHILD_TIMEOUT_SEC) + process.join(timeout=CHILD_TIMEOUT_SEC) + survivors = kill_subprocesses(process) + assert not survivors, "child did not exit within timeout" + assert process.exitcode == 0, f"child exited with {process.exitcode}" + assert seen == b"\x5d" * 16 + assert _read(buf, 0, 16) == b"\x6c" * 16 + buf.close() + + @pytest.mark.agent_authored(model="claude-sonnet-5-5") + def test_queue_round_trip(self, vmm_ipc_device): + """A buffer sent through a forkserver Queue imports in the child, and writes go both ways.""" + device = vmm_ipc_device + buf = _resource(device).allocate(1) + _fill(buf, 0x5E) + ctx = mp.get_context("forkserver") + to_child, from_child = ctx.Queue(), ctx.Queue() + process = ctx.Process(target=self.child_main, args=(device.device_id, to_child, from_child)) + process.start() + to_child.put(buf) + seen = from_child.get(timeout=CHILD_TIMEOUT_SEC) + process.join(timeout=CHILD_TIMEOUT_SEC) + survivors = kill_subprocesses(process) + assert not survivors, "child did not exit within timeout" + assert process.exitcode == 0, f"child exited with {process.exitcode}" + assert seen == b"\x5e" * 16 + assert _read(buf, 0, 16) == b"\x6d" * 16 + buf.close() + + @staticmethod + def child_main(device_id, to_child, from_child): + device = Device(device_id) + device.set_current() + buffer = to_child.get(timeout=CHILD_TIMEOUT_SEC) + from_child.put(_read(buffer, 0, 16)) + _fill(buffer, 0x6D) + buffer.close() + + class TestFileDescriptorLimit: @pytest.mark.agent_authored(model="claude-fable-5-1") @pytest.mark.thread_unsafe(reason="changes the process-wide file descriptor limit")