Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[BUG]: Device allocator OOM instead of reclaiming freed-but-cached memory #6801

Copy link
Copy link

Description

@gabrieldemarmiesse
Issue body actions

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)
mojo oom_repro.mojo
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
Reactions are currently unavailable

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingSomething isn't workingmax

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    Morty Proxy This is a proxified and sanitized view of the page, visit original site.