use std::ffi::c_void;
use std::sync::Arc;
use cudarc::driver::CudaDevice;
use super::arch::ModelConfig;
use super::tensor::{GpuDType, GpuTensor};
use super::InferenceError;
pub struct KvCache {
config: ModelConfig,
device: Arc<CudaDevice>,
keys: GpuTensor,
values: GpuTensor,
seq_len: usize,
max_seq_len: usize,
pending_tokens: usize,
}
impl KvCache {
pub fn new(
config: &ModelConfig,
max_seq_len: usize,
device: Arc<CudaDevice>,
) -> Result<Self, InferenceError> {
let num_layers = config.num_layers;
let num_kv_heads = config.num_kv_heads;
let head_dim = config.head_dim;
let cache_shape = vec![num_layers, num_kv_heads, max_seq_len, head_dim];
let keys = GpuTensor::zeros(cache_shape.clone(), GpuDType::F16, Arc::clone(&device))?;
let values = GpuTensor::zeros(cache_shape, GpuDType::F16, Arc::clone(&device))?;
let cache_bytes = keys.size_bytes() + values.size_bytes();
tracing::info!(
cache_mb = cache_bytes as f64 / 1024.0 / 1024.0,
max_seq_len = max_seq_len,
num_layers = num_layers,
kv_heads = num_kv_heads,
head_dim = head_dim,
"Created KV cache"
);
Ok(Self {
config: config.clone(),
device,
keys,
values,
seq_len: 0,
max_seq_len,
pending_tokens: 0,
})
}
pub fn seq_len(&self) -> usize {
self.seq_len
}
pub fn max_seq_len(&self) -> usize {
self.max_seq_len
}
pub fn num_layers(&self) -> usize {
self.config.num_layers
}
pub fn reset(&mut self) {
self.seq_len = 0;
self.pending_tokens = 0;
}
pub fn update(
&mut self,
layer_idx: usize,
keys: &GpuTensor,
values: &GpuTensor,
) -> Result<usize, InferenceError> {
let new_tokens = keys.shape()[0];
if self.seq_len + new_tokens > self.max_seq_len {
return Err(InferenceError::Shape {
expected: format!("seq_len + new_tokens <= {}", self.max_seq_len),
got: format!(
"{} + {} = {}",
self.seq_len,
new_tokens,
self.seq_len + new_tokens
),
});
}
let write_offset = self.seq_len;
self.keys.write_layer_at(layer_idx, write_offset, keys)?;
self.values
.write_layer_at(layer_idx, write_offset, values)?;
self.pending_tokens = new_tokens;
Ok(write_offset)
}
pub fn advance(&mut self, num_new_tokens: usize) {
self.seq_len += num_new_tokens;
self.pending_tokens = 0;
}
pub unsafe fn update_async(
&mut self,
layer_idx: usize,
keys: &GpuTensor,
values: &GpuTensor,
stream: *mut c_void,
) -> Result<usize, InferenceError> {
let new_tokens = keys.shape()[0];
if self.seq_len + new_tokens > self.max_seq_len {
return Err(InferenceError::Shape {
expected: format!("seq_len + new_tokens <= {}", self.max_seq_len),
got: format!(
"{} + {} = {}",
self.seq_len,
new_tokens,
self.seq_len + new_tokens
),
});
}
let write_offset = self.seq_len;
self.keys
.write_layer_at_async(layer_idx, write_offset, keys, stream)?;
self.values
.write_layer_at_async(layer_idx, write_offset, values, stream)?;
self.pending_tokens = new_tokens;
Ok(write_offset)
}
pub fn get_kv(&self, layer_idx: usize) -> Result<(GpuTensor, GpuTensor), InferenceError> {
if layer_idx >= self.config.num_layers {
return Err(InferenceError::Shape {
expected: format!("layer_idx < {}", self.config.num_layers),
got: format!("{}", layer_idx),
});
}
let total_tokens = self.seq_len + self.pending_tokens;
let effective_seq = if total_tokens > 0 { total_tokens } else { 1 };
let k_slice = self.keys.get_layer_kv_slice(layer_idx, effective_seq)?;
let v_slice = self.values.get_layer_kv_slice(layer_idx, effective_seq)?;
let k_shape = k_slice.shape();
let k_4d = k_slice.reshape(vec![1, k_shape[0], k_shape[1], k_shape[2]])?;
let v_shape = v_slice.shape();
let v_4d = v_slice.reshape(vec![1, v_shape[0], v_shape[1], v_shape[2]])?;
Ok((k_4d, v_4d))
}
pub fn keys_for_layer(&self, layer_idx: usize) -> Result<GpuTensor, InferenceError> {
self.keys.slice_layer(layer_idx)
}
pub fn values_for_layer(&self, layer_idx: usize) -> Result<GpuTensor, InferenceError> {
self.values.slice_layer(layer_idx)
}
pub fn has_capacity(&self, num_tokens: usize) -> bool {
self.seq_len + num_tokens <= self.max_seq_len
}
pub fn remaining_capacity(&self) -> usize {
self.max_seq_len - self.seq_len
}
pub fn config(&self) -> &ModelConfig {
&self.config
}
pub fn device(&self) -> &Arc<CudaDevice> {
&self.device
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cuda_inference::arch::{Activation, ModelArch};
fn test_config() -> ModelConfig {
ModelConfig {
arch: ModelArch::Llama,
vocab_size: 32000,
hidden_size: 576,
intermediate_size: 1536,
num_layers: 4,
num_attention_heads: 9,
num_kv_heads: 3,
head_dim: 64,
max_seq_len: 2048,
rms_norm_eps: 1e-5,
rope_theta: 10000.0,
rope_scaling: None,
attention_bias: false,
mlp_bias: false,
hidden_act: Activation::SiLU,
tie_word_embeddings: true,
sliding_window: None,
bos_token_id: 1,
eos_token_id: 2,
pad_token_id: None,
}
}
#[test]
fn test_kv_cache_tracking() {
let config = test_config();
assert_eq!(config.num_layers, 4);
assert_eq!(config.num_kv_heads, 3);
assert_eq!(config.head_dim, 64);
}
}