Skip to main content

haagenti_neural/
encoder.rs

1//! Neural encoder for compressing model weights
2
3use crate::{Codebook, LayerCodebook, LayerType, NeuralError, Result};
4use serde::{Deserialize, Serialize};
5
6/// Configuration for the encoder
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct EncoderConfig {
9    /// Target compression ratio
10    pub target_ratio: f32,
11    /// Maximum quality loss allowed
12    pub max_quality_loss: f32,
13    /// Whether to use residual encoding
14    pub use_residual: bool,
15    /// Residual quantization bits
16    pub residual_bits: u8,
17    /// Batch size for encoding
18    pub batch_size: usize,
19}
20
21impl Default for EncoderConfig {
22    fn default() -> Self {
23        Self {
24            target_ratio: 10.0,
25            max_quality_loss: 0.01,
26            use_residual: true,
27            residual_bits: 4,
28            batch_size: 1024,
29        }
30    }
31}
32
33/// Encoded tensor data
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct EncodedTensor {
36    /// Original tensor name
37    pub name: String,
38    /// Original shape
39    pub shape: Vec<usize>,
40    /// Original dtype
41    pub dtype: String,
42    /// Layer type for codebook selection
43    pub layer_type: LayerType,
44    /// Quantized indices
45    pub indices: Vec<u16>,
46    /// Residual data (if enabled)
47    pub residuals: Option<Vec<i8>>,
48    /// Scale factor for residuals
49    pub residual_scale: f32,
50    /// Compression statistics
51    pub stats: EncodingStats,
52}
53
54/// Statistics from encoding
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct EncodingStats {
57    /// Mean squared error
58    pub mse: f32,
59    /// Peak signal-to-noise ratio
60    pub psnr: f32,
61    /// Compression ratio achieved
62    pub compression_ratio: f32,
63    /// Original size in bytes
64    pub original_size: usize,
65    /// Compressed size in bytes
66    pub compressed_size: usize,
67}
68
69/// Neural encoder for model compression
70#[derive(Debug)]
71pub struct NeuralEncoder {
72    config: EncoderConfig,
73    codebooks: LayerCodebook,
74}
75
76impl NeuralEncoder {
77    /// Create a new encoder with codebooks
78    pub fn new(config: EncoderConfig, codebooks: LayerCodebook) -> Self {
79        Self { config, codebooks }
80    }
81
82    /// Encode a single tensor
83    pub fn encode_tensor(
84        &self,
85        name: &str,
86        data: &[f32],
87        shape: &[usize],
88        layer_type: LayerType,
89    ) -> Result<EncodedTensor> {
90        let codebook = self
91            .codebooks
92            .get(layer_type)
93            .ok_or_else(|| NeuralError::CodebookNotFound(format!("{:?}", layer_type)))?;
94
95        let dim = codebook.config.centroid_dim;
96        let num_vectors = data.len() / dim;
97
98        if !data.len().is_multiple_of(dim) {
99            return Err(NeuralError::DimensionMismatch {
100                expected: (num_vectors + 1) * dim,
101                actual: data.len(),
102            });
103        }
104
105        // Encode to indices
106        let indices = codebook.encode_batch(data, dim)?;
107
108        // Compute residuals if enabled
109        let (residuals, residual_scale) = if self.config.use_residual {
110            self.compute_residuals(data, &indices, codebook)
111        } else {
112            (None, 0.0)
113        };
114
115        // Compute statistics
116        let reconstructed = codebook.decode_batch(&indices);
117        let mse = self.compute_mse(data, &reconstructed);
118        let psnr = self.compute_psnr(mse);
119
120        let original_size = data.len() * 4; // FP32
121        let index_size = indices.len() * 2; // U16
122        let residual_size = residuals.as_ref().map(|r| r.len()).unwrap_or(0);
123        let compressed_size = index_size + residual_size;
124
125        let stats = EncodingStats {
126            mse,
127            psnr,
128            compression_ratio: original_size as f32 / compressed_size as f32,
129            original_size,
130            compressed_size,
131        };
132
133        Ok(EncodedTensor {
134            name: name.to_string(),
135            shape: shape.to_vec(),
136            dtype: "float32".to_string(),
137            layer_type,
138            indices,
139            residuals,
140            residual_scale,
141            stats,
142        })
143    }
144
145    /// Compute residuals between original and quantized
146    fn compute_residuals(
147        &self,
148        original: &[f32],
149        indices: &[u16],
150        codebook: &Codebook,
151    ) -> (Option<Vec<i8>>, f32) {
152        let reconstructed = codebook.decode_batch(indices);
153
154        // Compute difference
155        let residuals: Vec<f32> = original
156            .iter()
157            .zip(reconstructed.iter())
158            .map(|(o, r)| o - r)
159            .collect();
160
161        // Find scale to fit in i8 range
162        let max_abs = residuals.iter().map(|r| r.abs()).fold(0.0f32, f32::max);
163
164        if max_abs < 1e-6 {
165            return (None, 0.0);
166        }
167
168        let scale = max_abs / 127.0;
169
170        // Quantize residuals to i8
171        let quantized: Vec<i8> = residuals
172            .iter()
173            .map(|r| (r / scale).clamp(-127.0, 127.0) as i8)
174            .collect();
175
176        (Some(quantized), scale)
177    }
178
179    /// Compute mean squared error
180    fn compute_mse(&self, original: &[f32], reconstructed: &[f32]) -> f32 {
181        if original.len() != reconstructed.len() || original.is_empty() {
182            return f32::INFINITY;
183        }
184
185        let sum: f32 = original
186            .iter()
187            .zip(reconstructed.iter())
188            .map(|(o, r)| (o - r).powi(2))
189            .sum();
190
191        sum / original.len() as f32
192    }
193
194    /// Compute PSNR from MSE
195    fn compute_psnr(&self, mse: f32) -> f32 {
196        if mse < 1e-10 {
197            return 100.0; // Perfect reconstruction
198        }
199        10.0 * (1.0 / mse).log10()
200    }
201
202    /// Encode multiple tensors
203    pub fn encode_tensors(
204        &self,
205        tensors: &[(String, Vec<f32>, Vec<usize>, LayerType)],
206    ) -> Result<Vec<EncodedTensor>> {
207        tensors
208            .iter()
209            .map(|(name, data, shape, layer_type)| {
210                self.encode_tensor(name, data, shape, *layer_type)
211            })
212            .collect()
213    }
214
215    /// Get encoder configuration
216    pub fn config(&self) -> &EncoderConfig {
217        &self.config
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_encode_tensor() {
227        let codebooks = LayerCodebook::with_defaults("test");
228        let encoder = NeuralEncoder::new(EncoderConfig::default(), codebooks);
229
230        // Create test data matching centroid dimension (64 for QK)
231        let data: Vec<f32> = (0..640).map(|i| (i as f32 / 640.0) - 0.5).collect();
232        let shape = vec![10, 64];
233
234        let encoded = encoder
235            .encode_tensor("test.weight", &data, &shape, LayerType::AttentionQK)
236            .unwrap();
237
238        assert_eq!(encoded.name, "test.weight");
239        assert_eq!(encoded.indices.len(), 10);
240        assert!(encoded.stats.compression_ratio > 1.0);
241    }
242
243    #[test]
244    fn test_residual_encoding() {
245        let config = EncoderConfig {
246            use_residual: true,
247            ..Default::default()
248        };
249
250        let codebooks = LayerCodebook::with_defaults("test");
251        let encoder = NeuralEncoder::new(config, codebooks);
252
253        let data: Vec<f32> = (0..640).map(|i| (i as f32 / 640.0) - 0.5).collect();
254        let shape = vec![10, 64];
255
256        let encoded = encoder
257            .encode_tensor("test.weight", &data, &shape, LayerType::AttentionQK)
258            .unwrap();
259
260        assert!(encoded.residuals.is_some());
261        assert!(encoded.residual_scale > 0.0);
262    }
263
264    #[test]
265    fn test_psnr_calculation() {
266        let codebooks = LayerCodebook::with_defaults("test");
267        let encoder = NeuralEncoder::new(EncoderConfig::default(), codebooks);
268
269        // Low MSE should give high PSNR (MSE=0.001 -> PSNR=30.0)
270        assert!(encoder.compute_psnr(0.001) >= 30.0);
271        // High MSE should give low PSNR
272        assert!(encoder.compute_psnr(0.1) < 20.0);
273    }
274}