use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, RwLock};
use fuel_dispatch::{
optimize::OptimizedGraph,
pipelined::{PipelinedExecutor, StorageCache},
};
use fuel_graph::{Graph, Node, NodeId, Op};
use fuel_ir::{DType, DeviceLocation, Error, Layout, Result, SType, Shape, SymEnv, SymId};
use fuel_memory::{BackendStorage, Storage};
use crate::Device;
use crate::decode_shape::KvAllocId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KvLayerId {
pub layer_idx: usize,
pub slot: KvSlot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KvSlot {
K,
V,
}
#[derive(Debug, Clone)]
pub enum AuthorityState {
Host,
Device(DeviceLocation),
}
pub struct KvLayer {
pub k: Arc<RwLock<Storage>>,
pub v: Arc<RwLock<Storage>>,
pub k_layout: Layout,
pub v_layout: Layout,
pub k_version: u64,
pub v_version: u64,
pub k_authority: AuthorityState,
pub v_authority: AuthorityState,
}
pub struct KvCache {
pub layers: Vec<Option<KvLayer>>,
pub cached_len: usize,
pub n_kv_heads: usize,
pub head_dim: usize,
alloc_id: KvAllocId,
pub max_seq_len: Option<usize>,
pub dtype: Option<DType>,
device_location: Option<DeviceLocation>,
}
impl KvCache {
pub fn with_dims(n_layers: usize, n_kv_heads: usize, head_dim: usize) -> Self {
Self {
layers: (0..n_layers).map(|_| None).collect(),
cached_len: 0,
n_kv_heads,
head_dim,
max_seq_len: None,
dtype: None,
alloc_id: KvAllocId::next(),
device_location: None,
}
}
pub fn with_capacity(
n_layers: usize,
n_kv_heads: usize,
head_dim: usize,
max_seq_len: usize,
dtype: DType,
device: &Device,
) -> Result<Self> {
let shape = Shape::from_dims(&[1, n_kv_heads, max_seq_len, head_dim]);
let layout = Layout::contiguous(shape.clone());
let target_loc = device.location();
let graph = Arc::new(RwLock::new(Graph::new()));
let mut cache = StorageCache::new();
if let Some(seed) = crate::pipelined_bridge::device_seed_storage(device)? {
let anchor_id = {
let mut g = graph.write().map_err(|_| {
Error::Msg("graph lock poisoned during KvCache build".into()).bt()
})?;
g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[4]),
dtype: DType::U8,
})
};
cache.insert(anchor_id, Arc::new(RwLock::new(seed)));
}
let zero_fill_ids: Vec<NodeId> = {
let mut g = graph
.write()
.map_err(|_| Error::Msg("graph lock poisoned during KvCache build".into()).bt())?;
(0..(2 * n_layers))
.map(|_| {
let alloc_id = g.push(Node {
op: Op::Alloc { target: target_loc },
inputs: vec![],
shape: shape.clone(),
dtype,
});
g.push(Node {
op: Op::ZeroFill,
inputs: vec![alloc_id],
shape: shape.clone(),
dtype,
})
})
.collect()
};
let realized = PipelinedExecutor::realize_many(Arc::clone(&graph), &zero_fill_ids, cache)?;
if realized.len() != 2 * n_layers {
return Err(Error::Msg(format!(
"KvCache::with_capacity: realize_many returned {} storages \
for {} Op::ZeroFill targets — internal bug",
realized.len(),
2 * n_layers,
))
.bt());
}
let mut realized_iter = realized.into_iter();
let short = |what: &str, l: usize| {
Error::Msg(format!(
"alloc_kv: realize_many yielded fewer storages than expected \
(ran out at layer {l} fetching {what})"
))
.bt()
};
let mut layers: Vec<Option<KvLayer>> = Vec::with_capacity(n_layers);
for l in 0..n_layers {
let (k_arc, _) = realized_iter.next().ok_or_else(|| short("K", l))?;
let (v_arc, _) = realized_iter.next().ok_or_else(|| short("V", l))?;
layers.push(Some(KvLayer {
k: k_arc,
v: v_arc,
k_layout: layout.clone(),
v_layout: layout.clone(),
k_version: 0,
v_version: 0,
k_authority: AuthorityState::Host,
v_authority: AuthorityState::Host,
}));
}
let _ = (n_kv_heads, max_seq_len, head_dim);
Ok(Self {
layers,
cached_len: 0,
n_kv_heads,
head_dim,
max_seq_len: Some(max_seq_len),
dtype: Some(dtype),
alloc_id: KvAllocId::next(),
device_location: Some(target_loc),
})
}
pub fn slot_storage(&self, layer_idx: usize, slot: KvSlot) -> Option<Arc<RwLock<Storage>>> {
let layer = self.layer(layer_idx)?;
Some(match slot {
KvSlot::K => Arc::clone(&layer.k),
KvSlot::V => Arc::clone(&layer.v),
})
}
pub fn bump_version(&mut self, layer_idx: usize, slot: KvSlot) {
if let Some(layer) = self.layer_mut(layer_idx) {
match slot {
KvSlot::K => layer.k_version += 1,
KvSlot::V => layer.v_version += 1,
}
}
}
pub fn n_layers(&self) -> usize {
self.layers.len()
}
pub fn layer(&self, li: usize) -> Option<&KvLayer> {
self.layers.get(li).and_then(|o| o.as_ref())
}
pub fn layer_mut(&mut self, li: usize) -> Option<&mut KvLayer> {
self.layers.get_mut(li).and_then(|o| o.as_mut())
}
pub fn alloc_id(&self) -> KvAllocId {
self.alloc_id
}
pub fn device_location(&self) -> Option<DeviceLocation> {
self.device_location
}
pub fn set_layer(&mut self, li: usize, layer: KvLayer) {
self.layers[li] = Some(layer);
self.alloc_id = KvAllocId::next();
self.device_location = None;
}
pub fn clear(&mut self) {
for layer in &mut self.layers {
*layer = None;
}
self.cached_len = 0;
self.alloc_id = KvAllocId::next();
self.device_location = None;
}
pub fn truncate_to(&mut self, new_len: usize) {
if new_len < self.cached_len {
self.cached_len = new_len;
}
}
pub fn write_bytes(
&self,
layer_idx: usize,
slot: KvSlot,
start: usize,
n: usize,
bytes: &[u8],
device: &Device,
) -> Result<()> {
let dtype = self.dtype.ok_or_else(|| {
Error::Msg(
"KvCache::write_bytes: cache has no fixed dtype — not built via with_capacity"
.into(),
)
.bt()
})?;
let max_seq_len = self.max_seq_len.ok_or_else(|| {
Error::Msg(
"KvCache::write_bytes: cache has no fixed capacity — not built via with_capacity"
.into(),
)
.bt()
})?;
if start + n > max_seq_len {
return Err(Error::Msg(format!(
"KvCache::write_bytes: range [{start}, {}) exceeds max_seq_len {max_seq_len}",
start + n,
))
.bt());
}
let elem_bytes = kv_cache_dtype_elem_bytes(dtype)?;
let want = n * self.n_kv_heads * self.head_dim * elem_bytes;
if bytes.len() != want {
return Err(Error::Msg(format!(
"KvCache::write_bytes: {} bytes != expected {want} \
([1, n_kv_heads {}, n {n}, head_dim {}] for {dtype:?})",
bytes.len(),
self.n_kv_heads,
self.head_dim,
))
.bt());
}
let layer = self.layer(layer_idx).ok_or_else(|| {
Error::Msg(format!(
"KvCache::write_bytes: layer {layer_idx} out of range or unpopulated"
))
.bt()
})?;
let buf = match slot {
KvSlot::K => Arc::clone(&layer.k),
KvSlot::V => Arc::clone(&layer.v),
};
let full_shape = Shape::from_dims(&[1, self.n_kv_heads, max_seq_len, self.head_dim]);
let src = kv_cache_const_from_bytes(
dtype,
bytes,
Shape::from_dims(&[1, self.n_kv_heads, n, self.head_dim]),
device,
)?;
let dest = src.const_placeholder_like(full_shape, dtype);
let ranges = vec![
(0, 1),
(0, self.n_kv_heads),
(start, start + n),
(0, self.head_dim),
];
let post = dest.write_slice(&src, ranges)?;
let mut cache = StorageCache::new();
cache.insert(dest.node_id(), buf);
let _ = crate::pipelined_bridge::realize_one_bytes_with_initial(
post.graph_handle(),
post.node_id(),
device,
cache,
)?;
Ok(())
}
}
fn kv_cache_dtype_elem_bytes(dtype: DType) -> Result<usize> {
match dtype {
DType::F32 => Ok(4),
DType::BF16 => Ok(2),
other => Err(Error::Msg(format!(
"KvCache: byte movement unsupported for dtype {other:?} (F32/BF16)",
))
.bt()),
}
}
fn kv_cache_const_from_bytes(
dtype: DType,
bytes: &[u8],
shape: Shape,
device: &Device,
) -> Result<crate::lazy::Tensor> {
match dtype {
DType::F32 => {
let v: &[f32] = bytemuck::try_cast_slice(bytes).map_err(|e| {
Error::Msg(format!("kv_cache_const_from_bytes: f32 cast: {e:?}")).bt()
})?;
crate::lazy::Tensor::from_f32(v.to_vec(), shape, device)
}
DType::BF16 => {
let v: &[half::bf16] = bytemuck::try_cast_slice(bytes).map_err(|e| {
Error::Msg(format!("kv_cache_const_from_bytes: bf16 cast: {e:?}")).bt()
})?;
let anchor =
crate::lazy::Tensor::from_f32(vec![0.0f32], Shape::from_dims(&[1]), device)?;
crate::lazy::Tensor::from_bf16_on(anchor.graph(), v.to_vec(), shape, device)
}
other => Err(Error::Msg(format!(
"kv_cache_const_from_bytes: unsupported dtype {other:?} (F32/BF16)",
))
.bt()),
}
}
type KvStoragePair = (Arc<RwLock<Storage>>, Arc<RwLock<Storage>>);
pub fn alloc_batched_kv(
k: usize,
n_layers: usize,
n_kv_heads: usize,
head_dim: usize,
max_seq_len: usize,
dtype: DType,
device: &Device,
) -> Result<Vec<KvStoragePair>> {
let shape = Shape::from_dims(&[k, n_kv_heads, max_seq_len, head_dim]);
let target_loc = device.location();
let graph = Arc::new(RwLock::new(Graph::new()));
let mut cache = StorageCache::new();
if let Some(seed) = crate::pipelined_bridge::device_seed_storage(device)? {
let anchor_id = {
let mut g = graph.write().map_err(|_| {
Error::Msg("graph lock poisoned during alloc_batched_kv".into()).bt()
})?;
g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[4]),
dtype: DType::U8,
})
};
cache.insert(anchor_id, Arc::new(RwLock::new(seed)));
}
let zero_fill_ids: Vec<NodeId> = {
let mut g = graph
.write()
.map_err(|_| Error::Msg("graph lock poisoned during alloc_batched_kv".into()).bt())?;
(0..(2 * n_layers))
.map(|_| {
let alloc_id = g.push(Node {
op: Op::Alloc { target: target_loc },
inputs: vec![],
shape: shape.clone(),
dtype,
});
g.push(Node {
op: Op::ZeroFill,
inputs: vec![alloc_id],
shape: shape.clone(),
dtype,
})
})
.collect()
};
let realized = PipelinedExecutor::realize_many(Arc::clone(&graph), &zero_fill_ids, cache)?;
if realized.len() != 2 * n_layers {
return Err(Error::Msg(format!(
"alloc_batched_kv: realize_many returned {} storages for {} Op::ZeroFill targets",
realized.len(),
2 * n_layers,
))
.bt());
}
let mut it = realized.into_iter();
let mut pairs = Vec::with_capacity(n_layers);
let short = || {
Error::Msg(
"alloc_batched_kv: realize_many yielded fewer storages than expected".to_string(),
)
.bt()
};
for _ in 0..n_layers {
let (k_arc, _) = it.next().ok_or_else(short)?;
let (v_arc, _) = it.next().ok_or_else(short)?;
pairs.push((k_arc, v_arc));
}
Ok(pairs)
}
pub struct LatentSlot {
pub storage: Arc<RwLock<Storage>>,
pub layout: Layout,
pub version: u64,
pub authority: AuthorityState,
}
pub struct LatentKvCache {
pub layers: Vec<Option<Vec<LatentSlot>>>,
pub cached_len: usize,
pub slot_trailing: Vec<Vec<usize>>,
pub max_seq_len: usize,
pub dtype: DType,
alloc_id: KvAllocId,
device_location: DeviceLocation,
}
impl LatentKvCache {
pub fn with_capacity(
n_layers: usize,
max_seq_len: usize,
slot_trailing: Vec<Vec<usize>>,
dtype: DType,
device: &Device,
) -> Result<Self> {
if n_layers == 0 {
return Err(
Error::Msg("LatentKvCache::with_capacity: n_layers must be >= 1".into()).bt(),
);
}
if max_seq_len == 0 {
return Err(Error::Msg(
"LatentKvCache::with_capacity: max_seq_len must be >= 1".into(),
)
.bt());
}
if slot_trailing.is_empty() {
return Err(Error::Msg(
"LatentKvCache::with_capacity: need at least one slot (slot_trailing empty)".into(),
)
.bt());
}
let n_slots = slot_trailing.len();
let shapes: Vec<Shape> = slot_trailing
.iter()
.map(|trailing| {
let mut dims = Vec::with_capacity(1 + trailing.len());
dims.push(max_seq_len);
dims.extend_from_slice(trailing);
Shape::from_dims(&dims)
})
.collect();
let layouts: Vec<Layout> = shapes
.iter()
.map(|s| Layout::contiguous(s.clone()))
.collect();
let target_loc = device.location();
let graph = Arc::new(RwLock::new(Graph::new()));
let mut cache = StorageCache::new();
if let Some(seed) = crate::pipelined_bridge::device_seed_storage(device)? {
let anchor_id = {
let mut g = graph.write().map_err(|_| {
Error::Msg("graph lock poisoned during LatentKvCache build".into()).bt()
})?;
g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[4]),
dtype: DType::U8,
})
};
cache.insert(anchor_id, Arc::new(RwLock::new(seed)));
}
let mut zero_fill_ids: Vec<NodeId> = Vec::with_capacity(n_layers * n_slots);
{
let mut g = graph.write().map_err(|_| {
Error::Msg("graph lock poisoned during LatentKvCache build".into()).bt()
})?;
for _ in 0..n_layers {
for shape in &shapes {
let alloc_id = g.push(Node {
op: Op::Alloc { target: target_loc },
inputs: vec![],
shape: shape.clone(),
dtype,
});
let zf_id = g.push(Node {
op: Op::ZeroFill,
inputs: vec![alloc_id],
shape: shape.clone(),
dtype,
});
zero_fill_ids.push(zf_id);
}
}
}
let realized = PipelinedExecutor::realize_many(Arc::clone(&graph), &zero_fill_ids, cache)?;
if realized.len() != n_layers * n_slots {
return Err(Error::Msg(format!(
"LatentKvCache::with_capacity: realize_many returned {} storages \
for {} Op::ZeroFill targets — internal bug",
realized.len(),
n_layers * n_slots,
))
.bt());
}
let mut realized_iter = realized.into_iter();
let short = |l: usize| {
Error::Msg(format!(
"alloc_latent: realize_many yielded fewer storages than \
expected (ran out at layer {l})"
))
.bt()
};
let mut layers: Vec<Option<Vec<LatentSlot>>> = Vec::with_capacity(n_layers);
for l in 0..n_layers {
let mut slots = Vec::with_capacity(n_slots);
for layout in &layouts {
let (arc, _) = realized_iter.next().ok_or_else(|| short(l))?;
slots.push(LatentSlot {
storage: arc,
layout: layout.clone(),
version: 0,
authority: AuthorityState::Host,
});
}
layers.push(Some(slots));
}
Ok(Self {
layers,
cached_len: 0,
slot_trailing,
max_seq_len,
dtype,
alloc_id: KvAllocId::next(),
device_location: target_loc,
})
}
pub fn slot_storage(&self, layer_idx: usize, slot: usize) -> Option<Arc<RwLock<Storage>>> {
let layer = self.layers.get(layer_idx)?.as_ref()?;
layer.get(slot).map(|s| Arc::clone(&s.storage))
}
pub fn bump_version(&mut self, layer_idx: usize, slot: usize) {
if let Some(Some(layer)) = self.layers.get_mut(layer_idx)
&& let Some(s) = layer.get_mut(slot)
{
s.version += 1;
}
}
pub fn n_layers(&self) -> usize {
self.layers.len()
}
pub fn n_slots(&self) -> usize {
self.slot_trailing.len()
}
pub fn slot_trailing(&self, s: usize) -> &[usize] {
&self.slot_trailing[s]
}
pub fn alloc_id(&self) -> KvAllocId {
self.alloc_id
}
pub fn clear(&mut self) {
for layer in &mut self.layers {
*layer = None;
}
self.cached_len = 0;
self.alloc_id = KvAllocId::next();
}
pub fn truncate_to(&mut self, new_len: usize) {
if new_len < self.cached_len {
self.cached_len = new_len;
}
}
}
pub trait KvRebindSource {
fn alloc_id(&self) -> KvAllocId;
fn rebind_location(&self) -> Option<DeviceLocation>;
fn rebind_slot(&self, layer: usize, which: usize) -> Option<Arc<RwLock<Storage>>>;
}
impl KvRebindSource for KvCache {
fn alloc_id(&self) -> KvAllocId {
KvCache::alloc_id(self)
}
fn rebind_location(&self) -> Option<DeviceLocation> {
self.device_location
}
fn rebind_slot(&self, layer: usize, which: usize) -> Option<Arc<RwLock<Storage>>> {
let slot = match which {
0 => KvSlot::K,
1 => KvSlot::V,
_ => return None,
};
self.slot_storage(layer, slot)
}
}
impl KvRebindSource for LatentKvCache {
fn alloc_id(&self) -> KvAllocId {
LatentKvCache::alloc_id(self)
}
fn rebind_location(&self) -> Option<DeviceLocation> {
Some(self.device_location)
}
fn rebind_slot(&self, layer: usize, which: usize) -> Option<Arc<RwLock<Storage>>> {
self.slot_storage(layer, which)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RebindRefusal {
ResidencyUnknown,
ResidencyDiffers {
baked: DeviceLocation,
incoming: DeviceLocation,
},
MissingSlot { layer: usize, which: usize },
UnbakedSlot { layer: usize, which: usize },
StorageDiffers { layer: usize, which: usize },
LockPoisoned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlanValidity {
Valid,
AllocationChanged,
Stale,
}
#[derive(PartialEq, Eq)]
struct StorageFingerprint {
dtype: DType,
stype: SType,
bundled: bool,
backend: std::mem::Discriminant<BackendStorage>,
len_bytes: usize,
}
fn fingerprint(s: &Arc<RwLock<Storage>>) -> Option<StorageFingerprint> {
let g = s.read().ok()?;
Some(StorageFingerprint {
dtype: g.dtype,
stype: g.stype.clone(),
bundled: g.bundle.is_some(),
backend: std::mem::discriminant(&g.inner),
len_bytes: g.inner.len_bytes(),
})
}
fn storage_is_interchangeable(
baked: &Arc<RwLock<Storage>>,
incoming: &Arc<RwLock<Storage>>,
) -> Option<bool> {
if Arc::ptr_eq(baked, incoming) {
return Some(true);
}
let a = fingerprint(baked)?;
let b = fingerprint(incoming)?;
Some(a == b)
}
pub struct DecodeTokenData {
pub token_ids: Arc<RwLock<Storage>>,
pub rope_cos: Arc<RwLock<Storage>>,
pub rope_sin: Arc<RwLock<Storage>>,
pub mask: Arc<RwLock<Storage>>,
pub offset: Option<Arc<RwLock<Storage>>>,
}
pub struct DecodeSession {
graph: Arc<RwLock<Graph>>,
optimized: OptimizedGraph,
effective_target: NodeId,
logits_node: NodeId,
token_ids_node: NodeId,
rope_cos_node: NodeId,
rope_sin_node: NodeId,
mask_node: NodeId,
kv_nodes: Vec<(NodeId, NodeId)>,
offset_node: Option<NodeId>,
cached_len_sym: SymId,
attended_len_sym: SymId,
base_cache: StorageCache,
seq: usize,
max_seq_len: usize,
n_layers: usize,
cache_dtype: DType,
shape_key: u64,
alloc_id: KvAllocId,
kv_location: Option<DeviceLocation>,
order_cache: std::sync::OnceLock<Vec<NodeId>>,
}
impl DecodeSession {
#[allow(clippy::too_many_arguments)]
pub fn new(
graph: Arc<RwLock<Graph>>,
optimized: OptimizedGraph,
effective_target: NodeId,
logits_node: NodeId,
token_ids_node: NodeId,
rope_cos_node: NodeId,
rope_sin_node: NodeId,
mask_node: NodeId,
kv_nodes: Vec<(NodeId, NodeId)>,
offset_node: Option<NodeId>,
cached_len_sym: SymId,
attended_len_sym: SymId,
base_cache: StorageCache,
seq: usize,
max_seq_len: usize,
n_layers: usize,
cache_dtype: DType,
shape_key: u64,
kv: &dyn KvRebindSource,
) -> Self {
Self {
shape_key,
alloc_id: kv.alloc_id(),
kv_location: kv.rebind_location(),
graph,
optimized,
effective_target,
logits_node,
token_ids_node,
rope_cos_node,
rope_sin_node,
mask_node,
kv_nodes,
offset_node,
cached_len_sym,
attended_len_sym,
base_cache,
seq,
max_seq_len,
n_layers,
cache_dtype,
order_cache: std::sync::OnceLock::new(),
}
}
pub fn realize_token(
&self,
device: &Device,
data: DecodeTokenData,
sym_env: &SymEnv,
) -> Result<Vec<f32>> {
let mut cache = self.base_cache.clone();
cache.insert(self.token_ids_node, data.token_ids);
cache.insert(self.rope_cos_node, data.rope_cos);
cache.insert(self.rope_sin_node, data.rope_sin);
cache.insert(self.mask_node, data.mask);
if let (Some(offset_node), Some(offset)) = (self.offset_node, data.offset) {
cache.insert(offset_node, offset);
}
crate::pipelined_bridge::realize_one_prebuilt_cached_env::<f32>(
&self.graph,
self.effective_target,
&self.optimized,
&self.order_cache,
device,
cache,
sym_env,
)
}
pub fn is_valid_for(
&self,
seq: usize,
max_seq_len: usize,
n_layers: usize,
cache_dtype: DType,
shape_key: u64,
alloc_id: KvAllocId,
) -> bool {
matches!(
self.validity_for(seq, max_seq_len, n_layers, cache_dtype, shape_key, alloc_id),
PlanValidity::Valid,
)
}
pub fn validity_for(
&self,
seq: usize,
max_seq_len: usize,
n_layers: usize,
cache_dtype: DType,
shape_key: u64,
alloc_id: KvAllocId,
) -> PlanValidity {
let structural = self.seq == seq
&& self.max_seq_len == max_seq_len
&& self.n_layers == n_layers
&& self.cache_dtype == cache_dtype
&& self.shape_key == shape_key;
if !structural {
return PlanValidity::Stale;
}
if self.alloc_id == alloc_id {
PlanValidity::Valid
} else {
PlanValidity::AllocationChanged
}
}
pub fn rebind_kv(
&mut self,
kv: &dyn KvRebindSource,
) -> core::result::Result<(), RebindRefusal> {
let incoming_loc = kv
.rebind_location()
.ok_or(RebindRefusal::ResidencyUnknown)?;
let baked_loc = self.kv_location.ok_or(RebindRefusal::ResidencyUnknown)?;
if baked_loc != incoming_loc {
return Err(RebindRefusal::ResidencyDiffers {
baked: baked_loc,
incoming: incoming_loc,
});
}
let mut staged: Vec<(NodeId, Arc<RwLock<Storage>>)> =
Vec::with_capacity(self.kv_nodes.len() * 2);
for (layer, &(k_node, v_node)) in self.kv_nodes.iter().enumerate() {
for (which, node) in [(0usize, k_node), (1usize, v_node)] {
let incoming = kv
.rebind_slot(layer, which)
.ok_or(RebindRefusal::MissingSlot { layer, which })?;
let baked = self
.base_cache
.get(&node)
.ok_or(RebindRefusal::UnbakedSlot { layer, which })?;
if !storage_is_interchangeable(baked, &incoming)
.ok_or(RebindRefusal::LockPoisoned)?
{
return Err(RebindRefusal::StorageDiffers { layer, which });
}
staged.push((node, incoming));
}
}
for (node, arc) in staged {
self.base_cache.insert(node, arc);
}
self.alloc_id = kv.alloc_id();
Ok(())
}
pub fn graph(&self) -> &Arc<RwLock<Graph>> {
&self.graph
}
pub fn optimized(&self) -> &OptimizedGraph {
&self.optimized
}
pub fn effective_target(&self) -> NodeId {
self.effective_target
}
pub fn logits_node(&self) -> NodeId {
self.logits_node
}
pub fn token_ids_node(&self) -> NodeId {
self.token_ids_node
}
pub fn rope_cos_node(&self) -> NodeId {
self.rope_cos_node
}
pub fn rope_sin_node(&self) -> NodeId {
self.rope_sin_node
}
pub fn mask_node(&self) -> NodeId {
self.mask_node
}
pub fn kv_nodes(&self) -> &[(NodeId, NodeId)] {
&self.kv_nodes
}
pub fn offset_node(&self) -> Option<NodeId> {
self.offset_node
}
pub fn cached_len_sym(&self) -> SymId {
self.cached_len_sym
}
pub fn attended_len_sym(&self) -> SymId {
self.attended_len_sym
}
pub fn max_seq_len(&self) -> usize {
self.max_seq_len
}
pub fn per_token_sym_env(&self, cached_len: usize) -> Result<SymEnv> {
let mut env = SymEnv::new();
env.bind(self.cached_len_sym, cached_len)?;
env.bind(self.attended_len_sym, cached_len + self.seq)?;
Ok(env)
}
pub fn graph_node_count(&self) -> usize {
self.graph.read().map(|g| g.len()).unwrap_or(0)
}
pub fn base_cache(&self) -> &StorageCache {
&self.base_cache
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PagedDecodePlan {
Replan,
PlanOnce,
}
pub struct PagedDecodeTokenData {
pub token_ids: Arc<RwLock<Storage>>,
pub rope_cos: Arc<RwLock<Storage>>,
pub rope_sin: Arc<RwLock<Storage>>,
pub block_table: Arc<RwLock<Storage>>,
pub context_lens: Arc<RwLock<Storage>>,
pub offset: Option<Arc<RwLock<Storage>>>,
}
pub struct PagedDecodeSession {
graph: Arc<RwLock<Graph>>,
optimized: OptimizedGraph,
effective_target: NodeId,
logits_node: NodeId,
token_ids_node: NodeId,
rope_cos_node: NodeId,
rope_sin_node: NodeId,
block_table_node: NodeId,
context_lens_node: NodeId,
kv_nodes: Vec<(NodeId, NodeId)>,
offset_node: Option<NodeId>,
write_sym: SymId,
base_cache: StorageCache,
max_blocks_cap: usize,
n_layers: usize,
block_size: usize,
cache_dtype: DType,
shape_key: u64,
alloc_id: KvAllocId,
realize_count: AtomicUsize,
order_cache: std::sync::OnceLock<Vec<NodeId>>,
}
impl PagedDecodeSession {
#[allow(clippy::too_many_arguments)]
pub fn new(
graph: Arc<RwLock<Graph>>,
optimized: OptimizedGraph,
effective_target: NodeId,
logits_node: NodeId,
token_ids_node: NodeId,
rope_cos_node: NodeId,
rope_sin_node: NodeId,
block_table_node: NodeId,
context_lens_node: NodeId,
kv_nodes: Vec<(NodeId, NodeId)>,
offset_node: Option<NodeId>,
write_sym: SymId,
base_cache: StorageCache,
max_blocks_cap: usize,
n_layers: usize,
block_size: usize,
cache_dtype: DType,
shape_key: u64,
alloc_id: KvAllocId,
) -> Self {
Self {
alloc_id,
graph,
optimized,
effective_target,
logits_node,
token_ids_node,
rope_cos_node,
rope_sin_node,
block_table_node,
context_lens_node,
kv_nodes,
offset_node,
write_sym,
base_cache,
max_blocks_cap,
n_layers,
block_size,
cache_dtype,
shape_key,
realize_count: AtomicUsize::new(0),
order_cache: std::sync::OnceLock::new(),
}
}
pub fn realize_token(
&self,
device: &Device,
data: PagedDecodeTokenData,
sym_env: &SymEnv,
) -> Result<Vec<f32>> {
let mut cache = self.base_cache.clone();
cache.insert(self.token_ids_node, data.token_ids);
cache.insert(self.rope_cos_node, data.rope_cos);
cache.insert(self.rope_sin_node, data.rope_sin);
cache.insert(self.block_table_node, data.block_table);
cache.insert(self.context_lens_node, data.context_lens);
if let (Some(offset_node), Some(offset)) = (self.offset_node, data.offset) {
cache.insert(offset_node, offset);
}
self.realize_count.fetch_add(1, Ordering::Relaxed);
crate::pipelined_bridge::realize_one_prebuilt_cached_env::<f32>(
&self.graph,
self.effective_target,
&self.optimized,
&self.order_cache,
device,
cache,
sym_env,
)
}
pub fn is_valid_for(
&self,
max_blocks_cap: usize,
n_layers: usize,
block_size: usize,
cache_dtype: DType,
shape_key: u64,
alloc_id: KvAllocId,
) -> bool {
self.max_blocks_cap == max_blocks_cap
&& self.n_layers == n_layers
&& self.block_size == block_size
&& self.cache_dtype == cache_dtype
&& self.shape_key == shape_key
&& self.alloc_id == alloc_id
}
pub fn per_token_sym_env(&self, linear: usize) -> Result<SymEnv> {
let mut env = SymEnv::new();
env.bind(self.write_sym, linear)?;
Ok(env)
}
pub fn graph(&self) -> &Arc<RwLock<Graph>> {
&self.graph
}
pub fn optimized(&self) -> &OptimizedGraph {
&self.optimized
}
pub fn effective_target(&self) -> NodeId {
self.effective_target
}
pub fn logits_node(&self) -> NodeId {
self.logits_node
}
pub fn token_ids_node(&self) -> NodeId {
self.token_ids_node
}
pub fn rope_cos_node(&self) -> NodeId {
self.rope_cos_node
}
pub fn rope_sin_node(&self) -> NodeId {
self.rope_sin_node
}
pub fn block_table_node(&self) -> NodeId {
self.block_table_node
}
pub fn context_lens_node(&self) -> NodeId {
self.context_lens_node
}
pub fn kv_nodes(&self) -> &[(NodeId, NodeId)] {
&self.kv_nodes
}
pub fn offset_node(&self) -> Option<NodeId> {
self.offset_node
}
pub fn write_sym(&self) -> SymId {
self.write_sym
}
pub fn max_blocks_cap(&self) -> usize {
self.max_blocks_cap
}
pub fn realize_count(&self) -> usize {
self.realize_count.load(Ordering::Relaxed)
}
pub fn graph_node_count(&self) -> usize {
self.graph.read().map(|g| g.len()).unwrap_or(0)
}
pub fn base_cache(&self) -> &StorageCache {
&self.base_cache
}
}
pub struct InferenceContext {
device: Device,
persistent: HashMap<NodeId, Arc<RwLock<Storage>>>,
decode_session: Option<DecodeSession>,
}
impl InferenceContext {
pub fn new(device: Device) -> Self {
Self {
device,
persistent: HashMap::new(),
decode_session: None,
}
}
pub fn device(&self) -> &Device {
&self.device
}
pub fn take_decode_session(&mut self) -> Option<DecodeSession> {
self.decode_session.take()
}
pub fn put_decode_session(&mut self, session: Option<DecodeSession>) {
self.decode_session = session;
}
pub fn has_decode_session(&self) -> bool {
self.decode_session.is_some()
}
pub fn insert(&mut self, node_id: NodeId, storage: Arc<RwLock<Storage>>) {
self.persistent.insert(node_id, storage);
}
pub fn remove(&mut self, node_id: NodeId) -> Option<Arc<RwLock<Storage>>> {
self.persistent.remove(&node_id)
}
pub fn contains(&self, node_id: NodeId) -> bool {
self.persistent.contains_key(&node_id)
}
pub fn get(&self, node_id: NodeId) -> Option<&Arc<RwLock<Storage>>> {
self.persistent.get(&node_id)
}
pub fn len(&self) -> usize {
self.persistent.len()
}
pub fn is_empty(&self) -> bool {
self.persistent.is_empty()
}
pub fn clear_persistent(&mut self) {
self.persistent.clear();
}
pub fn realize_one_as<T: bytemuck::Pod + fuel_ir::WithDType>(
&self,
graph: &Arc<RwLock<Graph>>,
target: NodeId,
) -> Result<Vec<T>> {
self.realize_one_as_with_env::<T>(graph, target, &SymEnv::default())
}
pub fn realize_one_as_with_env<T: bytemuck::Pod + fuel_ir::WithDType>(
&self,
graph: &Arc<RwLock<Graph>>,
target: NodeId,
env: &SymEnv,
) -> Result<Vec<T>> {
crate::pipelined_bridge::realize_one_as_with_initial_env::<T>(
graph,
target,
&self.device,
self.cloned_persistent(),
env,
)
}
pub fn prebuild_optimized_as_with_env<T: bytemuck::Pod + fuel_ir::WithDType>(
&self,
graph: &Arc<RwLock<Graph>>,
target: NodeId,
env: &SymEnv,
) -> Result<(NodeId, fuel_dispatch::optimize::OptimizedGraph, Vec<T>)> {
crate::pipelined_bridge::prebuild_optimized_env::<T>(
graph,
target,
&self.device,
self.cloned_persistent(),
env,
)
}
pub fn prebuild_optimized_capturing_as_with_env<T: bytemuck::Pod + fuel_ir::WithDType>(
&self,
graph: &Arc<RwLock<Graph>>,
target: NodeId,
env: &SymEnv,
) -> Result<(
NodeId,
fuel_dispatch::optimize::OptimizedGraph,
StorageCache,
Vec<T>,
)> {
crate::pipelined_bridge::prebuild_optimized_env_capturing_cache::<T>(
graph,
target,
&self.device,
self.cloned_persistent(),
env,
)
}
pub fn realize_prebuilt_as_with_env<T: bytemuck::Pod + fuel_ir::WithDType>(
&self,
graph: &Arc<RwLock<Graph>>,
effective_target: NodeId,
optimized: &fuel_dispatch::optimize::OptimizedGraph,
env: &SymEnv,
) -> Result<Vec<T>> {
crate::pipelined_bridge::realize_one_prebuilt_env::<T>(
graph,
effective_target,
optimized,
&self.device,
self.cloned_persistent(),
env,
)
}
pub fn realize_many_as<T: bytemuck::Pod + fuel_ir::WithDType>(
&self,
graph: &Arc<RwLock<Graph>>,
targets: &[NodeId],
) -> Result<Vec<Vec<T>>> {
self.realize_many_as_with_env::<T>(graph, targets, &SymEnv::default())
}
pub fn realize_many_as_with_env<T: bytemuck::Pod + fuel_ir::WithDType>(
&self,
graph: &Arc<RwLock<Graph>>,
targets: &[NodeId],
env: &SymEnv,
) -> Result<Vec<Vec<T>>> {
crate::pipelined_bridge::realize_many_as_with_initial_env::<T>(
graph,
targets,
&self.device,
self.cloned_persistent(),
env,
)
}
pub fn realize_split_as<T: bytemuck::Pod + fuel_ir::WithDType>(
&self,
graph: &Arc<RwLock<Graph>>,
targets: &[NodeId],
n_host: usize,
) -> Result<crate::pipelined_bridge::SplitRealizeOutput<T>> {
crate::pipelined_bridge::realize_split_as_with_initial::<T>(
graph,
targets,
n_host,
&self.device,
self.cloned_persistent(),
)
}
fn cloned_persistent(&self) -> StorageCache {
let mut out = StorageCache::with_capacity(self.persistent.len());
for (id, arc) in &self.persistent {
out.insert(*id, Arc::clone(arc));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_graph::{Node, Op};
use fuel_ir::{DType, Shape};
#[test]
fn context_insert_retrieve() {
let mut ctx = InferenceContext::new(Device::cpu());
let storage = fuel_memory::from_slice_cpu(&[1.0_f32, 2.0, 3.0]);
let arc = Arc::new(RwLock::new(storage));
let id = NodeId(42);
assert!(!ctx.contains(id));
assert_eq!(ctx.len(), 0);
ctx.insert(id, Arc::clone(&arc));
assert!(ctx.contains(id));
assert_eq!(ctx.len(), 1);
let retrieved = ctx.get(id).expect("just inserted");
assert!(Arc::ptr_eq(retrieved, &arc));
}
#[test]
fn context_persistent_arc_survives_realize() {
let lhs_arc = Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
1.0_f32, 2.0, 3.0,
])));
let rhs_arc = Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[
10.0_f32, 20.0, 30.0,
])));
let graph = Arc::new(RwLock::new(Graph::new()));
let (lhs_id, rhs_id, add_id) = {
let mut g = graph.write().unwrap();
let lhs = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[3]),
dtype: DType::F32,
});
let rhs = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[3]),
dtype: DType::F32,
});
let add = g.push(Node {
op: Op::Add,
inputs: vec![lhs, rhs],
shape: Shape::from_dims(&[3]),
dtype: DType::F32,
});
(lhs, rhs, add)
};
let mut ctx = InferenceContext::new(Device::cpu());
ctx.insert(lhs_id, Arc::clone(&lhs_arc));
ctx.insert(rhs_id, Arc::clone(&rhs_arc));
assert_eq!(Arc::strong_count(&lhs_arc), 2);
assert_eq!(Arc::strong_count(&rhs_arc), 2);
let out1 = ctx
.realize_one_as::<f32>(&graph, add_id)
.expect("first realize");
assert_eq!(out1, vec![11.0, 22.0, 33.0]);
assert_eq!(Arc::strong_count(&lhs_arc), 2);
assert_eq!(Arc::strong_count(&rhs_arc), 2);
let out2 = ctx
.realize_one_as::<f32>(&graph, add_id)
.expect("second realize");
assert_eq!(out2, vec![11.0, 22.0, 33.0]);
assert_eq!(Arc::strong_count(&lhs_arc), 2);
assert_eq!(Arc::strong_count(&rhs_arc), 2);
}
fn read_kv_layer_f32(
cache: &KvCache,
layer_idx: usize,
slot: KvSlot,
device: &Device,
) -> Vec<f32> {
let max_seq_len = cache.max_seq_len.expect("with_capacity cache");
let full_shape = Shape::from_dims(&[1, cache.n_kv_heads, max_seq_len, cache.head_dim]);
let buf = match slot {
KvSlot::K => Arc::clone(&cache.layer(layer_idx).unwrap().k),
KvSlot::V => Arc::clone(&cache.layer(layer_idx).unwrap().v),
};
let anchor =
crate::lazy::Tensor::from_f32(vec![0.0f32], Shape::from_dims(&[1]), device).unwrap();
let dest = anchor.const_placeholder_like(full_shape, DType::F32);
let mut sc = StorageCache::new();
sc.insert(dest.node_id(), buf);
let bytes = crate::pipelined_bridge::realize_one_bytes_with_initial(
dest.graph_handle(),
dest.node_id(),
device,
sc,
)
.expect("readback realize");
bytemuck::try_cast_slice::<u8, f32>(&bytes)
.expect("f32 cast")
.to_vec()
}
#[test]
fn write_bytes_lands_at_the_correct_position_and_nowhere_else() {
let device = Device::cpu();
let (n_layers, n_kv_heads, head_dim, max_seq_len) = (1, 2, 3, 5);
let cache = KvCache::with_capacity(
n_layers,
n_kv_heads,
head_dim,
max_seq_len,
DType::F32,
&device,
)
.expect("with_capacity");
let alloc_before = cache.alloc_id();
let loc_before = cache.device_location();
let (start, n) = (1usize, 2usize);
let payload: Vec<f32> = (0..(n * n_kv_heads * head_dim))
.map(|i| 1000.0 + i as f32)
.collect();
cache
.write_bytes(
0,
KvSlot::K,
start,
n,
bytemuck::cast_slice(&payload),
&device,
)
.expect("write_bytes");
assert_eq!(
cache.alloc_id(),
alloc_before,
"write_bytes must not re-mint alloc_id (that would mean the Arc was replaced)",
);
assert_eq!(
cache.device_location(),
loc_before,
"write_bytes must not clear device_location (that would mean residency became unprovable)",
);
let full = read_kv_layer_f32(&cache, 0, KvSlot::K, &device);
for h in 0..n_kv_heads {
for p in 0..max_seq_len {
for d in 0..head_dim {
let got = full[h * max_seq_len * head_dim + p * head_dim + d];
if (start..start + n).contains(&p) {
let local_p = p - start;
let want = 1000.0 + (h * n * head_dim + local_p * head_dim + d) as f32;
assert_eq!(
got, want,
"head {h} position {p} dim {d}: written value must land exactly here",
);
} else {
assert_eq!(
got,
0.0,
"head {h} position {p} dim {d}: outside [{start},{}) must stay \
untouched (zero-init) — a wrong write range would corrupt this",
start + n,
);
}
}
}
}
}
#[test]
fn write_bytes_refuses_a_with_dims_cache() {
let device = Device::cpu();
let cache = KvCache::with_dims(1, 2, 3);
let err = cache
.write_bytes(0, KvSlot::K, 0, 1, &[0u8; 24], &device)
.expect_err("with_dims cache has no fixed dtype/capacity");
assert!(
err.to_string().contains("with_capacity"),
"refusal must name the real constraint, got: {err}",
);
}
#[test]
fn write_bytes_refuses_an_out_of_range_position() {
let device = Device::cpu();
let cache = KvCache::with_capacity(1, 2, 3, 5, DType::F32, &device).unwrap();
let payload = vec![0.0f32; 2 * 2 * 3]; let err = cache
.write_bytes(
0,
KvSlot::K,
4, 2,
bytemuck::cast_slice(&payload),
&device,
)
.expect_err("range must be refused, not clamped");
assert!(
err.to_string().contains("max_seq_len"),
"refusal must name the real constraint, got: {err}",
);
}
#[test]
fn write_bytes_refuses_a_mismatched_byte_length() {
let device = Device::cpu();
let cache = KvCache::with_capacity(1, 2, 3, 5, DType::F32, &device).unwrap();
let wrong_payload = vec![0.0f32; 5]; let err = cache
.write_bytes(
0,
KvSlot::K,
0,
1,
bytemuck::cast_slice(&wrong_payload),
&device,
)
.expect_err("mismatched byte length must be refused");
assert!(
err.to_string().contains("bytes"),
"refusal must name the byte-length mismatch, got: {err}",
);
}
#[test]
fn realize_one_as_with_env_resolves_write_slice_offset() {
use fuel_ir::{DynScalar, SymId};
let dest_arc = Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[0.0_f32; 6])));
let src_arc = Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[7.0_f32, 8.0])));
let graph = Arc::new(RwLock::new(Graph::new()));
let sym = SymId(0);
let (dest_id, src_id, ws_id) = {
let mut g = graph.write().unwrap();
let dest = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[6]),
dtype: DType::F32,
});
let src = g.push(Node {
op: Op::Const,
inputs: vec![],
shape: Shape::from_dims(&[2]),
dtype: DType::F32,
});
let ws = g.push(Node {
op: Op::WriteSlice {
ranges: vec![(0, 2)],
dyn_offset: Some((0, DynScalar::Sym(sym))),
},
inputs: vec![dest, src],
shape: Shape::from_dims(&[6]),
dtype: DType::F32,
});
(dest, src, ws)
};
let mut ctx = InferenceContext::new(Device::cpu());
ctx.insert(dest_id, Arc::clone(&dest_arc));
ctx.insert(src_id, Arc::clone(&src_arc));
let mut env = SymEnv::new();
env.bind(sym, 3).unwrap();
let out = ctx
.realize_one_as_with_env::<f32>(&graph, ws_id, &env)
.expect("realize_one_as_with_env");
assert_eq!(
out,
vec![0.0, 0.0, 0.0, 7.0, 8.0, 0.0],
"KV slab must land at the SymEnv-bound cached_len=3, not the placeholder 0",
);
let err = ctx.realize_one_as_with_env::<f32>(&graph, ws_id, &SymEnv::new());
assert!(
err.is_err(),
"unbound cached_len must surface a typed error"
);
}
#[test]
fn kv_cache_with_dims_constructs_fresh() {
let cache = KvCache::with_dims(4, 8, 64);
assert_eq!(cache.n_layers(), 4);
assert_eq!(cache.cached_len, 0);
assert_eq!(cache.n_kv_heads, 8);
assert_eq!(cache.head_dim, 64);
for li in 0..4 {
assert!(cache.layer(li).is_none());
}
}
#[test]
fn kv_cache_set_and_get_layer() {
let mut cache = KvCache::with_dims(2, 4, 8);
let k_arc = Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[0.0_f32; 32])));
let v_arc = Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[0.0_f32; 32])));
let layout = Layout::contiguous(Shape::from_dims(&[1, 4, 1, 8]));
cache.set_layer(
0,
KvLayer {
k: Arc::clone(&k_arc),
v: Arc::clone(&v_arc),
k_layout: layout.clone(),
v_layout: layout.clone(),
k_version: 0,
v_version: 0,
k_authority: AuthorityState::Host,
v_authority: AuthorityState::Host,
},
);
let layer = cache.layer(0).expect("just set");
assert!(Arc::ptr_eq(&layer.k, &k_arc));
assert!(Arc::ptr_eq(&layer.v, &v_arc));
assert_eq!(layer.k_version, 0);
assert!(matches!(layer.k_authority, AuthorityState::Host));
assert!(cache.layer(1).is_none());
}
#[test]
fn kv_cache_clear_drops_layers() {
let mut cache = KvCache::with_dims(2, 4, 8);
let k = Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[0.0_f32; 32])));
let v = Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[0.0_f32; 32])));
let layout = Layout::contiguous(Shape::from_dims(&[1, 4, 1, 8]));
cache.set_layer(
0,
KvLayer {
k: Arc::clone(&k),
v: Arc::clone(&v),
k_layout: layout.clone(),
v_layout: layout,
k_version: 1,
v_version: 1,
k_authority: AuthorityState::Host,
v_authority: AuthorityState::Host,
},
);
cache.cached_len = 7;
assert_eq!(Arc::strong_count(&k), 2);
cache.clear();
assert!(cache.layer(0).is_none());
assert_eq!(cache.cached_len, 0);
assert_eq!(Arc::strong_count(&k), 1);
}
#[test]
fn kv_cache_truncate_updates_cached_len() {
let mut cache = KvCache::with_dims(2, 4, 8);
cache.cached_len = 16;
cache.truncate_to(10);
assert_eq!(cache.cached_len, 10);
cache.truncate_to(15);
assert_eq!(cache.cached_len, 10);
cache.truncate_to(0);
assert_eq!(cache.cached_len, 0);
}
#[test]
fn kv_layer_default_authority_is_host() {
let arc = Arc::new(RwLock::new(fuel_memory::from_slice_cpu(&[0.0_f32; 4])));
let layout = Layout::contiguous(Shape::from_dims(&[4]));
let layer = KvLayer {
k: Arc::clone(&arc),
v: Arc::clone(&arc),
k_layout: layout.clone(),
v_layout: layout,
k_version: 0,
v_version: 0,
k_authority: AuthorityState::Host,
v_authority: AuthorityState::Host,
};
assert!(matches!(layer.k_authority, AuthorityState::Host));
assert!(matches!(layer.v_authority, AuthorityState::Host));
assert_eq!(layer.k_version, 0);
assert_eq!(layer.v_version, 0);
}
#[test]
#[allow(irrefutable_let_patterns)]
fn kv_cache_with_capacity_allocates_all_layers_on_cpu() {
let device = Device::cpu();
let cache = KvCache::with_capacity(
3,
4,
16,
32,
DType::F32,
&device,
)
.expect("with_capacity");
assert_eq!(cache.n_layers(), 3);
assert_eq!(cache.cached_len, 0);
assert_eq!(cache.n_kv_heads, 4);
assert_eq!(cache.head_dim, 16);
assert_eq!(cache.max_seq_len, Some(32));
assert_eq!(cache.dtype, Some(DType::F32));
for li in 0..3 {
let layer = cache.layer(li).expect("layer populated");
assert_eq!(layer.k_layout.shape().dims(), &[1, 4, 32, 16]);
assert_eq!(layer.v_layout.shape().dims(), &[1, 4, 32, 16]);
let k_guard = layer.k.read().unwrap();
assert_eq!(k_guard.inner.len_bytes(), 8192);
assert_eq!(k_guard.dtype, DType::F32);
if let BackendStorage::Cpu(c) = &k_guard.inner {
let typed: &[f32] = c.as_slice().unwrap();
assert!(typed.iter().all(|&x| x == 0.0));
} else {
panic!("expected CPU storage");
}
}
}
#[test]
fn kv_cache_slot_storage_returns_layer_arc() {
let device = Device::cpu();
let cache =
KvCache::with_capacity(2, 4, 8, 16, DType::F32, &device).expect("with_capacity");
let k_via_layer = Arc::clone(&cache.layer(0).unwrap().k);
let k_via_slot = cache.slot_storage(0, KvSlot::K).expect("layer 0 K");
assert!(
Arc::ptr_eq(&k_via_layer, &k_via_slot),
"slot_storage should return the same Arc as layer().k",
);
let v_via_layer = Arc::clone(&cache.layer(1).unwrap().v);
let v_via_slot = cache.slot_storage(1, KvSlot::V).expect("layer 1 V");
assert!(Arc::ptr_eq(&v_via_layer, &v_via_slot));
}
#[test]
fn kv_cache_slot_storage_returns_none_for_unpopulated() {
let cache = KvCache::with_dims(2, 4, 8);
assert!(cache.slot_storage(0, KvSlot::K).is_none());
assert!(cache.slot_storage(1, KvSlot::V).is_none());
assert!(cache.slot_storage(5, KvSlot::K).is_none());
}
#[test]
fn kv_cache_bump_version_advances_per_slot() {
let device = Device::cpu();
let mut cache =
KvCache::with_capacity(1, 4, 8, 16, DType::F32, &device).expect("with_capacity");
assert_eq!(cache.layer(0).unwrap().k_version, 0);
assert_eq!(cache.layer(0).unwrap().v_version, 0);
cache.bump_version(0, KvSlot::K);
cache.bump_version(0, KvSlot::K);
cache.bump_version(0, KvSlot::V);
assert_eq!(cache.layer(0).unwrap().k_version, 2);
assert_eq!(cache.layer(0).unwrap().v_version, 1);
}
#[test]
fn kv_cache_bump_version_unpopulated_is_noop() {
let mut cache = KvCache::with_dims(2, 4, 8);
cache.bump_version(0, KvSlot::K);
cache.bump_version(99, KvSlot::V); assert!(cache.layer(0).is_none());
}
#[test]
fn kv_cache_with_capacity_bf16_byte_count() {
let device = Device::cpu();
let cache =
KvCache::with_capacity(1, 2, 4, 8, DType::BF16, &device).expect("with_capacity bf16");
let layer = cache.layer(0).unwrap();
assert_eq!(layer.k.read().unwrap().inner.len_bytes(), 128);
assert_eq!(cache.dtype, Some(DType::BF16));
}
#[test]
#[allow(irrefutable_let_patterns)]
fn latent_kv_cache_with_capacity_allocates_all_layers_on_cpu() {
let device = Device::cpu();
let cache = LatentKvCache::with_capacity(
3,
8,
vec![vec![5], vec![2]],
DType::F32,
&device,
)
.expect("with_capacity");
assert_eq!(cache.n_layers(), 3);
assert_eq!(cache.n_slots(), 2);
assert_eq!(cache.cached_len, 0);
assert_eq!(cache.max_seq_len, 8);
assert_eq!(cache.dtype, DType::F32);
assert_eq!(cache.slot_trailing(0), &[5]);
assert_eq!(cache.slot_trailing(1), &[2]);
for li in 0..3 {
let latent = cache.slot_storage(li, 0).expect("latent slot populated");
let kpe = cache.slot_storage(li, 1).expect("k_pe slot populated");
assert_eq!(latent.read().unwrap().inner.len_bytes(), 160);
assert_eq!(kpe.read().unwrap().inner.len_bytes(), 64);
let guard = latent.read().unwrap();
if let BackendStorage::Cpu(c) = &guard.inner {
let typed: &[f32] = c.as_slice().unwrap();
assert!(typed.iter().all(|&x| x == 0.0));
} else {
panic!("expected CPU storage");
}
}
}
#[test]
fn latent_kv_cache_slot_storage_returns_none_for_oob() {
let device = Device::cpu();
let cache = LatentKvCache::with_capacity(2, 4, vec![vec![3]], DType::F32, &device)
.expect("with_capacity");
assert!(cache.slot_storage(5, 0).is_none()); assert!(cache.slot_storage(0, 5).is_none()); }
#[test]
fn latent_kv_cache_bump_version_advances_per_slot_and_oob_is_noop() {
let device = Device::cpu();
let mut cache =
LatentKvCache::with_capacity(1, 4, vec![vec![2], vec![1]], DType::F32, &device)
.expect("with_capacity");
cache.bump_version(0, 0);
cache.bump_version(0, 0);
cache.bump_version(0, 1);
cache.bump_version(99, 0); cache.bump_version(0, 99);
let layer0 = cache.layers[0].as_ref().expect("layer 0 populated");
assert_eq!(layer0[0].version, 2);
assert_eq!(layer0[1].version, 1);
}
#[test]
fn latent_kv_cache_with_capacity_rejects_bad_geometry() {
let device = Device::cpu();
assert!(LatentKvCache::with_capacity(0, 4, vec![vec![2]], DType::F32, &device).is_err());
assert!(LatentKvCache::with_capacity(1, 0, vec![vec![2]], DType::F32, &device).is_err());
assert!(LatentKvCache::with_capacity(1, 4, vec![], DType::F32, &device).is_err());
}
}