Bug description
When a device allocation cannot be satisfied, the buffer cache does not reclaim memory that has already been freed by the program, it fails immediately. If the frees are still pending behind in-flight kernels (the normal state of an async training loop, where the host enqueues ahead of theGPU), an allocation request OOMs even though almost the entire GPU is logically free. What PyTorch's CUDA caching allocator does on OOM ( synchronize,release cached blocks back to the driver, retry) would make these allocations succeed with no configuration.
Hit in training GPT-2 124M in eager mode via a PyTorch backend built on mojo (H100 80GB): the training aborts within ~70 optimizer steps once the transient [batch*seq, vocab] fp32 tensors (6–13 GB) stop fitting. Real-run
failure dump (nightly 2026072306): a 11.9 GB request fails physical allocation while the VMM arena shows 127 GB of free VA:
oom log request=11.90GB reason="device driver memAlloc failed: CUDA call failed: CUDA_ERROR_OUT_OF_MEMORY (out of memory)
oom log chunk=0 base=0x302000000 size=286GB in_use_blocks=619 in_use=39.87GB free_blocks=83 free=246.13GB largest_free=127.03GB
Steps to reproduce
Interleave big transient buffers with small long-lived ones (a training-heap shape), keep the stream busy with kernels reading the big buffers, free the big buffers (their stream-ordered frees stay pending behind the kernels), and request one 8 GiB buffer:
from std.gpu import block_dim, block_idx, grid_dim, thread_idx
from std.gpu.host import DeviceBuffer, DeviceContext
@__name("oom_repro_spin")
def _spin_kernel(
ptr: UnsafePointer[Scalar[DType.float32], MutAnyOrigin],
count: Int,
rounds: Int,
):
var i = Int(block_idx.x) * Int(block_dim.x) + Int(thread_idx.x)
var stride = Int(grid_dim.x) * Int(block_dim.x)
while i < count:
var acc = ptr[i]
for _ in range(rounds):
acc = acc * 1.000000119 + 0.25
ptr[i] = acc
i += stride
def main() raises:
comptime GiB = 1024 * 1024 * 1024
comptime MiB = 1024 * 1024
with DeviceContext() as ctx:
# Training-heap shape: big transients interleaved with small
# long-lived tensors.
var keep = List[DeviceBuffer[DType.uint8]]()
var drop = List[DeviceBuffer[DType.float32]]()
var i = 0
try:
while True:
drop.append(
ctx.enqueue_create_buffer[DType.float32](GiB // 4)
)
keep.append(ctx.enqueue_create_buffer[DType.uint8](MiB))
i += 1
except e:
_ = e
pass
print("pairs before first OOM:", i)
# Keep the stream busy with kernels READING the big buffers, then
# free them: the stream-ordered frees stay pending behind the
# kernels, exactly like async training where the host runs ahead.
for j in range(len(drop)):
ctx.enqueue_function[_spin_kernel](
drop[j].unsafe_ptr().as_unsafe_any_origin(),
GiB // 4,
2000,
grid_dim=(1024,),
block_dim=(256,),
)
var freed = len(drop)
drop.clear() # frees enqueue behind the spin kernels — all pending
# Request 8 GiB NOW, while every free is still in flight.
try:
var big = ctx.enqueue_create_buffer[DType.uint8](8 * GiB)
ctx.synchronize()
print("OK: 8 GiB allocation succeeded with frees pending")
_ = big^
except e2:
print(
"BUG: 8 GiB allocation failed;",
freed,
"x 1 GiB freed but pending behind in-flight kernels",
)
print("error:", e2)
ctx.synchronize()
_ = len(keep)
pairs before first OOM: 78
BUG: 8 GiB allocation failed; 78 x 1 GiB freed but pending behind in-flight kernels
error: At ...device_context.mojo:...: CUDA call failed: CUDA_ERROR_OUT_OF_MEMORY (out of memory)
Workaround
Capping the buffer cache leaves the driver enough un-hoarded memory for the
large requests and fully avoids the failure:
MODULAR_DEVICE_CONTEXT_MEMORY_MANAGER_SIZE=34359738368 # 32 GiB
But it slows down the allocation.
System information
- MAX/Mojo nightlies `26.5.0.dev2026072306` / `1.0.0b3.dev2026072306`, Linux x86-64
- NVIDIA H100 PCIe 80 GB, driver 580.126.20
Bug description
When a device allocation cannot be satisfied, the buffer cache does not reclaim memory that has already been freed by the program, it fails immediately. If the frees are still pending behind in-flight kernels (the normal state of an async training loop, where the host enqueues ahead of theGPU), an allocation request OOMs even though almost the entire GPU is logically free. What PyTorch's CUDA caching allocator does on OOM ( synchronize,release cached blocks back to the driver, retry) would make these allocations succeed with no configuration.
Hit in training GPT-2 124M in eager mode via a PyTorch backend built on mojo (H100 80GB): the training aborts within ~70 optimizer steps once the transient
[batch*seq, vocab]fp32 tensors (6–13 GB) stop fitting. Real-runfailure dump (nightly
2026072306): a 11.9 GB request fails physical allocation while the VMM arena shows 127 GB of free VA:Steps to reproduce
Interleave big transient buffers with small long-lived ones (a training-heap shape), keep the stream busy with kernels reading the big buffers, free the big buffers (their stream-ordered frees stay pending behind the kernels), and request one 8 GiB buffer:
Workaround
Capping the buffer cache leaves the driver enough un-hoarded memory for the
large requests and fully avoids the failure:
But it slows down the allocation.
System information