You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG]: Intermittent CUDA_ERROR_OUT_OF_MEMORY cascade in cuda_core test suite #2381
When running the full cuda_core v1.1.0 test suite (python -m pytest .) repeatedly across several Python environments on a single GPU, some of the runs intermittently fails with a large, identical cascade of failures.
Most of the failures are about CUDA_ERROR_OUT_OF_MEMORY.
This is similar to internal bug 5815123.
Observations:
The failures are random when tests with Python 3.10/3.11/3.12/3.13/3.14/3.14t.
When testing a single failed case, it could pass. If I added a sleep 2 or 3 seconds hook between the two test cases, mostly it could pass.
Attached the logs for several runs without sleeping.
Each CUDA memory pool reserves virtual address space at creation. Reservations are address space only — no physical memory is committed — but the per-process VA budget is bounded, and on Windows (notably MCDM) that bound is roughly 1 TB rather than the 47+ bits available on Linux.
Three factors combine:
1. Pools created without max_size get a system-dependent reservation. That default scales with device memory and lands in the hundreds of gigabytes on large-memory GPUs. Most cuda.core tests pass max_size=2097152 and are harmless; the ones that pass options without setting it (the IPC tests, primarily) reserve a full-size window for a pool that holds kilobytes. The cuda.bindings mempool tests construct pool properties by hand and leave maxSize at zero, with the same effect.
2. Pool destruction is not synchronous with respect to address-space release. Teardown blocks on the stream-ordered frees of the pool's outstanding allocations, which are themselves queued behind pending GPU work. Dropping the Python object marks the pool dead but does not return its reservation.
3. Test boundaries don't force that drain. Fixtures release memory resources and move on, so reservations from prior tests remain live while the next test creates its own. Over a session these accumulate until a creation — or a lazy default-pool lookup — fails with CUDA_ERROR_OUT_OF_MEMORY on a device with essentially all of its memory free.
Observed symptoms follow directly
Randomized ordering (pytest-randomly) determines which test is holding reservations when the budget is exhausted, so the failure migrates between runs. Isolated reruns pass because the process starts clean. A 2–3 second inter-test sleep mostly fixes it because it lets queued work and pending frees retire, at which point the abandoned pools actually tear down. Failures surface in whichever test asks next, which is why they cluster in graph and mempool tests that allocate only a few bytes — the allocation size is never the issue.
Scope
This accounts for the failures reported here. A few of them are tests asserting a specific error (e.g. "not supported") that instead receive the OOM, so they are masked rather than caused by their own logic. Note also that retry logic on the default device-pool lookup alone is insufficient: explicit pool creation, graph-pool initialization, and pinned/managed lookup fail identically.
Suggested fixes
Audit every pool construction in both test suites for a missing max_size and set an explicit small cap wherever the test doesn't specifically need the default.
Make the memory-resource fixtures release deterministically — synchronize before dropping the resource — so reservations don't leak across test boundaries.
Revised root cause thesis: two driver-managed memory pools, each permanently reserving 2x device memory of virtual address space
Measured by walking the process address space with Windows VirtualQuery (exact, non-invasive)
and driving the driver API directly, so cuda.core is not in the path. On a 23.89 GiB device:
Driver call
VA reserved
Reclaimable?
cuDeviceGetMemPool
+47.78 GiB (2.00x device mem)
never (process lifetime)
cuMemAllocFromPoolAsync (first alloc)
0
—
cuGraphAddMemAllocNode
+47.78 GiB (2.00x device mem)
never — not by cuGraphDestroy, cuGraphExecDestroy, or cuDeviceGraphMemTrim
cuMemPoolCreate(maxSize=0)
+47.78 GiB (2.00x)
yes, on cuMemPoolDestroy
cuMemPoolCreate(maxSize=2 MiB)
+1.00 GiB
yes, on cuMemPoolDestroy
Key points:
Merely querying the default pool costs 2x device memory.cuDeviceGetMemPool makes the
reservation; the subsequent allocation is free. This is exactly the call that fails in the
captured diagnostics (cuDeviceGetMemPool(dev 0) -> CUDA_ERROR_OUT_OF_MEMORY) and behind the DeviceMemoryResource(dev) line where every failing run first dies.
The graph memory pool is a second, distinct driver-managed pool, reserved at node creation
time and never released. It is per-device and shared: adding alloc nodes to a 2nd and 3rd graph
reserved 0 additional bytes.
Neither pool can be capped or substituted.cuGraphAddMemAllocNode takes no pool handle —
only a CUmemPoolPropsdescription. Setting poolProps.maxSize = 2 MiB is accepted and reads
back correctly via cuGraphMemAllocNodeGetParams, but the reservation is still 2.00x device
memory. The maxSize cap that works for cuMemPoolCreate is ignored here.
Note that cudaDeviceGraphMemTrim operates on a different pool from cudaMemPoolTrimTo(). The graph memory pool is not exposed to the stream ordered memory allocator. [...] cudaDeviceGraphMemTrim will unmap and release any physical memory reserved by graph memory
nodes that is not actively in use. [...] cudaGraphMemAttrReservedMemCurrent returns the amount
of physical memory reserved by the driver for graph allocations.
Same guide, on when the address is assigned:
CUDA assigns virtual addresses for the graph allocation at node creation time. Driver API, cuDeviceGraphMemTrim:
Blocks which are not in use by a graph that is either currently executing or scheduled to execute
are freed back to the operating system. Driver API, cuMemAllocFromPoolAsync
— stream capture is not a back door either:
During stream capture, this function results in the creation of an allocation node. In this case, the allocation is owned by the graph instead of the memory pool. The memory pool's properties
are used to set the node's creation parameters.
Why this explains the intermittent cascade
On the failing machine (178.81 GiB device), each reservation is 357.6 GiB; the two together are 715 GiB, on top of the address space consumed by context initialization. Against the bounded
per-process VA budget on Windows MCDM this leaves essentially no margin, so whether the second
reservation finds a contiguous hole depends on test order and fragmentation — which is exactly the
observed ~50% failure rate. It also explains the failure point: the cascade always begins at the
first test needing the default pool after the graph pool has taken its 357.6 GiB.
Note: cuda.core is not doing anything wrong here, and capping pools the tests create (#2484) was
correct but addresses a smaller, separate cost.
Possible workaround: materialize both pools at session start
Total consumption is fixed and unavoidable, but when it happens is not. Both reservations are
one-time-per-process, so a session-scoped autouse fixture that does one tiny allocation from the
device default pool and creates one throwaway graph mem-alloc node would force both 2x windows to be
reserved back-to-back into a nearly empty, unfragmented address space, before test order can
interfere.
This does not reduce the footprint; it makes the outcome deterministic. It is directly falsifiable:
if the failures are caused by the second reservation failing to find a contiguous hole, this should
convert an intermittent failure into a consistent pass or a consistent, immediate failure at session
start, which would be equally informative.
On Windows, bottom-up ASLR randomizes where VirtualAlloc placements start
on each launch. This carves the free address space into differently sized
pieces. With a ~1 TB budget and two 357.6 GiB reservations to place, whether
enough sufficiently wide address ranges exist depends on where the carving lands —
which is why the same suite passes on one launch and fails on the next, uncorrelated
with Python version or test order.
Disabling bottom-up ASLR (Set-ProcessMitigation -Name python.exe -Disable BottomUp,HighEntropy) removes the carving and makes the suite deterministically
pass: 12 of 12 sessions clean across two full runs, versus ~50% failure rate
with ASLR on.
Activity
Andy-Jost commentedon Aug 3, 2026
Root cause
Each CUDA memory pool reserves virtual address space at creation. Reservations are address space only — no physical memory is committed — but the per-process VA budget is bounded, and on Windows (notably MCDM) that bound is roughly 1 TB rather than the 47+ bits available on Linux.
Three factors combine:
1. Pools created without
max_sizeget a system-dependent reservation. That default scales with device memory and lands in the hundreds of gigabytes on large-memory GPUs. Mostcuda.coretests passmax_size=2097152and are harmless; the ones that pass options without setting it (the IPC tests, primarily) reserve a full-size window for a pool that holds kilobytes. Thecuda.bindingsmempool tests construct pool properties by hand and leavemaxSizeat zero, with the same effect.2. Pool destruction is not synchronous with respect to address-space release. Teardown blocks on the stream-ordered frees of the pool's outstanding allocations, which are themselves queued behind pending GPU work. Dropping the Python object marks the pool dead but does not return its reservation.
3. Test boundaries don't force that drain. Fixtures release memory resources and move on, so reservations from prior tests remain live while the next test creates its own. Over a session these accumulate until a creation — or a lazy default-pool lookup — fails with
CUDA_ERROR_OUT_OF_MEMORYon a device with essentially all of its memory free.Observed symptoms follow directly
Randomized ordering (
pytest-randomly) determines which test is holding reservations when the budget is exhausted, so the failure migrates between runs. Isolated reruns pass because the process starts clean. A 2–3 second inter-test sleep mostly fixes it because it lets queued work and pending frees retire, at which point the abandoned pools actually tear down. Failures surface in whichever test asks next, which is why they cluster in graph and mempool tests that allocate only a few bytes — the allocation size is never the issue.Scope
This accounts for the failures reported here. A few of them are tests asserting a specific error (e.g. "not supported") that instead receive the OOM, so they are masked rather than caused by their own logic. Note also that retry logic on the default device-pool lookup alone is insufficient: explicit pool creation, graph-pool initialization, and pinned/managed lookup fail identically.
Suggested fixes
max_sizeand set an explicit small cap wherever the test doesn't specifically need the default.juenglin commentedon Aug 6, 2026
Revised root cause thesis: two driver-managed memory pools, each permanently reserving 2x device memory of virtual address space
Measured by walking the process address space with Windows
VirtualQuery(exact, non-invasive)and driving the driver API directly, so
cuda.coreis not in the path. On a 23.89 GiB device:cuDeviceGetMemPoolcuMemAllocFromPoolAsync(first alloc)cuGraphAddMemAllocNodecuGraphDestroy,cuGraphExecDestroy, orcuDeviceGraphMemTrimcuMemPoolCreate(maxSize=0)cuMemPoolDestroycuMemPoolCreate(maxSize=2 MiB)cuMemPoolDestroyKey points:
cuDeviceGetMemPoolmakes thereservation; the subsequent allocation is free. This is exactly the call that fails in the
captured diagnostics (
cuDeviceGetMemPool(dev 0) -> CUDA_ERROR_OUT_OF_MEMORY) and behind theDeviceMemoryResource(dev)line where every failing run first dies.time and never released. It is per-device and shared: adding alloc nodes to a 2nd and 3rd graph
reserved 0 additional bytes.
cuGraphAddMemAllocNodetakes no pool handle —only a
CUmemPoolPropsdescription. SettingpoolProps.maxSize = 2 MiBis accepted and readsback correctly via
cuGraphMemAllocNodeGetParams, but the reservation is still 2.00x devicememory. The
maxSizecap that works forcuMemPoolCreateis ignored here.Documentation references
CUDA Programming Guide 4.2.5.5, Physical Memory Footprint:
Same guide, on when the address is assigned:
Why this explains the intermittent cascade
On the failing machine (178.81 GiB device), each reservation is 357.6 GiB; the two together are
715 GiB, on top of the address space consumed by context initialization. Against the bounded
per-process VA budget on Windows MCDM this leaves essentially no margin, so whether the second
reservation finds a contiguous hole depends on test order and fragmentation — which is exactly the
observed ~50% failure rate. It also explains the failure point: the cascade always begins at the
first test needing the default pool after the graph pool has taken its 357.6 GiB.
Note:
cuda.coreis not doing anything wrong here, and capping pools the tests create (#2484) wascorrect but addresses a smaller, separate cost.
Possible workaround: materialize both pools at session start
Total consumption is fixed and unavoidable, but when it happens is not. Both reservations are
one-time-per-process, so a session-scoped autouse fixture that does one tiny allocation from the
device default pool and creates one throwaway graph mem-alloc node would force both 2x windows to be
reserved back-to-back into a nearly empty, unfragmented address space, before test order can
interfere.
This does not reduce the footprint; it makes the outcome deterministic. It is directly falsifiable:
if the failures are caused by the second reservation failing to find a contiguous hole, this should
convert an intermittent failure into a consistent pass or a consistent, immediate failure at session
start, which would be equally informative.
test(cuda_core): reserve the driver's two pool address windows at ses…
test(cuda_core): reserve the driver's two pool address windows at ses…
test(cuda_core): reserve the driver's two pool address windows at ses…
juenglin commentedon Aug 11, 2026
Update: ASLR explains the intermittency
On Windows, bottom-up ASLR randomizes where
VirtualAllocplacements starton each launch. This carves the free address space into differently sized
pieces. With a ~1 TB budget and two 357.6 GiB reservations to place, whether
enough sufficiently wide address ranges exist depends on where the carving lands —
which is why the same suite passes on one launch and fails on the next, uncorrelated
with Python version or test order.
Disabling bottom-up ASLR (
Set-ProcessMitigation -Name python.exe -Disable BottomUp,HighEntropy) removes the carving and makes the suite deterministicallypass: 12 of 12 sessions clean across two full runs, versus ~50% failure rate
with ASLR on.
juenglin commentedon Aug 12, 2026
@rluo8 confirmed that turning off ASLR on the Windows 2025 server prevents the intermittent failures with CUDA_ERROR_OUT_OF_MEMORY. Closing the issue.
rluo8 commentedon Aug 13, 2026
Thanks @juenglin .
test(cuda.core): freeze the collected heap once so the per-test gc.co…
Remove xfail_if_mempool_oom and xfail_on_graph_mempool_oom helpers (#…
test(cuda.core): count live owned pools and VA reservations in C++
test(cuda.core): gate the init_cuda teardown gc.collect() on live own…
test(cuda.core): pop the context when the leftover warning aborts ini…