- paged_kv_cache: new block-paged KV cache; adds a pinned-host swap pool with
a second BlockAllocator, per-sequence Location {Gpu,Cpu}, and lossless
swap_out/swap_in (block-granular D2H/H2D) for vLLM-style preemption.
bytes_per_block helper exposes per-block cost for VRAM-based sizing.
- decode_graph: CUDA-graph decode path.
- qwen3/gpt2/kv_cache: paged prefill/decode forward + related updates.
- tokenizer/bins: BPE updates, new xserv-chat CLI, bench-qwen3 tweaks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
152 lines
6.3 KiB
Rust
152 lines
6.3 KiB
Rust
use xserv_cuda::GpuBuffer;
|
|
use xserv_tensor::{DType, Device, Tensor};
|
|
use crate::config::ModelConfig;
|
|
|
|
/// GPU-resident KV cache. Pre-allocates max_seq_len on GPU,
|
|
/// appends new K/V via D2D copy at offset (no CPU round-trip).
|
|
pub struct GpuKVCache {
|
|
// Per layer: contiguous GPU buffer for K and V
|
|
// Layout: [num_kv_heads, max_seq_len, head_dim] — contiguous per head
|
|
k_bufs: Vec<GpuBuffer>,
|
|
v_bufs: Vec<GpuBuffer>,
|
|
// Per layer: pre-allocated staging buffers for get_kv_len output.
|
|
// Size: num_kv_heads * max_seq_len * head_dim * elem_size (max possible output).
|
|
// Avoids cudaMalloc/cudaFree on every get_kv_len call.
|
|
k_staging: Vec<GpuBuffer>,
|
|
v_staging: Vec<GpuBuffer>,
|
|
seq_len: usize,
|
|
max_seq_len: usize,
|
|
num_kv_heads: usize,
|
|
head_dim: usize,
|
|
elem_size: usize,
|
|
dtype: DType,
|
|
device: u32,
|
|
}
|
|
|
|
impl GpuKVCache {
|
|
pub fn new(config: &ModelConfig, max_seq_len: usize, dtype: DType, device: u32) -> Self {
|
|
let num_layers = config.num_layers();
|
|
let num_kv_heads = config.num_kv_heads();
|
|
let head_dim = config.head_dim();
|
|
let elem_size = dtype.size_bytes();
|
|
let buf_size = num_kv_heads * max_seq_len * head_dim * elem_size;
|
|
|
|
let mut k_bufs = Vec::with_capacity(num_layers);
|
|
let mut v_bufs = Vec::with_capacity(num_layers);
|
|
let mut k_staging = Vec::with_capacity(num_layers);
|
|
let mut v_staging = Vec::with_capacity(num_layers);
|
|
for _ in 0..num_layers {
|
|
let mut k = GpuBuffer::alloc(buf_size).expect("alloc KV cache K");
|
|
let mut v = GpuBuffer::alloc(buf_size).expect("alloc KV cache V");
|
|
k.zero().unwrap();
|
|
v.zero().unwrap();
|
|
k_bufs.push(k);
|
|
v_bufs.push(v);
|
|
k_staging.push(GpuBuffer::alloc(buf_size).expect("alloc KV staging K"));
|
|
v_staging.push(GpuBuffer::alloc(buf_size).expect("alloc KV staging V"));
|
|
}
|
|
|
|
Self { k_bufs, v_bufs, k_staging, v_staging, seq_len: 0, max_seq_len, num_kv_heads, head_dim, elem_size, dtype, device }
|
|
}
|
|
|
|
pub fn seq_len(&self) -> usize { self.seq_len }
|
|
pub fn max_seq_len(&self) -> usize { self.max_seq_len }
|
|
|
|
/// Append new K/V tensors for a given layer.
|
|
/// k_new, v_new: [1, num_kv_heads, new_tokens, head_dim] on GPU, contiguous.
|
|
/// `write_pos` is the sequence position to write at (caller manages this).
|
|
pub fn append(&mut self, layer: usize, k_new: &Tensor, v_new: &Tensor, new_tokens: usize, write_pos: usize) {
|
|
assert!(write_pos + new_tokens <= self.max_seq_len, "KV cache overflow");
|
|
let es = self.elem_size;
|
|
let hd = self.head_dim;
|
|
let max_s = self.max_seq_len;
|
|
let nh = self.num_kv_heads;
|
|
|
|
let k_src = k_new.storage().gpu_buffer();
|
|
let v_src = v_new.storage().gpu_buffer();
|
|
|
|
for h in 0..nh {
|
|
let src_off = h * new_tokens * hd * es;
|
|
let dst_off = (h * max_s + write_pos) * hd * es;
|
|
let count = new_tokens * hd * es;
|
|
self.k_bufs[layer].copy_from_device_at(k_src, src_off, dst_off, count).unwrap();
|
|
self.v_bufs[layer].copy_from_device_at(v_src, src_off, dst_off, count).unwrap();
|
|
}
|
|
}
|
|
|
|
pub fn advance_seq_len(&mut self, new_tokens: usize) {
|
|
self.seq_len += new_tokens;
|
|
assert!(self.seq_len <= self.max_seq_len, "KV cache seq_len ({}) exceeds max_seq_len ({})", self.seq_len, self.max_seq_len);
|
|
}
|
|
|
|
/// Get K/V cache tensors for a layer up to `seq_len` tokens: [1, num_kv_heads, seq_len, head_dim]
|
|
pub fn get_kv(&mut self, layer: usize) -> (Tensor, Tensor) {
|
|
let sl = self.seq_len;
|
|
self.get_kv_len(layer, sl)
|
|
}
|
|
|
|
pub fn get_kv_len(&mut self, layer: usize, sl: usize) -> (Tensor, Tensor) {
|
|
assert!(sl <= self.max_seq_len, "get_kv_len: sl ({sl}) exceeds max_seq_len ({})", self.max_seq_len);
|
|
let hd = self.head_dim;
|
|
let nh = self.num_kv_heads;
|
|
let es = self.elem_size;
|
|
let max_s = self.max_seq_len;
|
|
|
|
// Copy each head's valid portion into pre-allocated staging buffers.
|
|
// Split borrows: staging (mut) vs cache (shared) are separate struct fields,
|
|
// so the borrow checker allows simultaneous &mut staging + &cache.
|
|
let out_size = nh * sl * hd * es;
|
|
let k_stg = &mut self.k_staging[layer];
|
|
let k_buf = &self.k_bufs[layer];
|
|
let v_stg = &mut self.v_staging[layer];
|
|
let v_buf = &self.v_bufs[layer];
|
|
for h in 0..nh {
|
|
let src_off = (h * max_s) * hd * es;
|
|
let dst_off = (h * sl) * hd * es;
|
|
let count = sl * hd * es;
|
|
k_stg.copy_from_device_at(k_buf, src_off, dst_off, count).unwrap();
|
|
v_stg.copy_from_device_at(v_buf, src_off, dst_off, count).unwrap();
|
|
}
|
|
// Grab raw pointers before dropping the mutable borrows
|
|
let k_ptr = k_stg.as_mut_ptr();
|
|
let v_ptr = v_stg.as_mut_ptr();
|
|
|
|
// Create Tensors that borrow from the staging buffers (no cudaMalloc/cudaFree).
|
|
// Safety: staging buffers are owned by GpuKVCache and outlive the returned Tensors
|
|
// in practice (Tensors are consumed within the same forward pass before the next
|
|
// get_kv_len call overwrites the staging buffer).
|
|
let shape = &[1usize, nh, sl, hd];
|
|
let k = unsafe {
|
|
tensor_from_gpu_buffer(GpuBuffer::borrow_raw(k_ptr, out_size), shape, self.dtype, self.device)
|
|
};
|
|
let v = unsafe {
|
|
tensor_from_gpu_buffer(GpuBuffer::borrow_raw(v_ptr, out_size), shape, self.dtype, self.device)
|
|
};
|
|
(k, v)
|
|
}
|
|
}
|
|
|
|
/// Create a Tensor from a GpuBuffer (takes ownership).
|
|
unsafe fn tensor_from_gpu_buffer(buf: GpuBuffer, shape: &[usize], dtype: DType, device: u32) -> Tensor {
|
|
use xserv_tensor::storage::Storage;
|
|
use xserv_tensor::shape::contiguous_strides;
|
|
use smallvec::SmallVec;
|
|
|
|
let storage = Storage::cuda(buf, device);
|
|
Tensor::from_storage(
|
|
storage,
|
|
SmallVec::from_slice(shape),
|
|
contiguous_strides(shape),
|
|
0,
|
|
dtype,
|
|
)
|
|
}
|
|
|
|
/// Public version for use by other modules (e.g., batched decode concat).
|
|
///
|
|
/// # Safety
|
|
/// `buf` must be a valid GPU allocation with at least `product(shape) * dtype.size_bytes()` bytes.
|
|
pub unsafe fn tensor_from_gpu_buffer_pub(buf: GpuBuffer, shape: &[usize], dtype: DType, device: u32) -> Tensor {
|
|
tensor_from_gpu_buffer(buf, shape, dtype, device)
|
|
}
|