Skip to content

Bug: ggml_set_rows faults on HIP/ROCm after many reuses of a cached single-token graph (Strix Halo / gfx1151) #516

Description

@howard0su

Environment

  • GPU: AMD Strix Halo, gfx1151, wavefront size 32
  • Backend: ggml-hip (ROCm), built with -DGGML_HIP=ON, AMDGPU_TARGETS=gfx1151
  • llama.cpp / ggml pinned at commit 53859c2
  • Kernel: ggml/src/ggml-cuda/set-rows.cu (k_set_rows / k_set_rows_quant), compiled through HIP
  • CUDA (same graph, same code path) does not fault.

Symptom

  • We build a cached single-token attention/decode graph once and re-execute it every
    decode step via ggml_backend_graph_compute, updating only input tensors (row
    indices, positions, embeddings) between runs.
  • The graph contains several ggml_set_rows nodes that write one row into persistent
    state / KV ring buffers each step:
    • KV compressor state: set_rows(state_kv, kv_cur, state_rows) and
      set_rows(state_score, sc_cur, state_rows)
    • Raw SWA KV ring: set_rows(raw_kv, kv_f32, raw_kv_rows)
    • Compressed cache flush: set_rows(comp_cache, pooled, comp_rows)
  • On HIP, after enough tokens (the destination row index advances / wraps the ring
    buffer), the compute faults (GPU memory access fault / incorrect writes). The same
    path is stable on CUDA and stable on HIP when we rebuild the graph dynamically each
    token.

What changes across reuses (the only moving parts)

  • The src1 index tensor of set_rows (state_rows, raw_kv_rows, comp_rows) is
    updated each step via ggml_backend_tensor_set with a new int64 destination row
    (e.g. kv_start % n_swa, token_pos / ratio, (ratio + pos_mod)), so
    dst_row = *(src1 + ...) reads a different offset in the destination buffer each run.
  • Destination tensors (state_kv, state_score, raw_kv, comp_cache) are persistent
    buffers that are written in-place and then read by later ops in the same graph.

Suspected root cause (for AMD to confirm)

The cached graph is allocated once and re-run; only leaf input tensors change. Two
candidate issues on the HIP backend:

  1. set_rows writing in-place into a persistent destination that is also consumed
    downstream in the same reused graph may expose a missing dependency / synchronization
    or aliasing assumption that only manifests on the HIP scheduler (RDNA3.5, wave32),
    not on CUDA.
  2. The set-rows.cu kernel uses int64 index math and
    blockDim.x * blockIdx.x + threadIdx.x; on gfx1151 (wave size 32, which we must
    force via -DDFLASH_WAVE_SIZE=32) there may be a launch-config / index-overflow /
    write-out-of-bounds edge when dst_row points into a high offset of the ring buffer.

Reproduction sketch

  1. Build a small graph: a persistent 2D buffer dst (e.g. [width, n_rows]), one
    set_rows(dst, src_row, idx) node, then an op that reads dst.
  2. Allocate once; run graph_compute in a loop, changing only idx (the int64
    destination row) each iteration to sweep across all rows / wrap a ring.
  3. On HIP/gfx1151 this should reproduce the fault / incorrect writes after N iterations;
    on CUDA it stays correct.

Current workaround (in our code)

  • We disabled reuse of the cached single-token decode graph on HIP only and fall back to
    rebuilding the graph dynamically each token. Non-HIP backends keep graph reuse.
  • Detection: ggml_backend_name(backend) contains "HIP" / "ROCm".
  • Reference:
    • server/src/deepseek4/deepseek4_graph.cpp:3250
      (reuse_decode_attn = n_tokens == 1 && !ds4_backend_is_hip(backend))
    • set_rows sites: deepseek4_graph.cpp:655-656, :744, :1009

Questions

  1. Is re-executing a pre-allocated ggml graph containing in-place ggml_set_rows into a
    persistent buffer (that is also read later in the same graph) expected to be safe on
    the HIP backend? Any required synchronization / allocator flags?
  2. Is there a known set-rows.cu issue on RDNA3.5 / wave32 (gfx1151) with int64 index
    writes into high offsets of a large destination?
  3. Does forcing DFLASH_WAVE_SIZE=32 interact with the CUDA-derived kernel launch
    geometry in a way that could cause out-of-bounds writes?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions