use crate::{Codebook, LayerCodebook, LayerType, NeuralError, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncoderConfig {
pub target_ratio: f32,
pub max_quality_loss: f32,
pub use_residual: bool,
pub residual_bits: u8,
pub batch_size: usize,
}
impl Default for EncoderConfig {
fn default() -> Self {
Self {
target_ratio: 10.0,
max_quality_loss: 0.01,
use_residual: true,
residual_bits: 4,
batch_size: 1024,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EncodedTensor {
pub name: String,
pub shape: Vec<usize>,
pub dtype: String,
pub layer_type: LayerType,
pub indices: Vec<u16>,
pub residuals: Option<Vec<i8>>,
pub residual_scale: f32,
pub stats: EncodingStats,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EncodingStats {
pub mse: f32,
pub psnr: f32,
pub compression_ratio: f32,
pub original_size: usize,
pub compressed_size: usize,
}
#[derive(Debug)]
pub struct NeuralEncoder {
config: EncoderConfig,
codebooks: LayerCodebook,
}
impl NeuralEncoder {
pub fn new(config: EncoderConfig, codebooks: LayerCodebook) -> Self {
Self { config, codebooks }
}
pub fn encode_tensor(
&self,
name: &str,
data: &[f32],
shape: &[usize],
layer_type: LayerType,
) -> Result<EncodedTensor> {
let codebook = self
.codebooks
.get(layer_type)
.ok_or_else(|| NeuralError::CodebookNotFound(format!("{:?}", layer_type)))?;
let dim = codebook.config.centroid_dim;
let num_vectors = data.len() / dim;
if !data.len().is_multiple_of(dim) {
return Err(NeuralError::DimensionMismatch {
expected: (num_vectors + 1) * dim,
actual: data.len(),
});
}
let indices = codebook.encode_batch(data, dim)?;
let (residuals, residual_scale) = if self.config.use_residual {
self.compute_residuals(data, &indices, codebook)
} else {
(None, 0.0)
};
let reconstructed = codebook.decode_batch(&indices);
let mse = self.compute_mse(data, &reconstructed);
let psnr = self.compute_psnr(mse);
let original_size = data.len() * 4; let index_size = indices.len() * 2; let residual_size = residuals.as_ref().map(|r| r.len()).unwrap_or(0);
let compressed_size = index_size + residual_size;
let stats = EncodingStats {
mse,
psnr,
compression_ratio: original_size as f32 / compressed_size as f32,
original_size,
compressed_size,
};
Ok(EncodedTensor {
name: name.to_string(),
shape: shape.to_vec(),
dtype: "float32".to_string(),
layer_type,
indices,
residuals,
residual_scale,
stats,
})
}
fn compute_residuals(
&self,
original: &[f32],
indices: &[u16],
codebook: &Codebook,
) -> (Option<Vec<i8>>, f32) {
let reconstructed = codebook.decode_batch(indices);
let residuals: Vec<f32> = original
.iter()
.zip(reconstructed.iter())
.map(|(o, r)| o - r)
.collect();
let max_abs = residuals.iter().map(|r| r.abs()).fold(0.0f32, f32::max);
if max_abs < 1e-6 {
return (None, 0.0);
}
let scale = max_abs / 127.0;
let quantized: Vec<i8> = residuals
.iter()
.map(|r| (r / scale).clamp(-127.0, 127.0) as i8)
.collect();
(Some(quantized), scale)
}
fn compute_mse(&self, original: &[f32], reconstructed: &[f32]) -> f32 {
if original.len() != reconstructed.len() || original.is_empty() {
return f32::INFINITY;
}
let sum: f32 = original
.iter()
.zip(reconstructed.iter())
.map(|(o, r)| (o - r).powi(2))
.sum();
sum / original.len() as f32
}
fn compute_psnr(&self, mse: f32) -> f32 {
if mse < 1e-10 {
return 100.0; }
10.0 * (1.0 / mse).log10()
}
pub fn encode_tensors(
&self,
tensors: &[(String, Vec<f32>, Vec<usize>, LayerType)],
) -> Result<Vec<EncodedTensor>> {
tensors
.iter()
.map(|(name, data, shape, layer_type)| {
self.encode_tensor(name, data, shape, *layer_type)
})
.collect()
}
pub fn config(&self) -> &EncoderConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_tensor() {
let codebooks = LayerCodebook::with_defaults("test");
let encoder = NeuralEncoder::new(EncoderConfig::default(), codebooks);
let data: Vec<f32> = (0..640).map(|i| (i as f32 / 640.0) - 0.5).collect();
let shape = vec![10, 64];
let encoded = encoder
.encode_tensor("test.weight", &data, &shape, LayerType::AttentionQK)
.unwrap();
assert_eq!(encoded.name, "test.weight");
assert_eq!(encoded.indices.len(), 10);
assert!(encoded.stats.compression_ratio > 1.0);
}
#[test]
fn test_residual_encoding() {
let config = EncoderConfig {
use_residual: true,
..Default::default()
};
let codebooks = LayerCodebook::with_defaults("test");
let encoder = NeuralEncoder::new(config, codebooks);
let data: Vec<f32> = (0..640).map(|i| (i as f32 / 640.0) - 0.5).collect();
let shape = vec![10, 64];
let encoded = encoder
.encode_tensor("test.weight", &data, &shape, LayerType::AttentionQK)
.unwrap();
assert!(encoded.residuals.is_some());
assert!(encoded.residual_scale > 0.0);
}
#[test]
fn test_psnr_calculation() {
let codebooks = LayerCodebook::with_defaults("test");
let encoder = NeuralEncoder::new(EncoderConfig::default(), codebooks);
assert!(encoder.compute_psnr(0.001) >= 30.0);
assert!(encoder.compute_psnr(0.1) < 20.0);
}
}