use crate::{NeuralError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodebookConfig {
pub num_centroids: usize,
pub centroid_dim: usize,
pub index_bits: u8,
pub product_quantization: bool,
pub pq_subspaces: usize,
}
impl CodebookConfig {
pub fn attention_qk() -> Self {
Self {
num_centroids: 4096,
centroid_dim: 64,
index_bits: 12,
product_quantization: false,
pq_subspaces: 1,
}
}
pub fn attention_vo() -> Self {
Self {
num_centroids: 8192,
centroid_dim: 64,
index_bits: 13,
product_quantization: false,
pq_subspaces: 1,
}
}
pub fn ffn() -> Self {
Self {
num_centroids: 2048,
centroid_dim: 128,
index_bits: 11,
product_quantization: true,
pq_subspaces: 2,
}
}
pub fn normalization() -> Self {
Self {
num_centroids: 256,
centroid_dim: 32,
index_bits: 8,
product_quantization: false,
pq_subspaces: 1,
}
}
pub fn memory_size(&self) -> usize {
self.num_centroids * self.centroid_dim * 4 }
pub fn bits_per_weight(&self) -> f32 {
if self.product_quantization {
self.index_bits as f32 * self.pq_subspaces as f32 / self.centroid_dim as f32
} else {
self.index_bits as f32 / self.centroid_dim as f32
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Codebook {
pub config: CodebookConfig,
pub centroids: Vec<f32>,
pub id: String,
pub stats: CodebookStats,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CodebookStats {
pub mse: f32,
pub iterations: usize,
pub usage_distribution: (f32, f32, f32),
pub samples_seen: usize,
}
impl Codebook {
pub fn new(config: CodebookConfig, id: impl Into<String>) -> Self {
use rand::Rng;
let mut rng = rand::thread_rng();
let centroids: Vec<f32> = (0..config.num_centroids * config.centroid_dim)
.map(|_| rng.gen_range(-0.1..0.1))
.collect();
Self {
config,
centroids,
id: id.into(),
stats: CodebookStats::default(),
}
}
pub fn from_centroids(
config: CodebookConfig,
centroids: Vec<f32>,
id: impl Into<String>,
) -> Result<Self> {
let expected = config.num_centroids * config.centroid_dim;
if centroids.len() != expected {
return Err(NeuralError::DimensionMismatch {
expected,
actual: centroids.len(),
});
}
Ok(Self {
config,
centroids,
id: id.into(),
stats: CodebookStats::default(),
})
}
pub fn find_nearest(&self, vector: &[f32]) -> Result<(usize, f32)> {
if vector.len() != self.config.centroid_dim {
return Err(NeuralError::DimensionMismatch {
expected: self.config.centroid_dim,
actual: vector.len(),
});
}
let mut best_idx = 0;
let mut best_dist = f32::INFINITY;
for i in 0..self.config.num_centroids {
let offset = i * self.config.centroid_dim;
let dist =
self.squared_distance(vector, &self.centroids[offset..offset + vector.len()]);
if dist < best_dist {
best_dist = dist;
best_idx = i;
}
}
Ok((best_idx, best_dist.sqrt()))
}
pub fn find_k_nearest(&self, vector: &[f32], k: usize) -> Result<Vec<(usize, f32)>> {
if vector.len() != self.config.centroid_dim {
return Err(NeuralError::DimensionMismatch {
expected: self.config.centroid_dim,
actual: vector.len(),
});
}
let mut distances: Vec<(usize, f32)> = (0..self.config.num_centroids)
.map(|i| {
let offset = i * self.config.centroid_dim;
let dist =
self.squared_distance(vector, &self.centroids[offset..offset + vector.len()]);
(i, dist.sqrt())
})
.collect();
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
distances.truncate(k);
Ok(distances)
}
pub fn get_centroid(&self, index: usize) -> Option<&[f32]> {
if index >= self.config.num_centroids {
return None;
}
let offset = index * self.config.centroid_dim;
Some(&self.centroids[offset..offset + self.config.centroid_dim])
}
fn squared_distance(&self, a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum()
}
pub fn encode_batch(&self, vectors: &[f32], vector_dim: usize) -> Result<Vec<u16>> {
if vector_dim != self.config.centroid_dim {
return Err(NeuralError::DimensionMismatch {
expected: self.config.centroid_dim,
actual: vector_dim,
});
}
let num_vectors = vectors.len() / vector_dim;
let mut indices = Vec::with_capacity(num_vectors);
for i in 0..num_vectors {
let offset = i * vector_dim;
let vector = &vectors[offset..offset + vector_dim];
let (idx, _) = self.find_nearest(vector)?;
indices.push(idx as u16);
}
Ok(indices)
}
pub fn decode_batch(&self, indices: &[u16]) -> Vec<f32> {
let dim = self.config.centroid_dim;
let mut vectors = Vec::with_capacity(indices.len() * dim);
for &idx in indices {
if let Some(centroid) = self.get_centroid(idx as usize) {
vectors.extend_from_slice(centroid);
} else {
vectors.extend(std::iter::repeat_n(0.0, dim));
}
}
vectors
}
pub fn to_bytes(&self) -> Vec<u8> {
bincode::serialize(self).unwrap_or_default()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
bincode::deserialize(bytes).map_err(|e| NeuralError::InvalidFormat(e.to_string()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LayerType {
AttentionQK,
AttentionVO,
FFN,
Normalization,
Embedding,
OutputHead,
}
impl LayerType {
pub fn default_config(&self) -> CodebookConfig {
match self {
LayerType::AttentionQK => CodebookConfig::attention_qk(),
LayerType::AttentionVO => CodebookConfig::attention_vo(),
LayerType::FFN => CodebookConfig::ffn(),
LayerType::Normalization => CodebookConfig::normalization(),
LayerType::Embedding => CodebookConfig::ffn(), LayerType::OutputHead => CodebookConfig::attention_vo(), }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayerCodebook {
codebooks: HashMap<LayerType, Codebook>,
pub model_id: String,
pub total_memory: usize,
}
impl LayerCodebook {
pub fn new(model_id: impl Into<String>) -> Self {
Self {
codebooks: HashMap::new(),
model_id: model_id.into(),
total_memory: 0,
}
}
pub fn add(&mut self, layer_type: LayerType, codebook: Codebook) {
self.total_memory += codebook.config.memory_size();
self.codebooks.insert(layer_type, codebook);
}
pub fn get(&self, layer_type: LayerType) -> Option<&Codebook> {
self.codebooks.get(&layer_type)
}
pub fn with_defaults(model_id: impl Into<String>) -> Self {
let model_id = model_id.into();
let mut lc = Self::new(&model_id);
for layer_type in &[
LayerType::AttentionQK,
LayerType::AttentionVO,
LayerType::FFN,
LayerType::Normalization,
] {
let config = layer_type.default_config();
let id = format!("{}_{:?}", model_id, layer_type);
lc.add(*layer_type, Codebook::new(config, id));
}
lc
}
pub fn to_bytes(&self) -> Vec<u8> {
bincode::serialize(self).unwrap_or_default()
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
bincode::deserialize(bytes).map_err(|e| NeuralError::InvalidFormat(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_codebook_creation() {
let config = CodebookConfig::attention_qk();
let codebook = Codebook::new(config.clone(), "test");
assert_eq!(codebook.config.num_centroids, 4096);
assert_eq!(codebook.centroids.len(), 4096 * 64);
}
#[test]
fn test_find_nearest() {
let config = CodebookConfig {
num_centroids: 4,
centroid_dim: 3,
index_bits: 2,
product_quantization: false,
pq_subspaces: 1,
};
let centroids = vec![
1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, ];
let codebook = Codebook::from_centroids(config, centroids, "test").unwrap();
let (idx, _) = codebook.find_nearest(&[0.1, 0.9, 0.1]).unwrap();
assert_eq!(idx, 1);
let (idx, _) = codebook.find_nearest(&[0.9, 0.9, 0.9]).unwrap();
assert_eq!(idx, 3);
}
#[test]
fn test_encode_decode() {
let config = CodebookConfig {
num_centroids: 256,
centroid_dim: 4,
index_bits: 8,
product_quantization: false,
pq_subspaces: 1,
};
let codebook = Codebook::new(config, "test");
let vectors: Vec<f32> = (0..40).map(|i| i as f32 * 0.01).collect();
let indices = codebook.encode_batch(&vectors, 4).unwrap();
assert_eq!(indices.len(), 10);
let decoded = codebook.decode_batch(&indices);
assert_eq!(decoded.len(), 40);
}
#[test]
fn test_layer_codebook() {
let lc = LayerCodebook::with_defaults("sdxl");
assert!(lc.get(LayerType::AttentionQK).is_some());
assert!(lc.get(LayerType::FFN).is_some());
assert!(lc.total_memory > 0);
}
#[test]
fn test_memory_size() {
let config = CodebookConfig::attention_qk();
assert_eq!(config.memory_size(), 4096 * 64 * 4); }
}