use libm::sqrtf;
#[derive(Clone, Copy)]
pub struct PerDimKey<const D: usize> {
pub codes: [u8; D],
pub norm: f32,
}
impl<const D: usize> Default for PerDimKey<D> {
fn default() -> Self {
Self {
codes: [0; D],
norm: 1.0,
}
}
}
#[derive(Clone)]
pub struct CompressedAttentionCache<const D: usize, const N: usize, const K: usize> {
keys: [PerDimKey<D>; N],
values: [[f32; D]; N],
dim_mins: [f32; D],
dim_ranges: [f32; D],
levels: u32,
len: usize,
}
impl<const D: usize, const N: usize, const K: usize> CompressedAttentionCache<D, N, K> {
pub fn new() -> Self {
Self {
keys: [PerDimKey::default(); N],
values: [[0.0; D]; N],
dim_mins: [-1.0; D],
dim_ranges: [2.0; D],
levels: 255, len: 0,
}
}
pub fn fit(&mut self, vectors: &[[f32; D]]) {
let mut mins = [f32::INFINITY; D];
let mut maxs = [f32::NEG_INFINITY; D];
for v in vectors {
let mut norm_sq = 0.0f32;
let mut i = 0;
while i < D {
norm_sq += v[i] * v[i];
i += 1;
}
let norm = sqrtf(norm_sq).max(1e-6);
let mut i = 0;
while i < D {
let unit = v[i] / norm;
if unit < mins[i] {
mins[i] = unit;
}
if unit > maxs[i] {
maxs[i] = unit;
}
i += 1;
}
}
let mut i = 0;
while i < D {
self.dim_mins[i] = mins[i];
let range = maxs[i] - mins[i];
self.dim_ranges[i] = range.max(1e-6);
i += 1;
}
}
pub fn push(&mut self, key: &[f32; D], value: &[f32; D]) -> bool {
if self.len >= N {
return false;
}
let mut norm_sq = 0.0f32;
let mut i = 0;
while i < D {
norm_sq += key[i] * key[i];
i += 1;
}
let norm = sqrtf(norm_sq).max(1e-6);
let _inv_levels = 1.0f32 / self.levels as f32;
let mut i = 0;
while i < D {
let unit = key[i] / norm;
let normalized = (unit - self.dim_mins[i]) / self.dim_ranges[i];
let code_f = libm::roundf(normalized * self.levels as f32);
let code = if code_f < 0.0 {
0u8
} else if code_f > self.levels as f32 {
self.levels as u8
} else {
code_f as u8
};
self.keys[self.len].codes[i] = code;
i += 1;
}
self.keys[self.len].norm = norm;
self.values[self.len] = *value;
self.len += 1;
true
}
pub fn attention_scores(&self, query: &[f32; D], scores: &mut [f32; N]) {
let inv_levels = 1.0f32 / self.levels as f32;
let mut scaled_query = [0.0f32; D];
let mut bias = 0.0f32;
let mut i = 0;
while i < D {
scaled_query[i] = self.dim_ranges[i] * inv_levels * query[i];
bias += self.dim_mins[i] * query[i];
i += 1;
}
let mut t = 0;
while t < self.len {
let mut dot = 0.0f32;
let mut i = 0;
while i < D {
dot += self.keys[t].codes[i] as f32 * scaled_query[i];
i += 1;
}
scores[t] = (dot + bias) * self.keys[t].norm;
t += 1;
}
}
pub fn attention_topk(&self, query: &[f32; D], out: &mut [f32; D]) -> usize {
if self.len == 0 {
let mut i = 0;
while i < D {
out[i] = 0.0;
i += 1;
}
return 0;
}
let mut scores = [0.0f32; N];
self.attention_scores(query, &mut scores);
let k = K.min(self.len);
let mut selected_indices: [usize; K] = [0; K];
let mut selected_scores: [f32; K] = [f32::NEG_INFINITY; K];
let mut ki = 0;
while ki < k {
let mut best_idx = 0;
let mut best_score = f32::NEG_INFINITY;
let mut t = 0;
while t < self.len {
let mut already = false;
let mut sj = 0;
while sj < ki {
if selected_indices[sj] == t {
already = true;
break;
}
sj += 1;
}
if !already && scores[t] > best_score {
best_score = scores[t];
best_idx = t;
}
t += 1;
}
selected_indices[ki] = best_idx;
selected_scores[ki] = best_score;
ki += 1;
}
let mut max_score = f32::NEG_INFINITY;
let mut i = 0;
while i < k {
if selected_scores[i] > max_score {
max_score = selected_scores[i];
}
i += 1;
}
let mut sum_exp = 0.0f32;
let mut weights = [0.0f32; K];
let mut i = 0;
while i < k {
let exp = libm::expf(selected_scores[i] - max_score);
weights[i] = exp;
sum_exp += exp;
i += 1;
}
let inv_sum = if sum_exp > 0.0 { 1.0 / sum_exp } else { 0.0 };
let mut i = 0;
while i < k {
weights[i] *= inv_sum;
i += 1;
}
let mut j = 0;
while j < D {
out[j] = 0.0;
let mut i = 0;
while i < k {
out[j] += weights[i] * self.values[selected_indices[i]][j];
i += 1;
}
j += 1;
}
k
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn capacity(&self) -> usize {
N
}
pub fn is_full(&self) -> bool {
self.len >= N
}
pub fn bytes_per_key() -> usize {
D + 4 }
pub fn total_key_bytes(&self) -> usize {
self.len * Self::bytes_per_key()
}
}
impl<const D: usize, const N: usize, const K: usize> Default for CompressedAttentionCache<D, N, K> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_push_and_len() {
let mut cache = CompressedAttentionCache::<4, 8, 2>::new();
assert_eq!(cache.len(), 0);
assert!(cache.push(&[1.0, 0.0, 0.0, 0.0], &[1.0, 2.0, 3.0, 4.0]));
assert_eq!(cache.len(), 1);
assert!(!cache.is_full());
}
#[test]
fn attention_scores_match_direction() {
let mut cache = CompressedAttentionCache::<4, 8, 2>::new();
cache.push(&[1.0, 0.0, 0.0, 0.0], &[1.0, 0.0, 0.0, 0.0]);
cache.push(&[0.0, 1.0, 0.0, 0.0], &[0.0, 1.0, 0.0, 0.0]);
cache.push(&[0.0, 0.0, 1.0, 0.0], &[0.0, 0.0, 1.0, 0.0]);
let mut scores = [0.0f32; 8];
cache.attention_scores(&[1.0, 0.0, 0.0, 0.0], &mut scores);
assert!(scores[0] > scores[1]);
assert!(scores[0] > scores[2]);
}
#[test]
fn attention_topk_decodes_only_k_values() {
let mut cache = CompressedAttentionCache::<4, 8, 2>::new();
cache.push(&[1.0, 0.0, 0.0, 0.0], &[1.0, 0.0, 0.0, 0.0]);
cache.push(&[0.0, 1.0, 0.0, 0.0], &[0.0, 1.0, 0.0, 0.0]);
cache.push(&[0.0, 0.0, 1.0, 0.0], &[0.0, 0.0, 1.0, 0.0]);
let mut out = [0.0f32; 4];
let decoded = cache.attention_topk(&[1.0, 0.0, 0.0, 0.0], &mut out);
assert_eq!(decoded, 2);
assert!(out[0] > out[1]);
assert!(out[0] > out[2]);
}
#[test]
fn fit_improves_accuracy() {
let mut cache = CompressedAttentionCache::<4, 8, 2>::new();
cache.fit(&[
[1.0, 0.5, -0.5, 0.3],
[-0.5, 1.0, 0.5, -0.3],
[0.3, -0.3, 1.0, 0.5],
]);
cache.push(&[1.0, 0.5, -0.5, 0.3], &[1.0, 2.0, 3.0, 4.0]);
cache.push(&[-0.5, 1.0, 0.5, -0.3], &[4.0, 3.0, 2.0, 1.0]);
let mut scores = [0.0f32; 8];
cache.attention_scores(&[1.0, 0.5, -0.5, 0.3], &mut scores);
assert!(scores[0] > scores[1]);
}
#[test]
fn memory_savings() {
let bytes_per_key = CompressedAttentionCache::<32, 64, 8>::bytes_per_key();
assert_eq!(bytes_per_key, 36);
let dense_bytes = 32 * 4;
let compression_ratio = dense_bytes as f32 / bytes_per_key as f32;
assert!(compression_ratio > 3.5);
}
}