Skip to main content

trustformers_core/quantization/
mixed_bit.rs

1//! Mixed-bit quantization implementation for TrustformeRS
2//!
3//! This module provides mixed-bit quantization where different layers/channels
4//! can use different quantization bit widths based on their importance and
5//! sensitivity to quantization errors.
6
7#![allow(unused_variables)] // Mixed-bit quantization implementation
8
9use crate::errors::Result;
10use crate::quantization::base::QuantizationScheme;
11use crate::tensor::Tensor;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15/// Mixed-bit quantization configuration
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct MixedBitConfig {
18    /// Layer-specific quantization configurations
19    pub layer_configs: HashMap<String, LayerQuantConfig>,
20    /// Default configuration for layers not specified
21    pub default_config: LayerQuantConfig,
22    /// Sensitivity analysis configuration
23    pub sensitivity_config: SensitivityConfig,
24    /// Automatic bit allocation strategy
25    pub auto_bit_allocation: Option<AutoBitAllocationStrategy>,
26}
27
28/// Layer-specific quantization configuration
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct LayerQuantConfig {
31    /// Bit width for weights
32    pub weight_bits: u8,
33    /// Bit width for activations
34    pub activation_bits: u8,
35    /// Quantization scheme to use
36    pub scheme: QuantizationScheme,
37    /// Whether to use symmetric quantization
38    pub symmetric: bool,
39    /// Group size for grouped quantization
40    pub group_size: Option<usize>,
41    /// Channel-specific bit allocation
42    pub channel_bits: Option<Vec<u8>>,
43}
44
45/// Sensitivity analysis configuration
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SensitivityConfig {
48    /// Number of calibration samples
49    pub calibration_samples: usize,
50    /// Threshold for sensitivity (higher = more sensitive)
51    pub sensitivity_threshold: f32,
52    /// Metrics to consider for sensitivity analysis
53    pub metrics: Vec<SensitivityMetric>,
54}
55
56/// Sensitivity metrics for determining quantization bit allocation
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub enum SensitivityMetric {
59    /// Gradient magnitude
60    GradientMagnitude,
61    /// Hessian diagonal
62    HessianDiagonal,
63    /// Activation variance
64    ActivationVariance,
65    /// Weight variance
66    WeightVariance,
67    /// Output sensitivity
68    OutputSensitivity,
69}
70
71/// Automatic bit allocation strategies
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum AutoBitAllocationStrategy {
74    /// Sensitivity-based allocation
75    SensitivityBased {
76        /// Target model size compression ratio
77        target_compression: f32,
78        /// Minimum bits per layer
79        min_bits: u8,
80        /// Maximum bits per layer
81        max_bits: u8,
82    },
83    /// Uniform allocation with adaptive adjustment
84    AdaptiveUniform {
85        /// Base bit width
86        base_bits: u8,
87        /// Adjustment range
88        adjustment_range: u8,
89    },
90    /// Performance-driven allocation
91    PerformanceDriven {
92        /// Target inference latency (ms)
93        target_latency: f32,
94        /// Accuracy tolerance
95        accuracy_tolerance: f32,
96    },
97}
98
99/// Mixed-bit quantized tensor
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct MixedBitQuantizedTensor {
102    /// Layer name
103    pub layer_name: String,
104    /// Quantized data with different bit widths
105    pub quantized_data: Vec<QuantizedBlock>,
106    /// Original tensor shape
107    pub shape: Vec<usize>,
108    /// Quantization configuration used
109    pub config: LayerQuantConfig,
110    /// Sensitivity scores for each block
111    pub sensitivity_scores: Vec<f32>,
112}
113
114/// A block of quantized data with specific bit width
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct QuantizedBlock {
117    /// Quantized data
118    pub data: Vec<u8>,
119    /// Scale factor
120    pub scale: f32,
121    /// Zero point
122    pub zero_point: i32,
123    /// Bit width used for this block
124    pub bit_width: u8,
125    /// Block shape
126    pub block_shape: Vec<usize>,
127    /// Block offset in the original tensor
128    pub block_offset: Vec<usize>,
129}
130
131/// Mixed-bit quantizer
132pub struct MixedBitQuantizer {
133    config: MixedBitConfig,
134    sensitivity_analyzer: SensitivityAnalyzer,
135}
136
137/// Sensitivity analyzer for determining optimal bit allocations
138struct SensitivityAnalyzer {
139    config: SensitivityConfig,
140    sensitivity_cache: HashMap<String, Vec<f32>>,
141}
142
143impl Default for MixedBitConfig {
144    fn default() -> Self {
145        Self {
146            layer_configs: HashMap::new(),
147            default_config: LayerQuantConfig::default(),
148            sensitivity_config: SensitivityConfig::default(),
149            auto_bit_allocation: Some(AutoBitAllocationStrategy::SensitivityBased {
150                target_compression: 0.25, // 4x compression
151                min_bits: 2,
152                max_bits: 8,
153            }),
154        }
155    }
156}
157
158impl Default for LayerQuantConfig {
159    fn default() -> Self {
160        Self {
161            weight_bits: 4,
162            activation_bits: 8,
163            scheme: QuantizationScheme::Int4,
164            symmetric: true,
165            group_size: Some(128),
166            channel_bits: None,
167        }
168    }
169}
170
171impl Default for SensitivityConfig {
172    fn default() -> Self {
173        Self {
174            calibration_samples: 128,
175            sensitivity_threshold: 0.01,
176            metrics: vec![
177                SensitivityMetric::GradientMagnitude,
178                SensitivityMetric::ActivationVariance,
179                SensitivityMetric::WeightVariance,
180            ],
181        }
182    }
183}
184
185impl MixedBitQuantizer {
186    /// Create a new mixed-bit quantizer
187    pub fn new(config: MixedBitConfig) -> Self {
188        let sensitivity_analyzer = SensitivityAnalyzer::new(config.sensitivity_config.clone());
189        Self {
190            config,
191            sensitivity_analyzer,
192        }
193    }
194
195    /// Quantize a tensor using mixed-bit quantization
196    pub fn quantize(
197        &mut self,
198        tensor: &Tensor,
199        layer_name: &str,
200    ) -> Result<MixedBitQuantizedTensor> {
201        // Get or create layer configuration
202        let layer_config = self
203            .config
204            .layer_configs
205            .get(layer_name)
206            .cloned()
207            .unwrap_or_else(|| self.config.default_config.clone());
208
209        // Analyze sensitivity if needed
210        let sensitivity_scores = if let Some(ref auto_strategy) = self.config.auto_bit_allocation {
211            self.sensitivity_analyzer
212                .analyze_sensitivity(tensor, layer_name, &layer_config)?
213        } else {
214            vec![1.0; tensor.shape().iter().product()]
215        };
216
217        // Allocate bits based on sensitivity
218        let bit_allocation = self.allocate_bits(&sensitivity_scores, &layer_config)?;
219
220        // Quantize tensor into blocks
221        let quantized_blocks = self.quantize_blocks(tensor, &bit_allocation, &layer_config)?;
222
223        Ok(MixedBitQuantizedTensor {
224            layer_name: layer_name.to_string(),
225            quantized_data: quantized_blocks,
226            shape: tensor.shape(),
227            config: layer_config,
228            sensitivity_scores,
229        })
230    }
231
232    /// Allocate bits based on sensitivity scores
233    fn allocate_bits(
234        &self,
235        sensitivity_scores: &[f32],
236        config: &LayerQuantConfig,
237    ) -> Result<Vec<u8>> {
238        let mut bit_allocation = vec![config.weight_bits; sensitivity_scores.len()];
239
240        if let Some(ref strategy) = self.config.auto_bit_allocation {
241            match strategy {
242                AutoBitAllocationStrategy::SensitivityBased {
243                    target_compression,
244                    min_bits,
245                    max_bits,
246                } => {
247                    // Sort indices by sensitivity
248                    let mut indexed_scores: Vec<(usize, f32)> = sensitivity_scores
249                        .iter()
250                        .enumerate()
251                        .map(|(i, &score)| (i, score))
252                        .collect();
253                    indexed_scores.sort_by(|a, b| {
254                        b.1.partial_cmp(&a.1).unwrap_or(::std::cmp::Ordering::Equal)
255                    });
256
257                    // Calculate target total bits
258                    let total_elements = sensitivity_scores.len();
259                    let target_total_bits = (total_elements as f32
260                        * config.weight_bits as f32
261                        * target_compression) as usize;
262                    let mut allocated_bits = 0;
263
264                    // Allocate bits starting from most sensitive
265                    for (idx, _) in indexed_scores {
266                        let remaining_elements =
267                            total_elements - allocated_bits / (*max_bits as usize);
268                        let remaining_budget = target_total_bits.saturating_sub(allocated_bits);
269
270                        let avg_bits_remaining =
271                            remaining_budget.checked_div(remaining_elements).unwrap_or(0);
272                        if avg_bits_remaining > 0 {
273                            let bits = (avg_bits_remaining as u8).clamp(*min_bits, *max_bits);
274                            bit_allocation[idx] = bits;
275                            allocated_bits += bits as usize;
276                        }
277                    }
278                },
279                AutoBitAllocationStrategy::AdaptiveUniform {
280                    base_bits,
281                    adjustment_range,
282                } => {
283                    // Calculate mean sensitivity
284                    let mean_sensitivity =
285                        sensitivity_scores.iter().sum::<f32>() / sensitivity_scores.len() as f32;
286
287                    for (i, &score) in sensitivity_scores.iter().enumerate() {
288                        let normalized_score = score / mean_sensitivity;
289                        let adjustment = (normalized_score * *adjustment_range as f32) as i8;
290                        let bits = (*base_bits as i8 + adjustment).clamp(1, 8) as u8;
291                        bit_allocation[i] = bits;
292                    }
293                },
294                AutoBitAllocationStrategy::PerformanceDriven {
295                    target_latency,
296                    accuracy_tolerance,
297                } => {
298                    // Performance-driven allocation optimizes for latency while maintaining accuracy
299                    return self.allocate_bits_performance_driven(
300                        sensitivity_scores,
301                        config,
302                        *target_latency,
303                        *accuracy_tolerance,
304                    );
305                },
306            }
307        }
308
309        Ok(bit_allocation)
310    }
311
312    /// Allocate bits based on sensitivity scores (fallback implementation)
313    #[allow(dead_code)]
314    fn allocate_bits_sensitivity_based(
315        &self,
316        sensitivity_scores: &[f32],
317        config: &LayerQuantConfig,
318    ) -> Result<Vec<u8>> {
319        let mut bit_allocation = vec![config.weight_bits; sensitivity_scores.len()];
320
321        // Find sensitivity percentiles
322        let mut sorted_scores = sensitivity_scores.to_vec();
323        sorted_scores.sort_by(|a, b| a.partial_cmp(b).unwrap_or(::std::cmp::Ordering::Equal));
324
325        let high_sensitivity_threshold =
326            sorted_scores[(sorted_scores.len() * 90 / 100).min(sorted_scores.len() - 1)];
327        let low_sensitivity_threshold = sorted_scores[sorted_scores.len() * 10 / 100];
328
329        for (i, &score) in sensitivity_scores.iter().enumerate() {
330            if score >= high_sensitivity_threshold {
331                bit_allocation[i] = 8; // High precision for sensitive parts
332            } else if score <= low_sensitivity_threshold {
333                bit_allocation[i] = 2; // Low precision for insensitive parts
334            } else {
335                bit_allocation[i] = 4; // Medium precision
336            }
337        }
338
339        Ok(bit_allocation)
340    }
341
342    /// Performance-driven bit allocation optimizing for latency while maintaining accuracy
343    fn allocate_bits_performance_driven(
344        &self,
345        sensitivity_scores: &[f32],
346        config: &LayerQuantConfig,
347        target_latency: f32,
348        accuracy_tolerance: f32,
349    ) -> Result<Vec<u8>> {
350        let total_elements = sensitivity_scores.len();
351
352        // Model performance characteristics (simplified model)
353        // Lower bits = faster computation but potentially lower accuracy
354        let performance_factor = |bits: u8| -> f32 {
355            match bits {
356                1 => 0.1,  // Very fast, very low accuracy impact
357                2 => 0.25, // Fast, low accuracy impact
358                3 => 0.4,  // Medium-fast, medium accuracy impact
359                4 => 0.6,  // Medium, medium accuracy impact
360                5 => 0.75, // Medium-slow, higher accuracy
361                6 => 0.85, // Slow, high accuracy
362                7 => 0.92, // Very slow, very high accuracy
363                8 => 1.0,  // Slowest, highest accuracy
364                _ => 1.0,
365            }
366        };
367
368        // Calculate accuracy impact based on sensitivity
369        let accuracy_impact = |sensitivity: f32, bits: u8| -> f32 {
370            let base_impact = sensitivity / 100.0; // Normalize sensitivity
371            let bit_factor = (8.0 - bits as f32) / 7.0; // Higher impact with fewer bits
372            base_impact * bit_factor
373        };
374
375        // Sort elements by sensitivity to prioritize important layers
376        let mut indexed_scores: Vec<(usize, f32)> =
377            sensitivity_scores.iter().enumerate().map(|(i, &score)| (i, score)).collect();
378        indexed_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(::std::cmp::Ordering::Equal));
379
380        // Start with lowest bits for maximum performance
381        let mut current_bits = vec![2u8; total_elements];
382        let mut current_latency = 0.0;
383        let mut current_accuracy_loss = 0.0;
384
385        // Calculate initial latency and accuracy
386        for (i, &score) in sensitivity_scores.iter().enumerate() {
387            current_latency += performance_factor(2);
388            current_accuracy_loss += accuracy_impact(score, 2);
389        }
390
391        // Iteratively increase bits for most sensitive elements until we hit constraints
392        for (idx, sensitivity) in indexed_scores {
393            let current_element_bits = current_bits[idx];
394
395            // Try increasing bits for this element
396            for new_bits in (current_element_bits + 1)..=8 {
397                let latency_change =
398                    performance_factor(new_bits) - performance_factor(current_element_bits);
399                let accuracy_change = accuracy_impact(sensitivity, current_element_bits)
400                    - accuracy_impact(sensitivity, new_bits);
401
402                let new_latency = current_latency + latency_change;
403                let new_accuracy_loss = current_accuracy_loss - accuracy_change;
404
405                // Check if this change fits within our constraints
406                let normalized_latency = new_latency / total_elements as f32;
407                if normalized_latency <= target_latency && new_accuracy_loss <= accuracy_tolerance {
408                    // Apply the change
409                    current_bits[idx] = new_bits;
410                    current_latency = new_latency;
411                    current_accuracy_loss = new_accuracy_loss;
412                } else {
413                    // Can't improve this element further, move to next
414                    break;
415                }
416            }
417        }
418
419        // Apply final allocation
420        let bit_allocation = current_bits;
421
422        Ok(bit_allocation)
423    }
424
425    /// Quantize tensor into blocks with different bit widths
426    fn quantize_blocks(
427        &self,
428        tensor: &Tensor,
429        bit_allocation: &[u8],
430        config: &LayerQuantConfig,
431    ) -> Result<Vec<QuantizedBlock>> {
432        let data = tensor.data()?;
433        let shape = tensor.shape();
434        let mut blocks = Vec::new();
435
436        // Group elements by bit width
437        let mut bit_groups: HashMap<u8, Vec<(usize, f32)>> = HashMap::new();
438        for (i, (&bits, &value)) in bit_allocation.iter().zip(data.iter()).enumerate() {
439            bit_groups.entry(bits).or_default().push((i, value));
440        }
441
442        // Quantize each group
443        for (bit_width, elements) in bit_groups {
444            let values: Vec<f32> = elements.iter().map(|(_, v)| *v).collect();
445            let indices: Vec<usize> = elements.iter().map(|(i, _)| *i).collect();
446
447            let (quantized_data, scale, zero_point) =
448                self.quantize_group(&values, bit_width, config)?;
449
450            blocks.push(QuantizedBlock {
451                data: quantized_data,
452                scale,
453                zero_point,
454                bit_width,
455                block_shape: vec![values.len()],
456                block_offset: vec![indices[0]], // Simplified for now
457            });
458        }
459
460        Ok(blocks)
461    }
462
463    /// Quantize a group of values with specified bit width
464    fn quantize_group(
465        &self,
466        values: &[f32],
467        bit_width: u8,
468        config: &LayerQuantConfig,
469    ) -> Result<(Vec<u8>, f32, i32)> {
470        if values.is_empty() {
471            return Ok((Vec::new(), 1.0, 0));
472        }
473
474        let min_val = values.iter().fold(f32::INFINITY, |a, &b| a.min(b));
475        let max_val = values.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
476
477        let qmin = 0;
478        let qmax = (1 << bit_width) - 1;
479
480        let (scale, zero_point) = if config.symmetric {
481            let max_abs = max_val.abs().max(min_val.abs());
482            let scale = max_abs / (qmax as f32 / 2.0);
483            (scale, qmax / 2)
484        } else {
485            let scale = (max_val - min_val) / (qmax - qmin) as f32;
486            let zero_point = qmin as f32 - min_val / scale;
487            (scale, zero_point.round() as i32)
488        };
489
490        let mut quantized = Vec::with_capacity(values.len());
491        for &value in values {
492            let q_val = (value / scale + zero_point as f32).round() as i32;
493            let clamped = q_val.clamp(qmin, qmax) as u8;
494            quantized.push(clamped);
495        }
496
497        Ok((quantized, scale, zero_point))
498    }
499
500    /// Get compression ratio achieved
501    pub fn compression_ratio(
502        &self,
503        original_size: usize,
504        quantized_tensor: &MixedBitQuantizedTensor,
505    ) -> f32 {
506        let compressed_size: usize =
507            quantized_tensor.quantized_data.iter().map(|block| block.data.len()).sum();
508
509        original_size as f32 / compressed_size as f32
510    }
511
512    /// Estimate memory savings
513    pub fn memory_savings(
514        &self,
515        original_tensor: &Tensor,
516        quantized_tensor: &MixedBitQuantizedTensor,
517    ) -> f32 {
518        let original_bytes = original_tensor.size() * std::mem::size_of::<f32>();
519        let quantized_bytes: usize =
520            quantized_tensor.quantized_data.iter().map(|block| block.data.len()).sum();
521
522        1.0 - (quantized_bytes as f32 / original_bytes as f32)
523    }
524}
525
526impl MixedBitQuantizedTensor {
527    /// Dequantize back to original tensor
528    pub fn dequantize(&self) -> Result<Tensor> {
529        let total_elements: usize = self.shape.iter().product();
530        let mut result = vec![0.0f32; total_elements];
531
532        for block in &self.quantized_data {
533            for (i, &quantized_val) in block.data.iter().enumerate() {
534                let dequantized = (quantized_val as i32 - block.zero_point) as f32 * block.scale;
535                // Simplified mapping - in practice, would need proper index mapping
536                if i < result.len() {
537                    result[i] = dequantized;
538                }
539            }
540        }
541
542        Tensor::from_vec(result, &self.shape)
543    }
544
545    /// Get average bit width used
546    pub fn average_bit_width(&self) -> f32 {
547        let total_elements: usize = self.quantized_data.iter().map(|b| b.data.len()).sum();
548        if total_elements == 0 {
549            return 0.0;
550        }
551
552        let total_bits: f32 = self
553            .quantized_data
554            .iter()
555            .map(|block| block.data.len() as f32 * block.bit_width as f32)
556            .sum();
557
558        total_bits / total_elements as f32
559    }
560
561    /// Get memory footprint in bytes
562    pub fn memory_footprint(&self) -> usize {
563        self.quantized_data.iter().map(|block| block.data.len()).sum()
564    }
565}
566
567impl SensitivityAnalyzer {
568    fn new(config: SensitivityConfig) -> Self {
569        Self {
570            config,
571            sensitivity_cache: HashMap::new(),
572        }
573    }
574
575    /// Analyze sensitivity of tensor elements
576    fn analyze_sensitivity(
577        &mut self,
578        tensor: &Tensor,
579        layer_name: &str,
580        _config: &LayerQuantConfig,
581    ) -> Result<Vec<f32>> {
582        // Check cache first
583        if let Some(cached_scores) = self.sensitivity_cache.get(layer_name) {
584            return Ok(cached_scores.clone());
585        }
586
587        let data = tensor.data()?;
588        let mut sensitivity_scores = vec![0.0; data.len()];
589
590        // Analyze each configured metric
591        for metric in &self.config.metrics {
592            let metric_scores = self.compute_metric_scores(tensor, metric)?;
593
594            // Combine metrics (simple averaging for now)
595            for (i, score) in metric_scores.iter().enumerate() {
596                sensitivity_scores[i] += score / self.config.metrics.len() as f32;
597            }
598        }
599
600        // Cache the results
601        self.sensitivity_cache
602            .insert(layer_name.to_string(), sensitivity_scores.clone());
603
604        Ok(sensitivity_scores)
605    }
606
607    /// Compute sensitivity scores for a specific metric
608    fn compute_metric_scores(
609        &self,
610        tensor: &Tensor,
611        metric: &SensitivityMetric,
612    ) -> Result<Vec<f32>> {
613        let data = tensor.data()?;
614
615        match metric {
616            SensitivityMetric::WeightVariance => {
617                // Compute local variance as a sensitivity measure
618                let mean = data.iter().sum::<f32>() / data.len() as f32;
619                let variance: Vec<f32> = data.iter().map(|&x| (x - mean).powi(2)).collect();
620                Ok(variance)
621            },
622            SensitivityMetric::GradientMagnitude => {
623                // Approximate gradient magnitude using weight magnitude
624                Ok(data.iter().map(|&x| x.abs()).collect())
625            },
626            SensitivityMetric::ActivationVariance => {
627                // For weights, use magnitude as proxy for activation impact
628                Ok(data.iter().map(|&x| x.abs()).collect())
629            },
630            SensitivityMetric::HessianDiagonal => {
631                // Simplified hessian approximation
632                let hessian_approx: Vec<f32> = data.iter().map(|&x| x.powi(2)).collect();
633                Ok(hessian_approx)
634            },
635            SensitivityMetric::OutputSensitivity => {
636                // Use weight magnitude as proxy for output sensitivity
637                Ok(data.iter().map(|&x| x.abs()).collect())
638            },
639        }
640    }
641}
642
643#[cfg(test)]
644mod tests {
645    use super::*;
646    use crate::tensor::Tensor;
647
648    #[test]
649    fn test_mixed_bit_quantizer_creation() {
650        let config = MixedBitConfig::default();
651        let quantizer = MixedBitQuantizer::new(config);
652        assert!(quantizer.config.auto_bit_allocation.is_some());
653    }
654
655    #[test]
656    fn test_mixed_bit_quantization() -> Result<()> {
657        let mut quantizer = MixedBitQuantizer::new(MixedBitConfig::default());
658        let tensor = Tensor::randn(&[4, 4])?;
659
660        let quantized = quantizer.quantize(&tensor, "test_layer")?;
661        assert_eq!(quantized.shape, vec![4, 4]);
662        assert!(!quantized.quantized_data.is_empty());
663
664        Ok(())
665    }
666
667    #[test]
668    fn test_mixed_bit_dequantization() -> Result<()> {
669        let mut quantizer = MixedBitQuantizer::new(MixedBitConfig::default());
670        let tensor = Tensor::randn(&[2, 2])?;
671
672        let quantized = quantizer.quantize(&tensor, "test_layer")?;
673        let dequantized = quantized.dequantize()?;
674
675        assert_eq!(dequantized.shape(), tensor.shape());
676        Ok(())
677    }
678
679    #[test]
680    fn test_average_bit_width() -> Result<()> {
681        let mut quantizer = MixedBitQuantizer::new(MixedBitConfig::default());
682        let tensor = Tensor::randn(&[8])?;
683
684        let quantized = quantizer.quantize(&tensor, "test_layer")?;
685        let avg_bits = quantized.average_bit_width();
686
687        assert!(avg_bits > 0.0);
688        assert!(avg_bits <= 8.0);
689        Ok(())
690    }
691
692    #[test]
693    fn test_compression_ratio() -> Result<()> {
694        let mut quantizer = MixedBitQuantizer::new(MixedBitConfig::default());
695        let tensor = Tensor::randn(&[1024])?; // Use larger tensor to overcome metadata overhead
696
697        let quantized = quantizer.quantize(&tensor, "test_layer")?;
698        let ratio = quantizer.compression_ratio(tensor.size(), &quantized);
699
700        assert!(ratio >= 1.0); // Current implementation stores as bytes, so ratio may be 1.0
701        Ok(())
702    }
703
704    #[test]
705    fn test_sensitivity_analysis() -> Result<()> {
706        let config = SensitivityConfig::default();
707        let mut analyzer = SensitivityAnalyzer::new(config);
708        let tensor = Tensor::randn(&[4, 4])?;
709
710        let layer_config = LayerQuantConfig::default();
711        let scores = analyzer.analyze_sensitivity(&tensor, "test_layer", &layer_config)?;
712
713        assert_eq!(scores.len(), 16);
714        assert!(scores.iter().all(|&score| score >= 0.0));
715        Ok(())
716    }
717}