Skip to main content

haagenti_neural/
codebook.rs

1//! Codebook definitions for neural compression
2
3use crate::{NeuralError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Configuration for a codebook
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct CodebookConfig {
10    /// Number of centroids (codewords)
11    pub num_centroids: usize,
12    /// Dimension of each centroid
13    pub centroid_dim: usize,
14    /// Bits per index (log2 of num_centroids)
15    pub index_bits: u8,
16    /// Whether to use product quantization
17    pub product_quantization: bool,
18    /// Number of subspaces for PQ
19    pub pq_subspaces: usize,
20}
21
22impl CodebookConfig {
23    /// Create config for attention Q/K weights
24    pub fn attention_qk() -> Self {
25        Self {
26            num_centroids: 4096,
27            centroid_dim: 64,
28            index_bits: 12,
29            product_quantization: false,
30            pq_subspaces: 1,
31        }
32    }
33
34    /// Create config for attention V/O weights
35    pub fn attention_vo() -> Self {
36        Self {
37            num_centroids: 8192,
38            centroid_dim: 64,
39            index_bits: 13,
40            product_quantization: false,
41            pq_subspaces: 1,
42        }
43    }
44
45    /// Create config for FFN weights
46    pub fn ffn() -> Self {
47        Self {
48            num_centroids: 2048,
49            centroid_dim: 128,
50            index_bits: 11,
51            product_quantization: true,
52            pq_subspaces: 2,
53        }
54    }
55
56    /// Create config for normalization weights
57    pub fn normalization() -> Self {
58        Self {
59            num_centroids: 256,
60            centroid_dim: 32,
61            index_bits: 8,
62            product_quantization: false,
63            pq_subspaces: 1,
64        }
65    }
66
67    /// Compute memory size for codebook in bytes
68    pub fn memory_size(&self) -> usize {
69        self.num_centroids * self.centroid_dim * 4 // FP32 centroids
70    }
71
72    /// Compute bits per weight
73    pub fn bits_per_weight(&self) -> f32 {
74        if self.product_quantization {
75            self.index_bits as f32 * self.pq_subspaces as f32 / self.centroid_dim as f32
76        } else {
77            self.index_bits as f32 / self.centroid_dim as f32
78        }
79    }
80}
81
82/// A codebook containing centroids for vector quantization
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Codebook {
85    /// Configuration
86    pub config: CodebookConfig,
87    /// Centroid vectors [num_centroids × centroid_dim]
88    pub centroids: Vec<f32>,
89    /// Codebook identifier
90    pub id: String,
91    /// Training statistics
92    pub stats: CodebookStats,
93}
94
95/// Statistics from codebook training
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
97pub struct CodebookStats {
98    /// Mean squared error after training
99    pub mse: f32,
100    /// Number of training iterations
101    pub iterations: usize,
102    /// Centroid usage distribution (min, max, mean)
103    pub usage_distribution: (f32, f32, f32),
104    /// Training samples seen
105    pub samples_seen: usize,
106}
107
108impl Codebook {
109    /// Create a new codebook with random centroids
110    pub fn new(config: CodebookConfig, id: impl Into<String>) -> Self {
111        use rand::Rng;
112        let mut rng = rand::thread_rng();
113
114        let centroids: Vec<f32> = (0..config.num_centroids * config.centroid_dim)
115            .map(|_| rng.gen_range(-0.1..0.1))
116            .collect();
117
118        Self {
119            config,
120            centroids,
121            id: id.into(),
122            stats: CodebookStats::default(),
123        }
124    }
125
126    /// Create from existing centroids
127    pub fn from_centroids(
128        config: CodebookConfig,
129        centroids: Vec<f32>,
130        id: impl Into<String>,
131    ) -> Result<Self> {
132        let expected = config.num_centroids * config.centroid_dim;
133        if centroids.len() != expected {
134            return Err(NeuralError::DimensionMismatch {
135                expected,
136                actual: centroids.len(),
137            });
138        }
139
140        Ok(Self {
141            config,
142            centroids,
143            id: id.into(),
144            stats: CodebookStats::default(),
145        })
146    }
147
148    /// Find nearest centroid for a vector
149    pub fn find_nearest(&self, vector: &[f32]) -> Result<(usize, f32)> {
150        if vector.len() != self.config.centroid_dim {
151            return Err(NeuralError::DimensionMismatch {
152                expected: self.config.centroid_dim,
153                actual: vector.len(),
154            });
155        }
156
157        let mut best_idx = 0;
158        let mut best_dist = f32::INFINITY;
159
160        for i in 0..self.config.num_centroids {
161            let offset = i * self.config.centroid_dim;
162            let dist =
163                self.squared_distance(vector, &self.centroids[offset..offset + vector.len()]);
164
165            if dist < best_dist {
166                best_dist = dist;
167                best_idx = i;
168            }
169        }
170
171        Ok((best_idx, best_dist.sqrt()))
172    }
173
174    /// Find K nearest centroids
175    pub fn find_k_nearest(&self, vector: &[f32], k: usize) -> Result<Vec<(usize, f32)>> {
176        if vector.len() != self.config.centroid_dim {
177            return Err(NeuralError::DimensionMismatch {
178                expected: self.config.centroid_dim,
179                actual: vector.len(),
180            });
181        }
182
183        let mut distances: Vec<(usize, f32)> = (0..self.config.num_centroids)
184            .map(|i| {
185                let offset = i * self.config.centroid_dim;
186                let dist =
187                    self.squared_distance(vector, &self.centroids[offset..offset + vector.len()]);
188                (i, dist.sqrt())
189            })
190            .collect();
191
192        distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
193        distances.truncate(k);
194
195        Ok(distances)
196    }
197
198    /// Get centroid by index
199    pub fn get_centroid(&self, index: usize) -> Option<&[f32]> {
200        if index >= self.config.num_centroids {
201            return None;
202        }
203        let offset = index * self.config.centroid_dim;
204        Some(&self.centroids[offset..offset + self.config.centroid_dim])
205    }
206
207    /// Squared L2 distance between two vectors
208    fn squared_distance(&self, a: &[f32], b: &[f32]) -> f32 {
209        a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum()
210    }
211
212    /// Encode a batch of vectors to indices
213    pub fn encode_batch(&self, vectors: &[f32], vector_dim: usize) -> Result<Vec<u16>> {
214        if vector_dim != self.config.centroid_dim {
215            return Err(NeuralError::DimensionMismatch {
216                expected: self.config.centroid_dim,
217                actual: vector_dim,
218            });
219        }
220
221        let num_vectors = vectors.len() / vector_dim;
222        let mut indices = Vec::with_capacity(num_vectors);
223
224        for i in 0..num_vectors {
225            let offset = i * vector_dim;
226            let vector = &vectors[offset..offset + vector_dim];
227            let (idx, _) = self.find_nearest(vector)?;
228            indices.push(idx as u16);
229        }
230
231        Ok(indices)
232    }
233
234    /// Decode indices to vectors
235    pub fn decode_batch(&self, indices: &[u16]) -> Vec<f32> {
236        let dim = self.config.centroid_dim;
237        let mut vectors = Vec::with_capacity(indices.len() * dim);
238
239        for &idx in indices {
240            if let Some(centroid) = self.get_centroid(idx as usize) {
241                vectors.extend_from_slice(centroid);
242            } else {
243                // Fallback to zeros
244                vectors.extend(std::iter::repeat_n(0.0, dim));
245            }
246        }
247
248        vectors
249    }
250
251    /// Serialize to bytes
252    pub fn to_bytes(&self) -> Vec<u8> {
253        bincode::serialize(self).unwrap_or_default()
254    }
255
256    /// Deserialize from bytes
257    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
258        bincode::deserialize(bytes).map_err(|e| NeuralError::InvalidFormat(e.to_string()))
259    }
260}
261
262/// Layer type for codebook selection
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
264pub enum LayerType {
265    /// Attention Q/K projection
266    AttentionQK,
267    /// Attention V/O projection
268    AttentionVO,
269    /// Feed-forward network
270    FFN,
271    /// Layer normalization
272    Normalization,
273    /// Embedding layer
274    Embedding,
275    /// Output head
276    OutputHead,
277}
278
279impl LayerType {
280    /// Get default codebook config for this layer type
281    pub fn default_config(&self) -> CodebookConfig {
282        match self {
283            LayerType::AttentionQK => CodebookConfig::attention_qk(),
284            LayerType::AttentionVO => CodebookConfig::attention_vo(),
285            LayerType::FFN => CodebookConfig::ffn(),
286            LayerType::Normalization => CodebookConfig::normalization(),
287            LayerType::Embedding => CodebookConfig::ffn(), // Similar to FFN
288            LayerType::OutputHead => CodebookConfig::attention_vo(), // High precision
289        }
290    }
291}
292
293/// Collection of codebooks for different layer types
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct LayerCodebook {
296    /// Codebooks by layer type
297    codebooks: HashMap<LayerType, Codebook>,
298    /// Model identifier
299    pub model_id: String,
300    /// Total memory used by all codebooks
301    pub total_memory: usize,
302}
303
304impl LayerCodebook {
305    /// Create a new layer codebook collection
306    pub fn new(model_id: impl Into<String>) -> Self {
307        Self {
308            codebooks: HashMap::new(),
309            model_id: model_id.into(),
310            total_memory: 0,
311        }
312    }
313
314    /// Add a codebook for a layer type
315    pub fn add(&mut self, layer_type: LayerType, codebook: Codebook) {
316        self.total_memory += codebook.config.memory_size();
317        self.codebooks.insert(layer_type, codebook);
318    }
319
320    /// Get codebook for a layer type
321    pub fn get(&self, layer_type: LayerType) -> Option<&Codebook> {
322        self.codebooks.get(&layer_type)
323    }
324
325    /// Initialize with default codebooks for all layer types
326    pub fn with_defaults(model_id: impl Into<String>) -> Self {
327        let model_id = model_id.into();
328        let mut lc = Self::new(&model_id);
329
330        for layer_type in &[
331            LayerType::AttentionQK,
332            LayerType::AttentionVO,
333            LayerType::FFN,
334            LayerType::Normalization,
335        ] {
336            let config = layer_type.default_config();
337            let id = format!("{}_{:?}", model_id, layer_type);
338            lc.add(*layer_type, Codebook::new(config, id));
339        }
340
341        lc
342    }
343
344    /// Serialize to bytes
345    pub fn to_bytes(&self) -> Vec<u8> {
346        bincode::serialize(self).unwrap_or_default()
347    }
348
349    /// Deserialize from bytes
350    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
351        bincode::deserialize(bytes).map_err(|e| NeuralError::InvalidFormat(e.to_string()))
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_codebook_creation() {
361        let config = CodebookConfig::attention_qk();
362        let codebook = Codebook::new(config.clone(), "test");
363
364        assert_eq!(codebook.config.num_centroids, 4096);
365        assert_eq!(codebook.centroids.len(), 4096 * 64);
366    }
367
368    #[test]
369    fn test_find_nearest() {
370        let config = CodebookConfig {
371            num_centroids: 4,
372            centroid_dim: 3,
373            index_bits: 2,
374            product_quantization: false,
375            pq_subspaces: 1,
376        };
377
378        let centroids = vec![
379            1.0, 0.0, 0.0, // Centroid 0
380            0.0, 1.0, 0.0, // Centroid 1
381            0.0, 0.0, 1.0, // Centroid 2
382            1.0, 1.0, 1.0, // Centroid 3
383        ];
384
385        let codebook = Codebook::from_centroids(config, centroids, "test").unwrap();
386
387        // Vector close to centroid 1
388        let (idx, _) = codebook.find_nearest(&[0.1, 0.9, 0.1]).unwrap();
389        assert_eq!(idx, 1);
390
391        // Vector close to centroid 3
392        let (idx, _) = codebook.find_nearest(&[0.9, 0.9, 0.9]).unwrap();
393        assert_eq!(idx, 3);
394    }
395
396    #[test]
397    fn test_encode_decode() {
398        let config = CodebookConfig {
399            num_centroids: 256,
400            centroid_dim: 4,
401            index_bits: 8,
402            product_quantization: false,
403            pq_subspaces: 1,
404        };
405
406        let codebook = Codebook::new(config, "test");
407
408        // Create some test vectors
409        let vectors: Vec<f32> = (0..40).map(|i| i as f32 * 0.01).collect();
410
411        // Encode
412        let indices = codebook.encode_batch(&vectors, 4).unwrap();
413        assert_eq!(indices.len(), 10);
414
415        // Decode
416        let decoded = codebook.decode_batch(&indices);
417        assert_eq!(decoded.len(), 40);
418    }
419
420    #[test]
421    fn test_layer_codebook() {
422        let lc = LayerCodebook::with_defaults("sdxl");
423
424        assert!(lc.get(LayerType::AttentionQK).is_some());
425        assert!(lc.get(LayerType::FFN).is_some());
426        assert!(lc.total_memory > 0);
427    }
428
429    #[test]
430    fn test_memory_size() {
431        let config = CodebookConfig::attention_qk();
432        assert_eq!(config.memory_size(), 4096 * 64 * 4); // 1MB
433    }
434}