Skip to content

[BUG] [CUDA] Illegal __global__ write in copy_s under CUDA graphs on consumer Blackwell (sm_120) → corrupted output #4700

Open

Description

@zhanpengwang2000

Summary

On an RTX 5070 Ti Laptop (Blackwell, sm_120, cc 12.0), MLX's CUDA backend
intermittently produces corrupted LLM output (fluent but off-task, runaway to
max tokens). compute-sanitizer memcheck pinpoints an out-of-bounds /
write-before-allocation in the scalar-broadcast copy kernel copy_s, launched
from a CUDA graph. The identical build is correct on RTX 3090 Ti (Ampere,
sm_86).

Environment

  • GPU: NVIDIA GeForce RTX 5070 Ti Laptop (sm_120, cc 12.0); driver 595.79; CUDA 13.x
  • MLX: <fill in your version, e.g. 0.32.x> (pip install mlx[cuda] or built with CMAKE_CUDA_ARCHITECTURES=120)
  • OS: Windows 11

compute-sanitizer memcheck (repeats continuously)

Invalid __global__ write of size 16 bytes
  at void mlx::core::cu::copy_s<float, float, unsigned int, (int)4>(const T1*, T2*, T3)+0x430
  by thread (0,0,0) in block (0,0,0)
  Access to 0x... is potentially made before memory is allocated
  and is inside the nearest allocation of size 128 bytes
  Saved host backtrace up to driver entry point at kernel launch time
    Host Frame: cuGraphLaunch in nvcuda64.dll
    Host Frame: mlx::core::cu::get_command_encoder in mlx.dll
    Host Frame: mlx::core::scheduler::Scheduler::notify_task_completion in mlx.dll
    Host Frame: mlx::core::eval in mlx.dll

Eventually escalates to a sticky cudaErrorUnknown (999) on
cudaEventSynchronize / cudaPeekAtLastError.

Analysis

  • copy_s (mlx/backend/cuda/copy/copy_contiguous.cu) is the scalar-broadcast
    copy; N_READS = 16/sizeof(float) = 4 → a 16-byte store_vector<4>.
  • CommandEncoder::insert_graph_dependencies (mlx/backend/cuda/device.cpp)
    derives graph edges only from input-buffer data dependencies via node_map_.
    A newly cudaMallocAsync'd output has no prior writer in node_map_, so the
    copy node is added with no in-graph dependency expressing "allocation
    complete"
    — ordering relies solely on same-stream semantics + the pool's
    reuse events.
  • get_graph_limits gives case 1200 // Consumer Blackwell a wide budget
    (ops=100), so many data-independent nodes run concurrently in one graph,
    a much wider timing window than Ampere — exposing the latent alloc→copy race
    on sm_120.

It is a timing/ordering race (not an index overrun)

All of the following make it disappear and give bit-correct output:

  • CUDA_LAUNCH_BLOCKING=1
  • attaching Nsight Systems
  • MLX_USE_CUDA_GRAPHS=0

Repro

Run the copy-heavy inference under
compute-sanitizer --tool memcheck --kernel-name kernel_substring=copy_s on an
sm_120 device; observe the copy_s "before memory is allocated" write.

Related

A fix PR follows.

Activity

  1. zcbenz commented on Oct 15, 2026

    @zcbenz
    Member

    This looks very likely caused by a temporary array not marked in graph with add_temproray, and with cuda graph parallelizing ops it becomes a write into unallocated memory. Do you have a simple way to reproduce this, like with mlx-lm?

  2. zhanpengwang2000 commented on Oct 19, 2026

    @zhanpengwang2000
    Author

    2. This looks very likely caused by a temporary array not marked in graph with add_temproray, and with cuda graph parallelizing ops it becomes a write into unallocated memory. Do you have a simple way to reproduce this, like with mlx-lm?

    @zcbenz Thanks — that matches our evidence exactly. The faulting kernel is copy_s (scalar-broadcast copy) writing a ~128B buffer “before memory is allocated”, from inside a CUDA graph, only on sm_120 (fine on sm_86). Your “temporary not registered via add_temporary, then parallelized inside the graph → write into unallocated memory” is almost certainly the cause: insert_graph_dependencies only adds edges for tracked buffers, so an untracked temporary gets no dependency and a concurrent node can write it before it is live.

    Repro with mlx-lm (sm_120, CUDA graphs ON, long generation — it’s intermittent, so a long/repeated run surfaces it). The faulting path is the int8 / affine-q8 quantized matmul, so use an 8-bit model:

    pip install mlx-lm
    compute-sanitizer --tool memcheck --kernel-name kernel_substring=copy_s
    python -m mlx_lm generate
    --model <an mlx-community *-8bit model>
    --prompt "Write a very long story." --max-tokens 2048 --temp 0.9

    Expected: Invalid global write of size 16 bytes at ... copy_s<...> ... Access ... made before memory is allocated ... nearest allocation 128 bytes, host backtrace through cuGraphLaunch. It disappears under CUDA_LAUNCH_BLOCKING=1, nsys, or MLX_USE_CUDA_GRAPHS=0.

    — Zhanpeng Wang

  3. zcbenz commented on Oct 20, 2026

    @zcbenz
    Member

    I added a debug check in #4711, which will help find out the bug before launching the kernel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions