fix(cuda): sync device before cuMem pool extension to fix VMM allocator race

Merged
EA
5mo ago
+14-0

The VMM pool's alloc() path extends the pool via cuMemCreate + cuMemMap + cuMemSetAccess when an allocation can't fit in the existing mapped region. cuMemSetAccess can race with previously-queued async work on the default stream and return CUDA_ERROR_NOT_READY ("device not ready"). The CU_CHECK macro then fires GGML_ABORT, but in some process configurations the abort doesn't actually terminate (the gdb-fork backtrace path interferes, the SIGABRT gets swallowed somewhere, etc.) and execution continues. The mapping is registered but access permissions were never set, so every subsequent read/write into that region silently misbehaves — typically returning zeroed or stale data without surfacing any error.

Visible failure mode: workloads that interleave compute with ggml_backend_alloc_ctx_tensors on the same backend. Reproduced deterministically by dflash's prefix-cache path (Luce-Org/lucebox-hub#59), where snapshot copies fire during prefill, then a follow-up alloc triggers pool extension, and subsequent cached-state restores return zeroed KV → decode emits 0 tokens with finish_reason=stop.

With this sync added, the same workload runs cleanly with VMM enabled and delivers 2.5× TTFT / 1.5× wall speedup on long agentic prefixes (RTX 3090 Ti, CUDA 13.2, Qwen 3.6 27B).

Sync placement

Just before the cuMem* calls inside the pool-extension branch:

if (size > avail) {
    // ... compute reserve_size ...
    GGML_ASSERT(pool_size + reserve_size <= CUDA_POOL_VMM_MAX_SIZE);

    CUDA_CHECK(cudaDeviceSynchronize());   // ← new

    // allocate more physical memory
    CU_CHECK(cuMemCreate(...));
    ...
}

The sync only fires when the pool actually needs to grow, so steady-state hot-path allocations (where pool_size already covers the request) are unaffected. Added cost is one cudaDeviceSynchronize per pool extension event — negligible on any real workload (pool extensions are infrequent by design — the pool is sized in granularity-rounded chunks and rarely shrinks).

Validation

Bench in dflash with VMM enabled, prefix cache, slots=2, max-ctx=24576:

sessionturnsTTFT coldTTFT warmTTFT ×wall ×empties
short (~10K chars)1040.0s39.9s1.00×0.77×0/10
medium (~25K)655.9s44.9s1.24×0.75×0/6
long (~38K)10133.8s51.7s2.59×1.27×0/10
huge (~70K)10242.1s97.3s2.49×1.52×0/10

Pre-fix: workloads at multi-slot prefix cache produce empty bodies on every cache hit after call 2 (deterministic). Post-fix: 0/36 empties.

Test plan

  • dflash prefix-cache repro reproduces empty bodies pre-fix; produces real content post-fix
  • No regressions in standard llama.cpp tests (caveat: only ran the dflash workload — full ggml-cuda test suite hasn't been run against this patch)
  • Test on Blackwell / sm_120 — only validated on RTX 3090 Ti / sm_86 so far. The race is platform-agnostic, but the timing window may differ.

🤖 Generated with Claude Code