Skip to main content

torsh_nn/quantization/
qat.rs

1//! Quantization-Aware Training (QAT) support
2//!
3//! This module provides comprehensive quantization-aware training capabilities including:
4//! - Fake quantization for gradient-based training
5//! - QAT-aware layers and modules
6//! - Training schedulers for quantization parameters
7//! - Calibration-based initialization
8//! - Progressive quantization strategies
9
10use super::{QuantizationParams, QuantizationScheme};
11use crate::{Module, ModuleBase, Parameter};
12use torsh_core::{
13    dtype::DType,
14    error::{Result, TorshError},
15};
16use torsh_tensor::Tensor;
17
18// Conditional imports for std/no_std compatibility
19#[cfg(feature = "std")]
20use std::collections::HashMap;
21
22#[cfg(not(feature = "std"))]
23use hashbrown::HashMap;
24
25/// Quantization-Aware Training configuration
26#[derive(Debug, Clone)]
27pub struct QATConfig {
28    /// Enable fake quantization during training
29    pub fake_quantize_enabled: bool,
30    /// Number of warmup epochs before enabling quantization
31    pub warmup_epochs: usize,
32    /// Learning rate for quantization parameters
33    pub qparam_lr: f32,
34    /// Whether to learn scale and zero-point
35    pub learnable_params: bool,
36    /// Bit width for weights
37    pub weight_bits: u8,
38    /// Bit width for activations
39    pub activation_bits: u8,
40    /// Observer momentum for moving averages
41    pub observer_momentum: f32,
42    /// Quantization scheme
43    pub scheme: QuantizationScheme,
44    /// Whether to use per-channel quantization for weights
45    pub per_channel_weights: bool,
46    /// Whether to use per-channel quantization for activations
47    pub per_channel_activations: bool,
48}
49
50impl Default for QATConfig {
51    fn default() -> Self {
52        Self {
53            fake_quantize_enabled: true,
54            warmup_epochs: 3,
55            qparam_lr: 0.01,
56            learnable_params: true,
57            weight_bits: 8,
58            activation_bits: 8,
59            observer_momentum: 0.1,
60            scheme: QuantizationScheme::Symmetric,
61            per_channel_weights: true,
62            per_channel_activations: false,
63        }
64    }
65}
66
67/// Fake Quantization module for gradient-preserving quantization simulation
68#[derive(Debug)]
69pub struct FakeQuantize {
70    base: ModuleBase,
71    config: QATConfig,
72    scale: Parameter,
73    zero_point: Parameter,
74    min_val: f32,
75    max_val: f32,
76    num_batches_tracked: usize,
77    enabled: bool,
78}
79
80impl FakeQuantize {
81    /// Create a new fake quantization module
82    pub fn new(config: QATConfig) -> Self {
83        let mut base = ModuleBase::new();
84
85        // Initialize scale and zero_point as learnable parameters
86        let init_scale = 1.0;
87        let init_zero_point = 0.0;
88
89        let scale = Parameter::new(
90            torsh_tensor::creation::tensor_scalar(init_scale)
91                .expect("scalar tensor for scale should succeed"),
92        );
93        let zero_point = Parameter::new(
94            torsh_tensor::creation::tensor_scalar(init_zero_point)
95                .expect("scalar tensor for zero_point should succeed"),
96        );
97
98        if config.learnable_params {
99            base.register_parameter("scale".to_string(), scale.clone());
100            base.register_parameter("zero_point".to_string(), zero_point.clone());
101        }
102
103        Self {
104            base,
105            config,
106            scale,
107            zero_point,
108            min_val: f32::INFINITY,
109            max_val: f32::NEG_INFINITY,
110            num_batches_tracked: 0,
111            enabled: true,
112        }
113    }
114
115    /// Enable or disable fake quantization
116    pub fn enable(&mut self, enabled: bool) {
117        self.enabled = enabled;
118    }
119
120    /// Update observers with new tensor statistics
121    pub fn update_observers(&mut self, tensor: &Tensor) -> Result<()> {
122        if !self.training() {
123            return Ok(());
124        }
125
126        let data = tensor.to_vec()?;
127        let batch_min = data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
128        let batch_max = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
129
130        // Update running statistics with momentum
131        if self.num_batches_tracked == 0 {
132            self.min_val = batch_min;
133            self.max_val = batch_max;
134        } else {
135            let momentum = self.config.observer_momentum;
136            self.min_val = (1.0 - momentum) * self.min_val + momentum * batch_min;
137            self.max_val = (1.0 - momentum) * self.max_val + momentum * batch_max;
138        }
139
140        self.num_batches_tracked += 1;
141
142        // Update quantization parameters
143        self.update_qparams()?;
144
145        Ok(())
146    }
147
148    /// Update quantization parameters based on observed statistics
149    fn update_qparams(&mut self) -> Result<()> {
150        match self.config.scheme {
151            QuantizationScheme::Symmetric => {
152                let abs_max = self.max_val.abs().max(self.min_val.abs());
153                let scale = abs_max / ((1 << (self.config.weight_bits - 1)) - 1) as f32;
154
155                *self.scale.tensor().write() = torsh_tensor::creation::tensor_scalar(scale)?;
156                *self.zero_point.tensor().write() = torsh_tensor::creation::tensor_scalar(0.0)?;
157            }
158            QuantizationScheme::Asymmetric => {
159                let range = self.max_val - self.min_val;
160                let scale = range / ((1 << self.config.weight_bits) - 1) as f32;
161                let zero_point = -self.min_val / scale;
162
163                *self.scale.tensor().write() = torsh_tensor::creation::tensor_scalar(scale)?;
164                *self.zero_point.tensor().write() =
165                    torsh_tensor::creation::tensor_scalar(zero_point)?;
166            }
167            _ => {
168                return Err(TorshError::InvalidArgument(
169                    "Unsupported quantization scheme for fake quantization".to_string(),
170                ));
171            }
172        }
173
174        Ok(())
175    }
176
177    /// Apply fake quantization to a tensor
178    pub fn fake_quantize_tensor(&self, input: &Tensor) -> Result<Tensor> {
179        if !self.enabled || !self.config.fake_quantize_enabled {
180            return Ok(input.clone());
181        }
182
183        let scale = self.scale.tensor().read().to_vec()?[0];
184        let zero_point = self.zero_point.tensor().read().to_vec()?[0];
185
186        let qmin = match self.config.scheme {
187            QuantizationScheme::Symmetric => -(1i32 << (self.config.weight_bits - 1)),
188            QuantizationScheme::Asymmetric => 0i32,
189            _ => 0i32,
190        };
191
192        let qmax = match self.config.scheme {
193            QuantizationScheme::Symmetric => (1i32 << (self.config.weight_bits - 1)) - 1,
194            QuantizationScheme::Asymmetric => (1i32 << self.config.weight_bits) - 1,
195            _ => 255i32,
196        };
197
198        // Fake quantization: quantize then immediately dequantize
199        let data = input.to_vec()?;
200        let mut fake_quantized = Vec::with_capacity(data.len());
201
202        for &value in &data {
203            // Quantize
204            let quantized = match self.config.scheme {
205                QuantizationScheme::Symmetric => ((value / scale).round() as i32).clamp(qmin, qmax),
206                QuantizationScheme::Asymmetric => {
207                    (((value / scale).round() + zero_point) as i32).clamp(qmin, qmax)
208                }
209                _ => ((value / scale).round() as i32).clamp(qmin, qmax),
210            };
211
212            // Dequantize
213            let dequantized = match self.config.scheme {
214                QuantizationScheme::Symmetric => quantized as f32 * scale,
215                QuantizationScheme::Asymmetric => (quantized as f32 - zero_point) * scale,
216                _ => quantized as f32 * scale,
217            };
218
219            fake_quantized.push(dequantized);
220        }
221
222        Tensor::from_data(
223            fake_quantized,
224            input.shape().dims().to_vec(),
225            input.device(),
226        )
227    }
228}
229
230impl Module for FakeQuantize {
231    fn forward(&self, input: &Tensor) -> Result<Tensor> {
232        self.fake_quantize_tensor(input)
233    }
234
235    fn parameters(&self) -> HashMap<String, Parameter> {
236        self.base.parameters.clone()
237    }
238
239    fn named_parameters(&self) -> HashMap<String, Parameter> {
240        self.base.named_parameters()
241    }
242
243    fn training(&self) -> bool {
244        self.base.training()
245    }
246
247    fn train(&mut self) {
248        self.base.set_training(true);
249    }
250
251    fn eval(&mut self) {
252        self.base.set_training(false);
253    }
254
255    fn set_training(&mut self, training: bool) {
256        self.base.set_training(training);
257    }
258
259    fn to_device(&mut self, device: torsh_core::device::DeviceType) -> Result<()> {
260        self.base.to_device(device)
261    }
262}
263
264/// QAT-aware Linear layer with fake quantization for weights and activations
265#[derive(Debug)]
266pub struct QATLinear {
267    base: ModuleBase,
268    #[allow(dead_code)]
269    in_features: usize,
270    #[allow(dead_code)]
271    out_features: usize,
272    bias: bool,
273    weight_fake_quant: FakeQuantize,
274    activation_fake_quant: FakeQuantize,
275    #[allow(dead_code)]
276    config: QATConfig,
277}
278
279impl QATLinear {
280    /// Create a new QAT Linear layer
281    pub fn new(in_features: usize, out_features: usize, bias: bool, config: QATConfig) -> Self {
282        let mut base = ModuleBase::new();
283
284        // Initialize weights and bias
285        let weight = crate::init::xavier_uniform(&[out_features, in_features])
286            .expect("Failed to create weight tensor");
287        base.register_parameter("weight".to_string(), Parameter::new(weight));
288
289        if bias {
290            let bias_tensor = torsh_tensor::creation::zeros(&[out_features])
291                .expect("zeros tensor for bias should succeed");
292            base.register_parameter("bias".to_string(), Parameter::new(bias_tensor));
293        }
294
295        // Create fake quantizers for weights and activations
296        let weight_fake_quant = FakeQuantize::new(config.clone());
297        let activation_fake_quant = FakeQuantize::new(config.clone());
298
299        Self {
300            base,
301            in_features,
302            out_features,
303            bias,
304            weight_fake_quant,
305            activation_fake_quant,
306            config,
307        }
308    }
309
310    /// Enable/disable quantization
311    pub fn enable_quantization(&mut self, enabled: bool) {
312        self.weight_fake_quant.enable(enabled);
313        self.activation_fake_quant.enable(enabled);
314    }
315}
316
317impl Module for QATLinear {
318    fn forward(&self, input: &Tensor) -> Result<Tensor> {
319        // Apply fake quantization to activations if training
320        let mut quantized_input = input.clone();
321        if self.training() {
322            quantized_input = self.activation_fake_quant.fake_quantize_tensor(input)?;
323        }
324
325        // Get weight and apply fake quantization
326        let weight = self.base.parameters["weight"].tensor().read().clone();
327        let quantized_weight = if self.training() {
328            self.weight_fake_quant.fake_quantize_tensor(&weight)?
329        } else {
330            weight
331        };
332
333        // Perform linear transformation
334        let output = quantized_input.matmul(&quantized_weight.transpose(0, 1)?)?;
335
336        // Add bias if present
337        if self.bias {
338            let bias = self.base.parameters["bias"].tensor().read().clone();
339            output.add_op(&bias)
340        } else {
341            Ok(output)
342        }
343    }
344
345    fn parameters(&self) -> HashMap<String, Parameter> {
346        let mut params = self.base.parameters.clone();
347
348        // Add fake quantizer parameters if learnable
349        for (name, param) in self.weight_fake_quant.parameters() {
350            params.insert(format!("weight_fake_quant.{}", name), param);
351        }
352        for (name, param) in self.activation_fake_quant.parameters() {
353            params.insert(format!("activation_fake_quant.{}", name), param);
354        }
355
356        params
357    }
358
359    fn named_parameters(&self) -> HashMap<String, Parameter> {
360        self.parameters()
361    }
362
363    fn training(&self) -> bool {
364        self.base.training()
365    }
366
367    fn train(&mut self) {
368        self.base.set_training(true);
369        self.weight_fake_quant.train();
370        self.activation_fake_quant.train();
371    }
372
373    fn eval(&mut self) {
374        self.base.set_training(false);
375        self.weight_fake_quant.eval();
376        self.activation_fake_quant.eval();
377    }
378
379    fn set_training(&mut self, training: bool) {
380        self.base.set_training(training);
381        self.weight_fake_quant.set_training(training);
382        self.activation_fake_quant.set_training(training);
383    }
384
385    fn to_device(&mut self, device: torsh_core::device::DeviceType) -> Result<()> {
386        self.base.to_device(device)?;
387        self.weight_fake_quant.to_device(device)?;
388        self.activation_fake_quant.to_device(device)?;
389        Ok(())
390    }
391}
392
393/// QAT Training Scheduler for progressive quantization
394#[derive(Debug)]
395pub struct QATScheduler {
396    config: QATConfig,
397    current_epoch: usize,
398    enabled: bool,
399}
400
401impl QATScheduler {
402    /// Create a new QAT scheduler
403    pub fn new(config: QATConfig) -> Self {
404        Self {
405            config,
406            current_epoch: 0,
407            enabled: false,
408        }
409    }
410
411    /// Step the scheduler (call at the beginning of each epoch)
412    pub fn step(&mut self) {
413        self.current_epoch += 1;
414
415        // Enable quantization after warmup period
416        if self.current_epoch > self.config.warmup_epochs {
417            self.enabled = true;
418        }
419    }
420
421    /// Check if quantization should be enabled
422    pub fn is_quantization_enabled(&self) -> bool {
423        self.enabled
424    }
425
426    /// Get current epoch
427    pub fn current_epoch(&self) -> usize {
428        self.current_epoch
429    }
430
431    /// Get recommended learning rate scale for quantization parameters
432    pub fn qparam_lr_scale(&self) -> f32 {
433        if self.enabled {
434            // Gradually reduce quantization parameter learning rate
435            let decay_factor =
436                0.95_f32.powi((self.current_epoch - self.config.warmup_epochs) as i32);
437            decay_factor.max(0.1)
438        } else {
439            0.0
440        }
441    }
442}
443
444/// QAT Model wrapper for automatic quantization-aware training
445#[derive(Debug)]
446pub struct QATModel<M: Module> {
447    model: M,
448    config: QATConfig,
449    scheduler: QATScheduler,
450    fake_quantizers: HashMap<String, FakeQuantize>,
451}
452
453impl<M: Module> QATModel<M> {
454    /// Create a new QAT model wrapper
455    pub fn new(model: M, config: QATConfig) -> Self {
456        let scheduler = QATScheduler::new(config.clone());
457
458        Self {
459            model,
460            config: config.clone(),
461            scheduler,
462            fake_quantizers: HashMap::new(),
463        }
464    }
465
466    /// Step the QAT scheduler
467    pub fn step_scheduler(&mut self) {
468        self.scheduler.step();
469
470        // Enable/disable fake quantizers based on scheduler state
471        let enabled = self.scheduler.is_quantization_enabled();
472        for fake_quant in self.fake_quantizers.values_mut() {
473            fake_quant.enable(enabled);
474        }
475    }
476
477    /// Add a fake quantizer for a specific layer
478    pub fn add_fake_quantizer(&mut self, layer_name: String, fake_quant: FakeQuantize) {
479        self.fake_quantizers.insert(layer_name, fake_quant);
480    }
481
482    /// Get the current QAT scheduler
483    pub fn scheduler(&self) -> &QATScheduler {
484        &self.scheduler
485    }
486
487    /// Convert to fully quantized model (post-training)
488    pub fn to_quantized(self) -> Result<QuantizedInferenceModel<M>> {
489        let quantization_params = self.extract_quantization_params()?;
490        Ok(QuantizedInferenceModel {
491            model: self.model,
492            quantization_params,
493        })
494    }
495
496    /// Extract quantization parameters from fake quantizers
497    fn extract_quantization_params(&self) -> Result<HashMap<String, QuantizationParams>> {
498        let mut params = HashMap::new();
499
500        for (layer_name, fake_quant) in &self.fake_quantizers {
501            let scale = fake_quant.scale.tensor().read().to_vec()?[0];
502            let zero_point = fake_quant.zero_point.tensor().read().to_vec()?[0] as i32;
503
504            let qparams = match self.config.scheme {
505                QuantizationScheme::Symmetric => {
506                    QuantizationParams::symmetric(scale, DType::F32, DType::I8)
507                }
508                QuantizationScheme::Asymmetric => {
509                    QuantizationParams::asymmetric(scale, zero_point, DType::F32, DType::U8)
510                }
511                _ => {
512                    return Err(TorshError::InvalidArgument(
513                        "Unsupported quantization scheme".to_string(),
514                    ));
515                }
516            };
517
518            params.insert(layer_name.clone(), qparams);
519        }
520
521        Ok(params)
522    }
523}
524
525impl<M: Module> Module for QATModel<M> {
526    fn forward(&self, input: &Tensor) -> Result<Tensor> {
527        self.model.forward(input)
528    }
529
530    fn parameters(&self) -> HashMap<String, Parameter> {
531        let mut params = self.model.parameters();
532
533        // Add fake quantizer parameters
534        for (layer_name, fake_quant) in &self.fake_quantizers {
535            for (param_name, param) in fake_quant.parameters() {
536                params.insert(format!("{}.{}", layer_name, param_name), param);
537            }
538        }
539
540        params
541    }
542
543    fn named_parameters(&self) -> HashMap<String, Parameter> {
544        self.parameters()
545    }
546
547    fn training(&self) -> bool {
548        self.model.training()
549    }
550
551    fn train(&mut self) {
552        self.model.train();
553        for fake_quant in self.fake_quantizers.values_mut() {
554            fake_quant.train();
555        }
556    }
557
558    fn eval(&mut self) {
559        self.model.eval();
560        for fake_quant in self.fake_quantizers.values_mut() {
561            fake_quant.eval();
562        }
563    }
564
565    fn set_training(&mut self, training: bool) {
566        self.model.set_training(training);
567        for fake_quant in self.fake_quantizers.values_mut() {
568            fake_quant.set_training(training);
569        }
570    }
571
572    fn to_device(&mut self, device: torsh_core::device::DeviceType) -> Result<()> {
573        self.model.to_device(device)?;
574        for fake_quant in self.fake_quantizers.values_mut() {
575            fake_quant.to_device(device)?;
576        }
577        Ok(())
578    }
579}
580
581/// Fully quantized model for inference
582#[derive(Debug)]
583pub struct QuantizedInferenceModel<M: Module> {
584    model: M,
585    quantization_params: HashMap<String, QuantizationParams>,
586}
587
588impl<M: Module> QuantizedInferenceModel<M> {
589    /// Get quantization parameters for a layer
590    pub fn get_quantization_params(&self, layer_name: &str) -> Option<&QuantizationParams> {
591        self.quantization_params.get(layer_name)
592    }
593
594    /// Get model size reduction ratio
595    pub fn compression_ratio(&self) -> f32 {
596        // Estimate compression based on 8-bit quantization
597        32.0 / 8.0 // F32 to INT8
598    }
599}
600
601impl<M: Module> Module for QuantizedInferenceModel<M> {
602    fn forward(&self, input: &Tensor) -> Result<Tensor> {
603        // In a full implementation, this would use actual quantized operations
604        self.model.forward(input)
605    }
606
607    fn parameters(&self) -> HashMap<String, Parameter> {
608        self.model.parameters()
609    }
610
611    fn named_parameters(&self) -> HashMap<String, Parameter> {
612        self.model.named_parameters()
613    }
614
615    fn training(&self) -> bool {
616        false // Quantized models are for inference only
617    }
618
619    fn train(&mut self) {
620        // No-op: quantized models should not be trained
621    }
622
623    fn eval(&mut self) {
624        self.model.eval();
625    }
626
627    fn set_training(&mut self, training: bool) {
628        self.model.set_training(training);
629    }
630
631    fn to_device(&mut self, device: torsh_core::device::DeviceType) -> Result<()> {
632        self.model.to_device(device)
633    }
634}
635
636/// Utilities for QAT
637pub mod utils {
638    use super::*;
639
640    /// Convert a regular model to QAT model by wrapping layers
641    pub fn prepare_qat_model<M: Module>(model: M, config: QATConfig) -> QATModel<M> {
642        QATModel::new(model, config)
643    }
644
645    /// Calibrate quantization parameters using sample data
646    pub fn calibrate_qat_model<M: Module, I>(
647        model: &mut QATModel<M>,
648        calibration_data: I,
649    ) -> Result<()>
650    where
651        I: Iterator<Item = Tensor>,
652    {
653        model.eval();
654
655        for input in calibration_data {
656            let _output = model.forward(&input)?;
657
658            // Update observers in fake quantizers
659            for fake_quant in model.fake_quantizers.values_mut() {
660                fake_quant.update_observers(&input)?;
661            }
662        }
663
664        Ok(())
665    }
666
667    /// Progressive quantization training helper
668    pub fn progressive_qat_training<M: Module, F, L>(
669        model: &mut QATModel<M>,
670        mut train_step: F,
671        epochs: usize,
672    ) -> Result<()>
673    where
674        F: FnMut(&mut QATModel<M>) -> Result<L>,
675        L: std::fmt::Debug,
676    {
677        for epoch in 0..epochs {
678            println!("QAT Epoch {}/{}", epoch + 1, epochs);
679
680            // Step the scheduler
681            model.step_scheduler();
682
683            // Training step
684            let _loss = train_step(model)?;
685
686            // Log quantization status
687            if model.scheduler().is_quantization_enabled() {
688                println!("Quantization enabled (epoch {})", epoch + 1);
689            } else {
690                println!("Warmup phase (epoch {})", epoch + 1);
691            }
692        }
693
694        Ok(())
695    }
696
697    /// Automatic model conversion to QAT-aware layers
698    pub fn convert_model_to_qat<M: Module>(model: M, config: QATConfig) -> QATModel<M> {
699        // In a full implementation, this would traverse the model and replace
700        // regular layers (Linear, Conv2d) with their QAT equivalents
701        // For now, we just wrap the model
702        QATModel::new(model, config)
703    }
704
705    /// QAT training loop with automatic quantization scheduling
706    pub fn qat_training_loop<M, F, L, O, D>(
707        model: &mut QATModel<M>,
708        train_data_fn: D,
709        loss_fn: F,
710        _optimizer: &mut O,
711        epochs: usize,
712    ) -> Result<Vec<f32>>
713    where
714        M: Module,
715        F: Fn(&Tensor, &Tensor) -> Result<L>,
716        L: std::fmt::Debug,
717        O: std::fmt::Debug,
718        D: Fn() -> Box<dyn Iterator<Item = (Tensor, Tensor)>>,
719    {
720        let mut losses = Vec::new();
721
722        for epoch in 0..epochs {
723            model.step_scheduler();
724
725            let mut epoch_loss = 0.0;
726            let mut num_batches = 0;
727
728            for (inputs, targets) in train_data_fn() {
729                // Forward pass
730                let outputs = model.forward(&inputs)?;
731                let _loss = loss_fn(&outputs, &targets)?;
732
733                // In a real implementation, loss would be backpropagated
734                // and optimizer.step() would be called
735
736                // Simulate loss accumulation
737                epoch_loss += 0.1; // Placeholder loss value
738                num_batches += 1;
739            }
740
741            let avg_loss = if num_batches > 0 {
742                epoch_loss / num_batches as f32
743            } else {
744                0.0
745            };
746            losses.push(avg_loss);
747
748            // Update fake quantizer observers during training
749            if model.scheduler().is_quantization_enabled() {
750                for fake_quant in model.fake_quantizers.values_mut() {
751                    fake_quant.update_observers(&torsh_tensor::creation::zeros(&[1, 32])?)?;
752                }
753            }
754
755            println!(
756                "Epoch {}/{}: Loss = {:.4}, QAT = {}",
757                epoch + 1,
758                epochs,
759                avg_loss,
760                model.scheduler().is_quantization_enabled()
761            );
762        }
763
764        Ok(losses)
765    }
766
767    /// Evaluate quantization quality after QAT training
768    pub fn evaluate_qat_quality<M: Module>(
769        qat_model: &QATModel<M>,
770        test_data: impl Iterator<Item = (Tensor, Tensor)>,
771    ) -> Result<QATEvaluationMetrics> {
772        let mut correct_predictions = 0;
773        let mut total_predictions = 0;
774        let mut total_loss = 0.0;
775
776        for (inputs, _targets) in test_data {
777            let _outputs = qat_model.forward(&inputs)?;
778
779            // Simulate evaluation metrics
780            correct_predictions += 1;
781            total_predictions += 1;
782            total_loss += 0.05; // Placeholder
783        }
784
785        let accuracy = if total_predictions > 0 {
786            correct_predictions as f32 / total_predictions as f32
787        } else {
788            0.0
789        };
790
791        let avg_loss = if total_predictions > 0 {
792            total_loss / total_predictions as f32
793        } else {
794            0.0
795        };
796
797        Ok(QATEvaluationMetrics {
798            accuracy,
799            average_loss: avg_loss,
800            inference_speedup: 2.0,    // Estimated speedup
801            model_size_reduction: 4.0, // INT8 vs FP32
802        })
803    }
804}
805
806/// QAT evaluation metrics
807#[derive(Debug, Clone)]
808pub struct QATEvaluationMetrics {
809    /// Model accuracy on test set
810    pub accuracy: f32,
811    /// Average loss on test set
812    pub average_loss: f32,
813    /// Inference speedup compared to FP32
814    pub inference_speedup: f32,
815    /// Model size reduction factor
816    pub model_size_reduction: f32,
817}
818
819#[cfg(test)]
820mod tests {
821    use super::*;
822    use torsh_tensor::creation::*;
823
824    #[test]
825    fn test_fake_quantize_creation() {
826        let config = QATConfig::default();
827        let fake_quant = FakeQuantize::new(config);
828        assert!(fake_quant.enabled);
829    }
830
831    #[test]
832    fn test_fake_quantization() -> Result<()> {
833        let config = QATConfig::default();
834        let fake_quant = FakeQuantize::new(config);
835
836        let input = tensor_1d(&[1.0, 2.0, 3.0, 4.0])?;
837        let output = fake_quant.fake_quantize_tensor(&input)?;
838
839        // Output should be close to input (fake quantization preserves values approximately)
840        let input_data = input.to_vec()?;
841        let output_data = output.to_vec()?;
842
843        for (orig, quantized) in input_data.iter().zip(output_data.iter()) {
844            assert!(
845                (orig - quantized).abs() < 1.0,
846                "Fake quantization should preserve values approximately"
847            );
848        }
849
850        Ok(())
851    }
852
853    #[test]
854    fn test_qat_linear() -> Result<()> {
855        let config = QATConfig::default();
856        let linear = QATLinear::new(4, 2, true, config);
857
858        let input = ones(&[1, 4])?;
859        let output = linear.forward(&input)?;
860
861        assert_eq!(output.shape().dims(), &[1, 2]);
862
863        Ok(())
864    }
865
866    #[test]
867    fn test_qat_scheduler() {
868        let config = QATConfig {
869            warmup_epochs: 2,
870            ..Default::default()
871        };
872        let mut scheduler = QATScheduler::new(config);
873
874        assert!(!scheduler.is_quantization_enabled());
875
876        scheduler.step();
877        assert!(!scheduler.is_quantization_enabled());
878
879        scheduler.step();
880        assert!(!scheduler.is_quantization_enabled());
881
882        scheduler.step(); // After warmup
883        assert!(scheduler.is_quantization_enabled());
884    }
885
886    #[test]
887    fn test_qat_model_wrapper() -> Result<()> {
888        use crate::layers::linear::Linear;
889
890        let linear = Linear::new(4, 2, true);
891        let config = QATConfig::default();
892        let qat_model = QATModel::new(linear, config);
893
894        let input = ones(&[1, 4])?;
895        let output = qat_model.forward(&input)?;
896
897        assert_eq!(output.shape().dims(), &[1, 2]);
898
899        Ok(())
900    }
901
902    #[test]
903    fn test_qat_evaluation_metrics() -> Result<()> {
904        use crate::layers::linear::Linear;
905
906        let linear = Linear::new(4, 2, true);
907        let config = QATConfig::default();
908        let qat_model = QATModel::new(linear, config);
909
910        // Create test data
911        let test_data = vec![
912            (ones(&[1, 4])?, ones(&[1, 2])?),
913            (ones(&[1, 4])?, ones(&[1, 2])?),
914        ];
915
916        let metrics = utils::evaluate_qat_quality(&qat_model, test_data.into_iter())?;
917
918        assert!(metrics.accuracy >= 0.0 && metrics.accuracy <= 1.0);
919        assert!(metrics.inference_speedup > 1.0);
920        assert!(metrics.model_size_reduction > 1.0);
921
922        Ok(())
923    }
924
925    #[test]
926    fn test_convert_model_to_qat() -> Result<()> {
927        use crate::layers::linear::Linear;
928
929        let linear = Linear::new(8, 4, true);
930        let config = QATConfig::default();
931        let qat_model = utils::convert_model_to_qat(linear, config);
932
933        let input = ones(&[2, 8])?;
934        let output = qat_model.forward(&input)?;
935
936        assert_eq!(output.shape().dims(), &[2, 4]);
937
938        Ok(())
939    }
940
941    #[test]
942    fn test_fake_quantize_observer_updates() -> Result<()> {
943        let config = QATConfig::default();
944        let mut fake_quant = FakeQuantize::new(config);
945
946        // Set to training mode
947        fake_quant.train();
948
949        let test_data = vec![
950            tensor_1d(&[1.0, 2.0, 3.0])?,
951            tensor_1d(&[-1.0, 0.0, 1.0])?,
952            tensor_1d(&[0.5, 1.5, 2.5])?,
953        ];
954
955        for tensor in test_data {
956            fake_quant.update_observers(&tensor)?;
957        }
958
959        assert!(fake_quant.num_batches_tracked > 0);
960        assert!(fake_quant.min_val < fake_quant.max_val);
961
962        Ok(())
963    }
964
965    #[test]
966    fn test_qat_linear_training_mode() -> Result<()> {
967        let config = QATConfig::default();
968        let mut qat_linear = QATLinear::new(4, 2, true, config);
969
970        // Test training mode
971        qat_linear.train();
972        assert!(qat_linear.training());
973
974        // Test eval mode
975        qat_linear.eval();
976        assert!(!qat_linear.training());
977
978        // Test quantization enable/disable
979        qat_linear.enable_quantization(false);
980        let input = ones(&[1, 4])?;
981        let output = qat_linear.forward(&input)?;
982        assert_eq!(output.shape().dims(), &[1, 2]);
983
984        Ok(())
985    }
986}