perf(kvflash): async page-out D2H and page-in H2D via pinned copy stream#408
Conversation
Replace synchronous ggml_backend_tensor_get/set in KvFlashPager::copy_chunk with cudaMemcpyAsync on a dedicated page_stream_. Host backing buffers (ChunkState::host_data) are now cudaMallocHost-pinned for maximum PCIe throughput instead of std::vector<uint8_t> in pageable memory. zero_block() uses cudaMemsetAsync on the same stream so the slot zero is always serialised after the preceding D2H and before any subsequent H2D that reuses the slot. Page-out D2H fires and is left running — no CPU block after eviction. alloc_span() synchronises page_stream_ only when a page-in (H2D) was issued this step, ensuring recalled KV data is resident before the next attention forward. reselect() applies the same single-sync pattern after batching N evictions + M recalls. Because all transfers and memsets share one stream, the ordering guarantee is: D2H(evict) → memset(zero) → H2D(recall) within a single alloc_span/reselect call, with no need for cross-stream events. Build guard: KVFLASH_HAS_ASYNC_DMA is defined only when DFLASH27B_BACKEND_CUDA, DFLASH27B_BACKEND_HIP, or GGML_USE_HIP is set. Non-CUDA builds fall back to the original ggml_backend_tensor_get/set path with std::vector<uint8_t> backing unchanged. Expected: page_out latency (1.26 ms p50 per 64-token chunk) and page_in latency (0.63 ms p50) no longer on the decode-thread critical path.
There was a problem hiding this comment.
2 issues found across 1 file
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
…ool state - Check cudaMallocHost return in page_out(), return false on failure - Check cudaMemcpyAsync return in copy_chunk(), log D2H/H2D direction - Check cudaMemsetAsync return in zero_block(), log on failure - Check cudaFreeHost return in reset() and cleanup_(), log on failure - Remove pool state (chunks_, free_blocks_, n_blocks_) from cleanup_() cleanup_() now only handles GPU resources (stream + pinned memory). Pool state is managed by reset() and the vector destructor.
| uint64_t clock_ = 0; | ||
| uint64_t epoch_ = 0; | ||
|
|
||
| #ifdef KVFLASH_HAS_ASYNC_DMA |
There was a problem hiding this comment.
any side impact to introduce this option? If this is always better, maybe we can default on this.
|
Small clarification first — #if defined(DFLASH27B_BACKEND_CUDA) || defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP)
#include "gpu_runtime_compat.h"
#define KVFLASH_HAS_ASYNC_DMA 1
#endifThe sync path is just the CPU fallback. So it's already "on" everywhere a GPU exists. On side impacts, the main one is pinned host memory. |
…ream lifecycle page_in()/page_out() copy on page_stream_ asynchronously; alloc_span() and reselect() batch the page-in sync, which is correct for the decode path. A caller that issues page_in() directly and then reads device KV (the test_kvflash roundtrip) raced the async H2D on HIP, where the default stream does not implicitly synchronize with page_stream_ (masked on CUDA). Add a public synchronize_paging(), route the alloc_span/reselect batched syncs through it, and call it in the roundtrip test before reading device KV. Harden the stream lifecycle: check cudaStreamCreate() and fall back to the default stream on failure; free pinned host_data unconditionally in cleanup_() so a failed create does not leak; synchronize page_stream_ unconditionally (null == default stream). Realign server/deps/llama.cpp to main (574be6132) so merging this branch does not downgrade the submodule.
Add serialize()/deserialize() to KvFlashPager (snapshot the full resident+paged KV in logical chunk order; header-validated against layout) and a factored for_each_segment() helper. serde uses synchronous get/set and adapts to the pinned void* host_data of the async-DMA path (Luce-Org#408). Add critical-chunk pinning (pin_range/is_pinned/unpin_all + a best-effort deadlock floor) OR-ed into the ensure_free_block + reselect protections; empty by default (byte-identical non-pin path). CPU unit test (no GPU) covers serde round-trip, header-guard reject, pinning, deadlock guard, reset.
Add serialize()/deserialize() to KvFlashPager (snapshot the full resident+paged KV in logical chunk order; header-validated against layout) and a factored for_each_segment() helper. serde uses synchronous get/set and adapts to the pinned void* host_data of the async-DMA path (Luce-Org#408). Add critical-chunk pinning (pin_range/is_pinned/unpin_all + a best-effort deadlock floor) OR-ed into the ensure_free_block + reselect protections; empty by default (byte-identical non-pin path). CPU unit test (no GPU) covers serde round-trip, header-guard reject, pinning, deadlock guard, reset.
…r KVFlash Drive the MoE cold-expert hybrid path through KVFlash's resident pool: prompts larger than the pool prefill via a chunk loop over hybrid_forward_batch (eviction automatic in alloc_span); the restore residual delta routes through the same chunked path. Pooled snapshot save/restore serializes the pager into the prefix snapshot (PrefixSnapshot += is_pooled + blob; snapshot_target_cache/restore gain skip_kv; the blob rides the disk prefix-cache via a named tensor so cross-turn 128K restore composes). Drafter-scorer residency + DFLASH_KVFLASH_PIN_SPANS critical-chunk pinning wired in. Composes with the landed KVFlash (Luce-Org#373/Luce-Org#408/Luce-Org#385) and MoE restore (Luce-Org#362); serde adapts to the async pinned host_data. GPU gate (RTX 3090): pooled prefill preserves sink context + stable across pool sizes; cross-turn disk restore round-trips losslessly.
Add serialize()/deserialize() to KvFlashPager (snapshot the full resident+paged KV in logical chunk order; header-validated against layout) and a factored for_each_segment() helper. serde uses synchronous get/set and adapts to the pinned void* host_data of the async-DMA path (Luce-Org#408). Add critical-chunk pinning (pin_range/is_pinned/unpin_all + a best-effort deadlock floor) OR-ed into the ensure_free_block + reselect protections; empty by default (byte-identical non-pin path). CPU unit test (no GPU) covers serde round-trip, header-guard reject, pinning, deadlock guard, reset.
Add serialize()/deserialize() to KvFlashPager (snapshot the full resident+paged KV in logical chunk order; header-validated against layout) and a factored for_each_segment() helper. serde uses synchronous get/set and adapts to the pinned void* host_data of the async-DMA path (Luce-Org#408). Add critical-chunk pinning (pin_range/is_pinned/unpin_all + a best-effort deadlock floor) OR-ed into the ensure_free_block + reselect protections; empty by default (byte-identical non-pin path). CPU unit test (no GPU) covers serde round-trip, header-guard reject, pinning, deadlock guard, reset.
…r KVFlash Drive the MoE cold-expert hybrid path through KVFlash's resident pool: prompts larger than the pool prefill via a chunk loop over hybrid_forward_batch (eviction automatic in alloc_span); the restore residual delta routes through the same chunked path. Pooled snapshot save/restore serializes the pager into the prefix snapshot (PrefixSnapshot += is_pooled + blob; snapshot_target_cache/restore gain skip_kv; the blob rides the disk prefix-cache via a named tensor so cross-turn 128K restore composes). Drafter-scorer residency + DFLASH_KVFLASH_PIN_SPANS critical-chunk pinning wired in. Composes with the landed KVFlash (Luce-Org#373/Luce-Org#408/Luce-Org#385) and MoE restore (Luce-Org#362); serde adapts to the async pinned host_data. GPU gate (RTX 3090): pooled prefill preserves sink context + stable across pool sizes; cross-turn disk restore round-trips losslessly.
Add serialize()/deserialize() to KvFlashPager (snapshot the full resident+paged KV in logical chunk order; header-validated against layout) and a factored for_each_segment() helper. serde uses synchronous get/set and adapts to the pinned void* host_data of the async-DMA path (Luce-Org#408). Add critical-chunk pinning (pin_range/is_pinned/unpin_all + a best-effort deadlock floor) OR-ed into the ensure_free_block + reselect protections; empty by default (byte-identical non-pin path). CPU unit test (no GPU) covers serde round-trip, header-guard reject, pinning, deadlock guard, reset.
…r KVFlash Drive the MoE cold-expert hybrid path through KVFlash's resident pool: prompts larger than the pool prefill via a chunk loop over hybrid_forward_batch (eviction automatic in alloc_span); the restore residual delta routes through the same chunked path. Pooled snapshot save/restore serializes the pager into the prefix snapshot (PrefixSnapshot += is_pooled + blob; snapshot_target_cache/restore gain skip_kv; the blob rides the disk prefix-cache via a named tensor so cross-turn 128K restore composes). Drafter-scorer residency + DFLASH_KVFLASH_PIN_SPANS critical-chunk pinning wired in. Composes with the landed KVFlash (Luce-Org#373/Luce-Org#408/Luce-Org#385) and MoE restore (Luce-Org#362); serde adapts to the async pinned host_data. GPU gate (RTX 3090): pooled prefill preserves sink context + stable across pool sizes; cross-turn disk restore round-trips losslessly.
Add serialize()/deserialize() to KvFlashPager (snapshot the full resident+paged KV in logical chunk order; header-validated against layout) and a factored for_each_segment() helper. serde uses synchronous get/set and adapts to the pinned void* host_data of the async-DMA path (Luce-Org#408). Add critical-chunk pinning (pin_range/is_pinned/unpin_all + a best-effort deadlock floor) OR-ed into the ensure_free_block + reselect protections; empty by default (byte-identical non-pin path). CPU unit test (no GPU) covers serde round-trip, header-guard reject, pinning, deadlock guard, reset.
…r KVFlash Drive the MoE cold-expert hybrid path through KVFlash's resident pool: prompts larger than the pool prefill via a chunk loop over hybrid_forward_batch (eviction automatic in alloc_span); the restore residual delta routes through the same chunked path. Pooled snapshot save/restore serializes the pager into the prefix snapshot (PrefixSnapshot += is_pooled + blob; snapshot_target_cache/restore gain skip_kv; the blob rides the disk prefix-cache via a named tensor so cross-turn 128K restore composes). Drafter-scorer residency + DFLASH_KVFLASH_PIN_SPANS critical-chunk pinning wired in. Composes with the landed KVFlash (Luce-Org#373/Luce-Org#408/Luce-Org#385) and MoE restore (Luce-Org#362); serde adapts to the async pinned host_data. GPU gate (RTX 3090): pooled prefill preserves sink context + stable across pool sizes; cross-turn disk restore round-trips losslessly.
Summary
KvFlashPagercurrently blocks the decode thread for every page-out (D2H, 1.26 ms p50) and page-in (H2D, 0.63 ms p50) via synchronousggml_backend_tensor_get/set. This PR replaces those withcudaMemcpyAsyncon a dedicatedpage_stream_, making page-out fire-and-forget and page-in paid at most once peralloc_span()call.Changes
server/src/common/kvflash_pager.hgpu_runtime_compat.hwhenDFLASH27B_BACKEND_CUDA,DFLASH27B_BACKEND_HIP, orGGML_USE_HIPis set; defineKVFLASH_HAS_ASYNC_DMAChunkState::host_data:std::vector<uint8_t>→void *(cudaMallocHost-pinned) underKVFLASH_HAS_ASYNC_DMAcudaStream_t page_stream_andbool has_pending_page_in_toKvFlashPagercleanup_()that syncs and frees pinned buffers then destroys the streamattach(): callscleanup_()before re-init, creates stream withcudaStreamCreatereset(): syncs stream + frees all pinnedhost_databefore clearingchunks_zero_free_blocks(): syncs stream after async memsetscopy_chunk():cudaMemcpyAsync(DeviceToHost / HostToDevice, page_stream_)replacesggml_backend_tensor_get/setzero_block():cudaMemsetAsync(..., page_stream_)replacesggml_backend_tensor_setwith zero bufferalloc_span()/reselect():cudaStreamSynchronize(page_stream_)only whenhas_pending_page_in_is set; page-outs left asyncHow it works
All transfers share
page_stream_, giving this ordering guarantee within eachalloc_span()/reselect()call:No cross-stream events needed. The CPU thread is unblocked after issuing the transfers; it syncs only if a page-in was queued (H2D must land before the attention forward reads the recalled rows).
Performance / Numbers
Measured baselines from
server/DESIGN.mdandtest_kvflashprofile (E):cudaStreamSynchronizeper step with recallNumbers not yet validated on RTX 3090 (no local GPU). Benchmark via
test_kvflash --skip-profilesection E, or by instrumentingalloc_span()withstd::chrono.Limitations / Known follow-up
page_stream_and ggml's compute stream: relies onggml_backend_graph_computealready synchronising beforealloc_span()is called, which holds for the current single-threaded inference loop. If a future caller issues an async graph compute and callsalloc_span()concurrently the ordering must be revisited.reset()is called before the DMA completes,reset()syncspage_stream_first — safe, but adds latency at reset time.cudaStreamCreatereturn value is not checked; a failed create leavespage_stream_ = nullptr, in which casecudaMemcpyAsync/cudaMemsetAsyncfall back to the default (synchronous) stream — correct but unintentionally blocking.cudaEvent_t page_done_event_so the compute graph cancudaStreamWaitEventinstead of a hard sync; that is left for the graph PR.Verification / Test plan
cmake -B server/build -S server -DCMAKE_BUILD_TYPE=Release && cmake --build server/build --target test_kvflash -jserver/build/test_kvflash <qwen35.gguf>— suites A–D (relocation, paging, reselect/recall) must pass bit-exact vs baselinegfx1151ROCm build (samegpu_runtime_compat.hshim used byqwen35_dflash_target.cppsince qwen35: include gpu_runtime_compat.h for HIP builds #395)