Repository navigation
Eval bug: llama-server hard crash (cublasSgemm INVALID_VALUE) with --spec-type draft-mtp under KV-cache saturation #26558
Description
Activity
Update: graph-reuse test result
LLAMA_GRAPH_REUSE_DISABLE=1does not prevent the crash. A server run with identical config (the 1024-context variant) but graph reuse disabled crashed on the identical failing op after ~51 min (vs ~21 min baseline). Same GEMM-FAIL signature:GEMM-FAIL: status=7 src0="blk.0.ssm_alpha.weight"(0) ne00=1024 ne01=16 nb00=4 nb01=4096 src1="attn_norm-0"(0) ne10=1024 ne11=16 nb10=4 nb11=4096 dst="node_43" ne0=16 | s01=1024 s11=1024 ne0=16 align0=0 align1=0 alignD=0 src0_ptr=0x7f0900d64100 src1_ptr=0x7f0934018300 dst_ptr=0x7f09340c8300So graph reuse (the
graphs reused = 21k-62kcounters) is not the corruption vector; the extended time-to-crash matches the server being proportionally slower without graph reuse. The crash is deterministic on the layer-0 linear-attentionssm_alphaprojection every time, always right after all slots are cleared via context-exceeded and fresh decodes start, always with a cuBLAS-valid parameter set — pointing to deterministic memory corruption (candidate: cuBLAS handle/workspace state) rather than a timing race.Next test being run: forcing
llama_synchronize(ctx_tgt)beforeMTP::process()to test the un-synchronized asynch_nextnread.Update: sync test result — does NOT prevent the crash
I rebuilt with
llama_synchronize(ctx_tgt)inserted incommon_speculative_impl_draft_mtp::process()immediately before thellama_get_embeddings_nextn(ctx_tgt)read (common/speculative.cpp:1450), to rule out the un-synchronized asynch_nextncopy (llama-context.cpp:2080has a commented-outsynchronize(); the copy isggml_backend_tensor_get_asyncatllama-context.cpp:2008).Result: still crashes, on the identical failing op, after ~57 min (vs ~21 min baseline):
GEMM-FAIL: status=7 src0="blk.0.ssm_alpha.weight"(0) ne00=1024 ne01=16 nb00=4 nb01=4096 src1="attn_norm-0"(0) ne10=1024 ne11=16 nb10=4 nb11=4096 dst="node_43" ne0=16 | s01=1024 s11=1024 ne0=16 align0=0 align1=0 alignD=0Combined test matrix (all 1024-ctx
--kv-unified -np 4config, same soak):variant time to crash failing op baseline ~21 min ssm_alpha × attn_normLLAMA_GRAPH_REUSE_DISABLE=1~51 min ssm_alpha × attn_normllama_synchronize(ctx_tgt)inMTP::process()~57 min ssm_alpha × attn_normBoth changes only delay the crash roughly in proportion to how much they slow the server down; neither removes it. So the crash is robust and is not caused by graph reuse and not caused by the unsynced async
h_nextnread. The deterministic same-op failure (always the first F32cublasSgemmin layer-0 linear attention, always immediately after all slots are cleared via context-exceeded, always with a cuBLAS-valid parameter set) points to a genuine deterministic memory corruption in the MTP dual-context path — e.g. a host-side buffer overflow or a use-after-free between the target and draft contexts` (separate CUDA streams sharing the device memory allocator) — rather than a timing race.The crash does not reproduce with
--spec-typeoff (same load), confirming it is specific to the MTP dual-context path.Update: root mechanism identified — the CUDA stream handed to cuBLAS is corrupted
Instrumented the crash site with probes that dump, at the exact failing call:
cudaPeekAtLastError()(prior async CUDA error),cublasGetStream()(handle alive?), and the stream pointer stored in the handle. All three independent crashes (1024-ctx, 256-ctx, and an ASAN build) show the identical pattern:GEMM-FAIL: status=7 src0="blk.0.ssm_alpha.weight" ... src1="attn_norm-0" ... | handle=0x624c2c2262b0 stream=0x624c1f0e6fc0 ce_peek=0(no error) cublasGetStream=0 stream_after=0x624c1f0e6fc0ce_peek=0→ no prior device kernel error; the stream is not poisoned (rules out a crashed kernel).cublasGetStreamreturns success → the cuBLAS handle struct itself is intact (rules out handle corruption).- BUT the stream pointer stored in the handle (
0x624c1f0e6fc0,0x5b27815f1000,0x633347fcd1d0) is a host-process-heap address, not a CUDA driver stream handle (valid ones are0x7f...-range driver pointers on this system). cuBLAS launches the GEMM on this bogus stream →cudaErrorInvalidValue→CUBLAS_STATUS_INVALID_VALUE.
Since
cublasSetStream(handle, ctx.stream())runs immediately before every GEMM (ggml-cuda.cu:1419), the garbage value almost certainly comes fromctx.stream()— i.e. thestreams[device][stream]array inside theggml_backend_cuda_contextstruct itself has been overwritten by a host-side write (a heap pointer value landed in the stream slot). That struct also owns the buffer pool and the CUDA-graph cache — consistent with the observed unbounded GPU memory growth (a 0.8B/1024-ctx server using 7.7 GiB, growing over time; reporter observed total GPU footprint creeping 20→23 GiB).An ASAN build reproduced the identical crash with no AddressSanitizer report — so the write is either into memory ASAN does not track (the cuBLAS handle's internals) or a same-size pointer write (not a byte-overrun). Pinning the exact writer is the next step; the MTP dual-context setup (separate CUDA streams, two interleaved decode passes) remains the only configuration that triggers it.
Update: CUDA graphs confirmed as the root cause of both the GPU memory leak and the crash
Running with
GGML_CUDA_DISABLE_GRAPHS=1(which disables the backend-level CUDA graph capture, https://github.com/ggml-org/llama.cpp/blob/6c8dcaa7ae/ggml/src/ggml-cuda/common.cuh#L1258) completely changes the behavior:metric GGML_CUDA_DISABLE_GRAPHS=0(baseline)GGML_CUDA_DISABLE_GRAPHS=1GPU memory (after ~10 min soak) 3184 MiB and growing 1430 MiB — flat GPU leak rate (post-reservation) ~40-50 MiB/min 0 MiB/min Crash (cublas INVALID_VALUE) Always in 21-57 min ALIVE after 46 min, 0 probes, 534k retries, 135k context-exceeded The cache is keyed by
cgraph->nodes[0](a ggml tensor pointer). The MTP draft context's graphs are rebuilt constantly (due to sampler-based graph-reuse rejection at https://github.com/ggml-org/llama.cpp/blob/6c8dcaa7ae/src/llama-graph.h#L820-L831), creating a steady stream of new cache entries. Each entry holds a captured cudaGraph with device memory — the eviction sweep (every 5s, evicting idle >=10s, https://github.com/ggml-org/llama.cpp/blob/6c8dcaa7ae/ggml/src/ggml-cuda/common.cuh#L1435-L1441) can't keep up.The crash mechanism (the
streamsarray slot insideggml_backend_cuda_contextbeing overwritten with a host-heap pointer, previously documented in #26558 (comment)) ties to the sameggml_backend_cuda_contextstruct that owns the cache map, the pool, and the streams — the graph-cache churn corrupts the context's bookkeeping.Disabling CUDA graphs (
GGML_CUDA_DISABLE_GRAPHS=1) is a clean workaround: no leak, no crash, at the cost of performance (no graph replay).Final confirmatory experiment:
GGML_CUDA_DISABLE_GRAPHS=1— no crash, no leakTo test whether the CUDA graph capture mechanism itself is the root cause (rather than the MTP logic per se), I ran the same repro (1024-ctx,
--kv-unified -np 4,--spec-type draft-mtp,--temp 0, same soak) withGGML_CUDA_DISABLE_GRAPHS=1, which disables the backend-level CUDA graph capture (common.cuh:1258). The result:metric GGML_CUDA_DISABLE_GRAPHS=0(baseline, all ∼6 runs)GGML_CUDA_DISABLE_GRAPHS=1Crash (cublas INVALID_VALUE) Always, in 21–57 min 0 after 1h47m of continuous soak (still running) GPU memory (post-reservation) Grows ~40–50 MiB/min Flat at 1430 MiB from startup GEMM probes fired ce_peek=0, handle alive, garbage host-heap stream0 probes Retries processed 206k–536k before crash 1.27M (alive) Context-exceeded events 52k–92k before crash 325k (alive) The server survives 1.7× the longest graphs-ON crash time, with zero memory growth, zero probes, and zero crashes, while processing 1.27M retries and 325k context-exceeded events — far more churn than any baseline run.
Interpretation: The CUDA graph cache (
cuda_graphsunordered_map inggml_backend_cuda_context, keyed bycgraph->nodes[0]pointer, common.cuh:1428) grows under the MTP draft context because the draft graph is rebuilt constantly (the sampler-based graph-reuse rejection at llama-graph.h:820-831 preventscan_reusefor identical shapes when the token/seq_id composition changes). Each rebuild creates a new cache entry with a capturedcudaGraphholding device memory → the leak. The cache churn also corrupts theggml_backend_cuda_contextstruct (which owns the cache map, the pool, and thestreams[]array) — a heap pointer lands in the stream slot →cublasSgemmlaunches on a garbage stream →cudaErrorInvalidValue→CUBLAS_STATUS_INVALID_VALUE→GGML_ABORT.Workaround:
GGML_CUDA_DISABLE_GRAPHS=1(no crash, no leak — at the cost of CUDA graph replay performance). The proper fix would be to key the graph cache on the graph's structural identity (uid or shape) rather than the transientcgraph->nodes[0]pointer, or to disable the graph cache for the MTP draft context specifically.Summary of all findings in this issue:
- The crash is a
cublasSgemm_v2returningCUBLAS_STATUS_INVALID_VALUE(7) with a provably valid parameter set — the cuBLAS handle is intact, the pointers are non-null and 16-byte aligned, and all dimension/leading-dimension constraints hold. - The root cause is a garbage CUDA stream pointer stored in the cuBLAS handle — a host-heap address (
0x624c...range) instead of a valid driver stream handle, causingcudaLaunchKernelto fail on the bogus stream. - The garbage stream comes from the
ggml_backend_cuda_context::streams[]array being corrupted by the CUDA graph cache churn (the cache'scuda_graphsunordered_map, the pool, and the streams array all live in the same struct). - The trigger is always the same: all slots hit context-exceeded, fresh decodes start, and the first F32
cublasSgemmin layer-0's linear-attentionssm_alphaprojection fails — because it's the first cuBLAS call after the corruption. LLAMA_GRAPH_REUSE_DISABLE=1andllama_synchronize(ctx_tgt)beforeMTP::process()both delay the crash proportionally to how much they slow the server, but do not prevent it — confirming the corruption is not a timing race.GGML_CUDA_DISABLE_GRAPHS=1completely prevents both the leak and the crash — the server survives 1.7× the longest baseline crash time with zero symptoms.- The GPU memory leak (~40-50 MiB/min per server under MTP, observed across multiple independent servers) is also eliminated by
GGML_CUDA_DISABLE_GRAPHS=1.
- The crash is a
- added a commit that references this issue
on Aug 15, 2026 Another data point + independent confirmation, from a different setup (dual RTX 3090, NVLink, Linux, CUDA 12.x, driver 580):
We hit what looks like exactly this crash class running Qwen3.6-27B-MTP (
--spec-type draft-mtp) under sustained batched load, with--split-mode tensornoticeably accelerating time-to-failure vs single-GPU layer split. Same signature:CUDA error: an unsupported value or parameter was passed to the functionfromcublasGemmEx(CUBLAS_STATUS_INVALID_VALUE), always on device 1, all GEMM params valid on inspection. Reproduced across three builds spanning June–August (4c65955,d69b7e606,4dee52f).Mitigation matrix (build
4dee52f, same load generator, ~55s median time-to-failure baseline):config time to failure stock (CUDA graphs on) 53–69 s LLAMA_GRAPH_REUSE_DISABLE=1~7 min (≈7×, but still eventually dies) GGML_CUDA_DISABLE_GRAPHS=1no failure — 15 min / 118 req clean, then 50 min sustained clean That matches your finding above almost exactly: graph-reuse-disable only delays the crash roughly in proportion to how much it slows the server down, while full graph-capture disable eliminates it outright. On our side we'd independently landed on the same suspect (the per-context graph cache keyed by
cgraph->nodes[0], swept by the timed eviction, colliding/racing with an in-flightcudaGraphExec_treplay) before seeing your root-cause writeup — good to see it nailed down precisely to theggml_backend_cuda_context::streamsarray getting a host-heap pointer written over a driver stream handle. Confirms this isn't specific to your 4090/single-GPU/tiny-model repro: same corruption reproduces on multi-GPU tensor-split with a much larger MTP model, and--split-mode tensormakes it worse, not better (more concurrent graph churn across devices presumably shrinks the race window).We're running with
GGML_CUDA_DISABLE_GRAPHS=1as a permanent workaround in production. Happy to run further diagnostics on our repro if useful — it fires reliably in under 2 minutes with graphs enabled, and we can pull the same stream/handle probes you used if that helps narrow the fix.I can't seem to reproduce the memory-growth at all. Did you configure the project in a specific way? Running server with
./build-x64-linux-gcc-reldbg/bin/llama-server -m /mnt/share/gguf/unsloth/Qwen3.5-0.8B-MTP-GGUF/Qwen3.5-0.8B-UD-Q4_K_XL.gguf --spec-type draft-mtp --temp 0 -c 1024 -np 4 --kv-unified --host 127.0.0.1 --port 18081 --no-webuiand your command above@ZisIsNotZis please run your repro on latest master and see if it re-occurs (#26574 has been merged)
CUDA 12.x, driver 580
For CTK < 12.4, #26574 should fix a cudaGraph-associated memory-leak
@ORippler Interesting, now it seems not crash any more, I'm on Driver Version: 595.84, CUDA Version: 13.2, cuda-toolkit 13.3.1-1, cudnn9-cuda-13 9.25.0.15-1.
@ORippler Interesting, now it seems not crash any more, I'm on Driver Version: 595.84, CUDA Version: 13.2, cuda-toolkit 13.3.1-1, cudnn9-cuda-13 9.25.0.15-1.
Closing as it no longer repros apparently
Name and Version
Commit tested:
6c8dcaa7ae41fa9f4aa2b3b68ee82cb8b2a03632(master,sycl: parallelize the non-contiguous concat kernel (#25852))CUDA build:
CMAKE_BUILD_TYPE=Release,GGML_CUDA=ON,CMAKE_CUDA_ARCHITECTURES=89, built with GNU 13.3.0.Operating systems
7.0.0-28-generic)GGML backends
Hardware
NVIDIA GeForce RTX 4090 (24 GiB), driver
595.84, CUDA13.2.Models
unsloth/Qwen3.5-0.8B-MTP-GGUFatQ4_K_XL(theQwen3.5-0.8B-UD-Q4_K_XL.gguffile). This is the Qwen3.5 "unified decoder" (hybrid full-attention / linear-attention / gated delta net) architecture with a built-in MTP/NextN head (qwen35.nextn_predict_layers, one nextn block atblk.24).Problem description & steps to reproduce
llama-servercrashes with a hard CUDA error (cublasSgemm_v2→CUDA_ERROR_INVALID_VALUE→GGML_ABORT) when run with--spec-type draft-mtpunder parallel load with KV-cache saturation. The crash is a GPU-API abort, not a graceful "context size exceeded" return, so it is a bug rather than an expected failure mode of running out of context.This has been reproducible for a long time ("ever since MTP code merged"); the same crash class is reported in #20049 and #23803, and in Indras-Mirror/llama.cpp-turboq-mtp#17. The maintainers' earlier explanation ("total parallel tokens exceeded total KV slots") describes the trigger (the log is full of context-exceeded/retry events) but not the mechanism: a context-full condition must return
decode() == 1gracefully, never reachcublasSgemm.Stable, fast reproduction (~20–25 min, no benchmark harness needed). Two independent server configs both crash; both crash on the same op:
Server A (smaller context):
Server B (larger context):
Stress client: many parallel
/completionrequests with mixed prompt lengths /n_predict, designed to keep the KV cache permanently saturated so the server is constantly in thefailed to find free space in the KV cache, retrying with smaller batch size/Context size has been exceededregime (the exact regime shown in the attached log). A minimal Python soak that reproduces it:Both servers crash after ~20–25 min with the identical signature. The original (real-world) report was under
hb run -d terminal-bench -a mini-swe-agentagainst the same server, where it took much longer (many thousands of tasks); the soak above just makes the same KV-saturation regime much denser.First Bad Commit
Not bisected. Long-standing — the reporter states the crash has occurred "ever since the MTP code was merged", and the same crash class is described in #20049 / #23803 / Indras-Mirror/llama.cpp-turboq-mtp#17.
Relevant log output
Every crash is preceded by the same pattern: one or more
Context size has been exceededevents, slots released, then 4 fresh slots launched, then the crash in the first decode:Captured failing GEMM (via a local diagnostic patch,
LLAMA_DEBUG_GEMM=1)I added a one-line diagnostic at the exact crash site (
ggml-cuda.cu:1541, F32cublasSgemmbranch) that dumps the failing call's parameters oncublasSgemm_v2 != CUBLAS_STATUS_SUCCESS. Both independent reproductions failed on the identical matmul, with all cuBLAS constraints satisfied:Mapping to the call
cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_N, m, n, k, …):status=7isCUBLAS_STATUS_INVALID_VALUE. The operands are:src0 = blk.0.ssm_alpha.weight— F32[1024×16]weight of layer-0 linear attention (gated delta net) alpha projection (src/models/qwen35moe.cpp:393),src1 = attn_norm-0— F32[1024×16]layer-0 attention norm output,dst = node_43—[16×16].Since every printed parameter provably satisfies cuBLAS's own validation rules, the rejection indicates corruption of the cuBLAS handle / its internal state (i.e., memory corruption elsewhere in the MTP path), rather than a bad tensor shape. It is deterministic — the same op fails on independent runs — which is consistent with "first F32 cuBLAS call after the corruption event."
Analysis done
Confirmed:
cublasSgemmwith a valid parameter set → handle/state corruption, not a user-config or context-size issue.ssm_alphaprojection, always right after all slots are cleared via context-exceeded and fresh decodes start.ctx_dft,LLAMA_CONTEXT_TYPE_MTP) that re-decodes every target batch and maintains its own KV cache. The draft head graph isLLM_GRAPH_TYPE_DECODER_MTP(src/models/qwen35moe.cpp:553+).Suspected mechanisms (pending confirmation):
Un-synchronized read of the target's hidden states.
llama_context::decode()deliberately does not synchronize at the end (//synchronize();is commented out atsrc/llama-context.cpp:2080); nextn embeddings are copied to host withggml_backend_tensor_get_async(llama-context.cpp:2008), andllama_get_embeddings_nextn()performs no sync (llama-context.cpp:934). The server callsllama_decode(ctx_tgt, …)and immediatelycommon_speculative_process(...)→MTP::process()readsllama_get_embeddings_nextn(ctx_tgt)atcommon/speculative.cpp:1450— on a different context/stream. This is a genuine host-memory data race under load.Batch-retry loop is not MTP-aware.
tools/server/server-context.cpp:3684-3692retries by halvingn_batchon target-decode failure;MTP::process()(the second decode pass) is only invoked on target success (server-context.cpp:3697) but its KV/state are coupled to the batches that failed/retried.Separate CUDA streams + shared device memory between
ctx_tgtandctx_dft(each context owns a backend with its own stream/pool) enabling a use-after-free window on GPU buffers.Tests in progress:
LLAMA_GRAPH_REUSE_DISABLE=1soak (the logs show very heavy graph reuse:graphs reused = 21941–62952). If disabling graph reuse stops the crash, that pins the vector.llama_synchronize(ctx_tgt)beforeMTP::process()test (targets suspicion Merging tensors of larger models #1).Related PRs/issues