Skip to content
Merged
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
4 changes: 3 additions & 1 deletion cuda_core/cuda/core/_memory/_virtual_memory_resource.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,9 @@ cdef VirtualMemoryIPCBufferDescriptor _export_ipc_descriptor(Buffer buf):
try:
handle = IPCAllocationHandle._init(fd, None)
except: # noqa: E722 rollback-then-raise: the fd is not owned yet
os.close(fd)
# nvbug 6977893: an Orin driver can return a negative fd despite success.
if fd >= 0:
os.close(fd)
raise
# The handle owns the fd now; if append fails, the handle closes it once.
handles.append(handle)
Expand Down
113 changes: 67 additions & 46 deletions cuda_core/tests/memory_ipc/test_vmm_ipc.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""

import ctypes
import errno
import gc
import json
import multiprocessing as mp
Expand All @@ -20,6 +21,7 @@
import pickle
import socket
import threading
from contextlib import ExitStack

import pytest
from helpers.child_processes import child_timeout_sec, kill_subprocesses, noop, run_child, warmup_multiprocessing
Expand Down Expand Up @@ -700,66 +702,85 @@ def child_main(device_id, to_child, from_child):
buffer.close()


class _OrinIPCExportFDExhaustionError(Exception):
"""nvbug 6977893: Orin export reports success with an invalid -EMFILE fd."""


class TestFileDescriptorLimit:
@pytest.mark.agent_authored(model="claude-fable-5-1")
@pytest.mark.thread_unsafe(reason="changes the process-wide file descriptor limit")
def test_export_near_the_limit(self, vmm_ipc_device):
def test_export_near_the_limit(self, vmm_ipc_device, request):
"""Under a lowered RLIMIT_NOFILE an export either succeeds or fails with a readable error and no leak."""
import resource # POSIX-only; a module-level import breaks collection on Windows

device = vmm_ipc_device
is_orin = device.name == "Orin"
xfail_reason = "Workaround for nvbug 6977893: Orin IPC export mishandles FD exhaustion"
if is_orin:
request.node.add_marker(
pytest.mark.xfail(reason=xfail_reason, raises=_OrinIPCExportFDExhaustionError, strict=False)
)
orin_export_bug = False
stream = device.default_stream
mr = _resource(device)
first = mr.allocate(1)
gran = first.size
second = mr.modify_allocation(first, 2 * gran)
buf = mr.modify_allocation(second, 3 * gran)
values = (0x61, 0x62, 0x63)
for i, value in enumerate(values):
_fill(buf, value, offset=i * gran, size=gran)
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
fillers = []
imported = desc = None
try:
# Fill the holes in the descriptor table so that the limit alone decides how many more fit.
highest = max(int(name) for name in os.listdir("/proc/self/fd"))
while True:
fd = os.open(os.devnull, os.O_RDONLY)
if fd > highest:
os.close(fd)
break
fillers.append(fd)
baseline = _settled_open_fds()
# Room for two more descriptors: a three-allocation export cannot complete.
resource.setrlimit(resource.RLIMIT_NOFILE, (highest + 1 + 2, hard))
failure = None
with ExitStack() as buffers:
first = buffers.enter_context(mr.allocate(1))
gran = first.size
second = buffers.enter_context(mr.modify_allocation(first, 2 * gran))
buf = buffers.enter_context(mr.modify_allocation(second, 3 * gran))
values = (0x61, 0x62, 0x63)
for i, value in enumerate(values):
_fill(buf, value, offset=i * gran, size=gran)
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
fillers = []
desc = None
try:
# Fill the holes in the descriptor table so that the limit alone decides how many more fit.
highest = max(int(name) for name in os.listdir("/proc/self/fd"))
while True:
fd = os.open(os.devnull, os.O_RDONLY)
if fd > highest:
os.close(fd)
break
fillers.append(fd)
baseline = _settled_open_fds()
# Room for two more descriptors: a three-allocation export cannot complete.
resource.setrlimit(resource.RLIMIT_NOFILE, (highest + 1 + 2, hard))
export_failed = False
try:
desc = buf.ipc_descriptor
except CUDAError as exc:
if not _has_note(exc, "file descriptor limit"):
raise
export_failed = True
except ValueError as exc:
if not is_orin or str(exc) != (
f"Invalid allocation handle (fd) {-errno.EMFILE}: must be non-negative"
):
raise
orin_export_bug = export_failed = True
if export_failed:
assert _open_fds() == baseline, "a failed export must close the descriptors it already took"
else:
# Acceptable only if the driver did not need one descriptor per allocation.
assert len(desc.fds) == 3
desc = None
gc.collect()
# Room for the three descriptors and the listing: export and import both succeed.
resource.setrlimit(resource.RLIMIT_NOFILE, (highest + 1 + 6, hard))
desc = buf.ipc_descriptor
except CUDAError as exc:
failure = exc
if failure is not None:
assert _has_note(failure, "file descriptor limit")
assert _open_fds() == baseline, "a failed export must close the descriptors it already took"
else:
# Acceptable only if the driver did not need one descriptor per allocation.
assert len(desc.fds) == 3
imported = buffers.enter_context(Buffer.from_ipc_descriptor(mr, desc, stream=stream))
assert _open_fds() == baseline + 3, "the import must not take file descriptors of its own"
finally:
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
for fd in fillers:
os.close(fd)
desc = None
gc.collect()
# Room for the three descriptors and the listing: export and import both succeed.
resource.setrlimit(resource.RLIMIT_NOFILE, (highest + 1 + 6, hard))
desc = buf.ipc_descriptor
assert len(desc.fds) == 3
imported = Buffer.from_ipc_descriptor(mr, desc, stream=stream)
assert _open_fds() == baseline + 3, "the import must not take file descriptors of its own"
finally:
resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
for fd in fillers:
os.close(fd)
assert [_read(imported, i * gran, 16) for i in range(3)] == [bytes([value]) * 16 for value in values]
del desc
gc.collect()
for buffer in (imported, buf, second, first):
buffer.close()
assert [_read(imported, i * gran, 16) for i in range(3)] == [bytes([value]) * 16 for value in values]
if orin_export_bug:
raise _OrinIPCExportFDExhaustionError(xfail_reason)


class TestRawFileDescriptorTransport:
Expand Down
Loading