Skip to content

Tiered kv cache implementation along with vattention (mad-lab-cache)#8

Merged
kmbandy merged 21 commits into
masterfrom
feature-kv-cache-improvements
Apr 27, 2026
Merged

Tiered kv cache implementation along with vattention (mad-lab-cache)#8
kmbandy merged 21 commits into
masterfrom
feature-kv-cache-improvements

Conversation

@kmbandy

@kmbandy kmbandy commented Apr 25, 2026

Copy link
Copy Markdown
Owner

AMD ROCm Tiered KV Cache + NVMe Demand Weight Paging

Overview

This PR implements and stabilizes two major infrastructure features for running large models on AMD ROCm hardware with limited VRAM:

  1. 3-tier KV cache — hot (VRAM), warm (secondary GPU VRAM or RAM), cold (SSD NVMe)
  2. NVMe demand weight paging — 8-slot VRAM pool with on-demand weight loading for models that exceed VRAM

Both features are WIP infrastructure. Output correctness for weight paging is a known follow-up item.


Tiered KV Cache Fixes

Warm GPU tier OOM fix
When --kv-warm-device N is set, the pool previously allocated both RAM staging buffers and GPU VRAM buffers for all warm slots. On a 262144-slot warm tier this caused ~8GB of wasted RAM and an OOM kill. RAM staging buffers are now skipped when a GPU warm device is configured.

k_bytes/v_bytes quantization fix
Per-token KV byte size was computed as ne[0] * ggml_element_size(tensor). For block-quantized types (turbo4, Q4_K, etc.) ggml_element_size returns the block size in bytes rather than bytes-per-element, producing a 128× overestimate. Switched to tensor->nb[1] (actual row stride), which is correct for all types.

Hybrid model KV wiring
init_slot cast llama_get_memory() to llama_kv_cache *. Hybrid-architecture models (Qwen3-27B and similar) return llama_memory_hybrid or llama_memory_hybrid_iswa, causing silent metadata-only fallback. Added casts for both hybrid types so attention KV layers wire correctly into the tiered cache.

Tier init logging
Added startup log lines for warm GPU (ROCm%d KV warm buffer size = X MiB) and cold SSD (cold tier: N slots reserved, path: ...) to match the existing hot tier output.


Weight Pager Fixes

  • Pool allocation via ggml buffer: replaced raw hipMalloc with ggml_backend_buft_alloc_buffer; sets tensor->buffer = pool.ggml_buf on page-in so ggml_cuda_mul_mat doesn't crash on src->buffer->buft dereference
  • Async prefetch disabled by default: io_uring page-index tracking in complete_prefetch is broken; disabled until fixed
  • fd dup moved outside prefetch gate: page_in uses pread regardless of prefetch state; fds must always be populated
  • O_DIRECT cleared on dup fds: GGUF tensor offsets are not sector-aligned; O_DIRECT on dup fds caused silent misreads
  • Padding zeroed after hipMemcpy: zeroes bytes past page.size up to slot_size so quantized kernel reads past tensor data are safe
  • hipGraph disabled: GGML_CUDA_DISABLE_GRAPHS=1 via setenv; removed static from disable_cuda_graphs_due_to_env in common.cuh

Known Issues / Follow-ups

  • Weight pager output correctness: gallocr allocates paged tensors (buffer=nullptr) in scratchpad, sets tensor->extra; callback overrides data and buffer but not extra; GPU kernel reads stale extra → coherent but wrong tokens. Fix: pre-seat paged tensors into pool.ggml_buf before ggml_backend_sched_alloc_graph.
  • Async prefetch: re-enable after fixing page-index tracking in complete_prefetch / submit_prefetch
  • Tiered KV eviction under load: watch for hot→warm→cold pressure as context fills up under concurrent agent workloads

Hardware / Test Config

  • AMD Radeon AI PRO R9700 — gfx1201, 32GB VRAM (primary inference + hot KV tier)
  • AMD RX 6900XT — 16GB VRAM (warm KV tier via --kv-warm-device 1)
  • NVMe SSD (cold KV tier, weight paging source)

Models Tested

Model Feature
Qwen3.6-27B-Q6_K Tiered KV cache (hot/warm/cold), turbo4 KV quant
Qwen3.6-35B-A3B Tiered KV cache
Qwen3.5-REAP-97B Tiered KV cache
MiniMax-M2.7-229B-IQ3_S NVMe demand weight paging (8-slot VRAM pool, 0.03 t/s)

Note: As context fills up under concurrent agent workloads, keep an eye on hot→warm→cold eviction pressure. The tiered eviction pipeline is functional but has not been stress-tested at full 512K context utilization.


Co-authored with Claude Sonnet 4.6

kmbandy added 2 commits April 24, 2026 20:19
- Fix Issue 1: Replace CUDA API calls with HIP API in ggml-cuda.cu
  - cudaMemcpyPeerAsync → hipMemcpyPeerAsync
  - cudaError_t → hipError_t
  - cudaSuccess → hipSuccess

- Fix Issue 2: Fix typo std.max → std::max in llama-eviction-policy.h

- Fix Issue 3: Implement migrate_tokens and batch_migrate_tokens with HIP-aware memory copies

- Fix Issue 4: Implement save_to_ssd and load_from_ssd using llama_ssd_storage_format

- Fix Issue 5: Add ggml tensor handles (ggml_tensor*) for K and V data to tier class

- Fix Issue 6: Add device-to-host copy for hot→warm path before serialization
  - Added is_device_data parameter to save_to_ssd()
  - Added to_device parameter to load_from_ssd()
  - Uses hipMemcpy with hipMemcpyDeviceToHost/hipMemcpyHostToDevice as needed
kmbandy and others added 19 commits April 24, 2026 23:10
- llama-io-uring: async fixed-buffer read class wrapping liburing;
  auto-detected at cmake configure time on Linux; falls back gracefully
  to pread if io_uring_queue_init fails
- llama-model-loader: replaces read_raw_unsafe with io_uring submit+wait
  in the weight-upload loop; achieves ~5.7 GB/s on SN850X vs 1.5 GB/s
  baseline via SAM pipeline
- llama-kv-cache-tiered: warm_device field in llama_tier_config; when
  set, init() hipMalloc warm K+V buffers on that device; hot->warm
  migrations copy R9700 VRAM -> host staging -> 6900XT VRAM instead of
  SSD; spills to SSD only when warm tier is full
Adds --kv-warm-device <N> flag (env: LLAMA_ARG_KV_WARM_DEVICE) to
specify a HIP device index for the warm KV cache tier. Wires through
common_params into llama_tier_config.warm_device and calls
set_warm_elem_bytes() before init() so hipMalloc sizing is correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ops.cpp: remove redundant extern in extern-C decl (turbo-quant merge issue)
- src/CMakeLists.txt: add llama-kv-cache-tiered.cpp to llama lib + HIP includes
- tools/server/CMakeLists.txt: add /opt/rocm/include + __HIP_PLATFORM_AMD__
- server-tiered-cache.h: add default ctor; fix global_stats/stats_ name collision
- server-tiered-cache.cpp: update stats_ references after rename
- server-context.cpp: fix unique_ptr copy assignment; fix tiered_cache scope in
  metrics lambda; fix n_discard ordering; fix SLT_INF variadic macro;
  fix json::object push_back for ordered_json; add friend server_routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The tiered cache percentages define hard capacity limits per tier that
sum to the full ctx budget. Previously the R9700 KV buffer was still
allocated at full n_ctx despite the hot tier being only a fraction.

- Add kv_tier_total_ctx to common_params to preserve the full budget
- Before common_init_from_params, reduce n_ctx to hot_pct * n_ctx so
  the GPU KV buffer only allocates for the hot tier
- server-tiered-cache.cpp uses kv_tier_total_ctx for config.total_ctx
  so warm/cold capacity calculations remain correct

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace placeholder warm tier (flat single-buffer) with per-layer TieredKVLayer
structs. Add public get_layer_k_raw/v_raw/get_num_kv_layers/is_v_transposed
accessors to llama_kv_cache. Wire set_kv_layers_from_cache() from server init_slot
so tensor pointers are populated at startup.

Hot→warm migration now:
- Copies K contiguously via hipMemcpy (VRAM→RAM or VRAM→eGPU D2D)
- Gathers transposed V column at pos via hipMemcpy2D (VRAM→RAM) or element loop
- Warm→hot restores with scatter for v_trans=true (hipMemcpy2D)

Metadata-only fallback retained when set_kv_layers_from_cache() not called.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…s 1-4)

Phase 1-2: Real per-layer K/V tensor data movement via hipMemcpy/hipMemcpy2D.
  - hot->warm: K contiguous copy, V transposed gather (hipMemcpy2D)
  - warm->hot: K direct copy, V scatter back (hipMemcpy2D)
  - TieredKVLayer struct with per-layer tensor pointers and warm buffers
  - set_kv_layers_from_cache() wired from server init

Phase 3: io_uring cold tier for NVMe direct I/O.
  - liburing integrated for async cold storage reads
  - SSD path for evicted KV chunks

Phase 4: Semantic embedding-driven prefetch.
  - bge-small (33M) fingerprints evicted KV chunks
  - Cosine similarity prefetch hints on new prompt arrival
  - Two-stage cold->warm->hot promotion for semantic cache hits
  - semantic_top_k and prefetch hint routing fully wired

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…abled

Hot tier shrinks n_ctx passed to llama init, so llama_n_ctx_seq() returns
the hot tier size (~60% of --ctx-size). With tiering, slot.n_ctx must equal
kv_tier_total_ctx so requests up to the full context window are accepted.
O_DIRECT on a 100GB model file during the metadata-only scan hangs.
The dry-run only needs metadata so normal buffered IO is fine.
hipHostMalloc buffers are already pinned by the HIP driver so
io_uring_register_buffers fails with ENOMEM on ROCm. Fall back to
unregistered io_uring reads rather than abandoning the ring entirely.
With --direct-io on ROCm+SAM, allocate staging buffers using
hipExtMallocWithFlags(hipDeviceMallocFinegrained) instead of hipHostMalloc.
Fine-grained VRAM is CPU-accessible via BAR1, so io_uring writes land
directly in VRAM over PCIe without touching system RAM.
Falls back to pinned host memory if hipExtMallocWithFlags fails.
…aults

After context shift, slot.prompt.checkpoints retained pre-shift positions.
Subsequent checkpoint restore via llama_state_seq_set_data_ext used stale
positions causing GPU memory access faults on multi-GPU ROCm setups.
- llama-weight-pager: page table, VRAM slot pool, LRU eviction
- page_in via pread into hipExtMallocWithFlags fine-grained VRAM (SAM path)
- ggml eval callback hook: redirects tensor data pointers before each op
- async io_uring prefetch: pipelines next layer NVMe read with GPU compute
- CLI: --weight-paging, --weight-paging-slots, --weight-paging-prefetch
- metrics: weight pager stats exposed at /metrics endpoint

Fixes: hipMalloc pool alloc, strcmp tensor name comparison, slot eviction
page invalidation, llama-model.h include for server-context.
…d model support

- Fix warm GPU tier OOM: skip RAM staging buffers when warm_device >= 0 (6900XT);
  previously allocated both RAM and GPU warm buffers, wasting ~8GB RAM per session
- Fix k_bytes/v_bytes computation: use nb[1] (row stride) instead of
  ne[0]*element_size which returns block size for quantized types like turbo4,
  causing 557056 MiB warm GPU misreads and silent hipMalloc failures
- Fix hybrid model KV wiring: handle llama_memory_hybrid and llama_memory_hybrid_iswa
  in init_slot so Qwen3-27B and similar hybrid models wire attention KV layers
  into the tiered cache instead of metadata-only mode
- Add tier logging: warm GPU (ROCm device + MiB), cold SSD (slots + path) on init
- Weight pager: use ggml_backend_buft_alloc_buffer for pool allocation so
  tensor->buffer is valid on callback; set tensor->buffer = pool.ggml_buf on page-in
- Weight pager: disable async prefetch default (io_uring page-index tracking broken);
  move fd dup outside prefetch gate so pread always has valid fds
- Weight pager: clear O_DIRECT on dup fds; zero padding bytes after hipMemcpy
- hipGraph: disable via setenv + remove static from disable_cuda_graphs_due_to_env

Tested on AMD Radeon AI PRO R9700 (gfx1201, 32GB) + RX 6900XT (16GB warm tier)
with Qwen3.6-27B-Q6_K (tiered KV), Qwen3.6-35B-A3B, Qwen3.5-REAP-97B, and
MiniMax-M2.7-229B-IQ3_S (NVMe demand weight paging, 8-slot VRAM pool).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@kmbandy
kmbandy merged commit a526aa5 into master Apr 27, 2026
8 of 50 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant