use thiserror::Error;
use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
use crate::region::wrap_anonymous;
const OP_ID: &str = "vyre-libs::nn::kv_cache_append";
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum KvCacheAppendError {
#[error(
"KV cache append requires nonzero batch, heads, capacity, chunk length, and head dimension"
)]
EmptyShape,
#[error(
"KV cache append range offset={offset}, chunk_len={chunk_len} exceeds capacity={capacity}"
)]
Range {
offset: u32,
chunk_len: u32,
capacity: u32,
},
#[error("KV cache append element count overflows u32; shard the cache")]
ElementCountOverflow,
#[error("KV cache append requires F16, BF16, or F32 elements; got {dtype:?}")]
UnsupportedDtype {
dtype: DataType,
},
}
#[allow(clippy::too_many_arguments)]
pub fn kv_cache_append(
prior: &str,
chunk: &str,
next: &str,
batch: u32,
heads: u32,
capacity: u32,
chunk_len: u32,
head_dim: u32,
offset: u32,
) -> Result<Program, KvCacheAppendError> {
kv_cache_append_typed(
prior,
chunk,
next,
batch,
heads,
capacity,
chunk_len,
head_dim,
offset,
DataType::F32,
)
}
#[allow(clippy::too_many_arguments)]
pub fn kv_cache_append_typed(
prior: &str,
chunk: &str,
next: &str,
batch: u32,
heads: u32,
capacity: u32,
chunk_len: u32,
head_dim: u32,
offset: u32,
dtype: DataType,
) -> Result<Program, KvCacheAppendError> {
if batch == 0 || heads == 0 || capacity == 0 || chunk_len == 0 || head_dim == 0 {
return Err(KvCacheAppendError::EmptyShape);
}
if !matches!(dtype, DataType::F16 | DataType::BF16 | DataType::F32) {
return Err(KvCacheAppendError::UnsupportedDtype { dtype });
}
let end = offset
.checked_add(chunk_len)
.filter(|end| *end <= capacity)
.ok_or(KvCacheAppendError::Range {
offset,
chunk_len,
capacity,
})?;
let checked = |values: &[u32]| {
values.iter().try_fold(1_u32, |product, value| {
product
.checked_mul(*value)
.ok_or(KvCacheAppendError::ElementCountOverflow)
})
};
let cache_count = checked(&[batch, heads, capacity, head_dim])?;
let chunk_count = checked(&[batch, heads, chunk_len, head_dim])?;
let cache_head_span = capacity
.checked_mul(head_dim)
.ok_or(KvCacheAppendError::ElementCountOverflow)?;
let chunk_head_span = chunk_len
.checked_mul(head_dim)
.ok_or(KvCacheAppendError::ElementCountOverflow)?;
let index = Expr::var("index");
let dimension = Expr::rem(index.clone(), Expr::u32(head_dim));
let head_row = Expr::div(index.clone(), Expr::u32(cache_head_span));
let token = Expr::rem(
Expr::div(index.clone(), Expr::u32(head_dim)),
Expr::u32(capacity),
);
let chunk_index = Expr::add(
Expr::mul(head_row, Expr::u32(chunk_head_span)),
Expr::add(
Expr::mul(
Expr::sub(token.clone(), Expr::u32(offset)),
Expr::u32(head_dim),
),
dimension,
),
);
let in_chunk = Expr::and(
Expr::ge(token.clone(), Expr::u32(offset)),
Expr::lt(token, Expr::u32(end)),
);
let body = vec![
Node::let_bind("index", Expr::InvocationId { axis: 0 }),
Node::if_then(
Expr::lt(index.clone(), Expr::u32(cache_count)),
vec![Node::if_then_else(
in_chunk,
vec![Node::Store {
buffer: next.into(),
index: index.clone(),
value: Expr::load(chunk, chunk_index),
}],
vec![Node::Store {
buffer: next.into(),
index: index.clone(),
value: Expr::load(prior, index),
}],
)],
),
];
Ok(Program::wrapped(
vec![
BufferDecl::storage(prior, 0, BufferAccess::ReadWrite, dtype.clone())
.with_count(cache_count),
BufferDecl::storage(chunk, 1, BufferAccess::ReadOnly, dtype.clone())
.with_count(chunk_count),
BufferDecl::output(next, 2, dtype).with_count(cache_count),
],
[64, 1, 1],
vec![wrap_anonymous(OP_ID, body)],
))
}