1use crate::quantization::{CalibrationMethod, QuantizationParams, QuantizationScheme};
4use torsh_core::{
5 dtype::DType,
6 error::{Result, TorshError},
7};
8use torsh_tensor::Tensor;
9
10pub struct PostTrainingQuantization {
12 scheme: QuantizationScheme,
13 target_dtype: DType,
14 calibration_method: CalibrationMethod,
15}
16
17impl PostTrainingQuantization {
18 pub fn new(
20 scheme: QuantizationScheme,
21 target_dtype: DType,
22 calibration_method: CalibrationMethod,
23 ) -> Self {
24 Self {
25 scheme,
26 target_dtype,
27 calibration_method,
28 }
29 }
30
31 pub fn quantize_tensor(&self, tensor: &Tensor) -> Result<(Tensor, QuantizationParams)> {
33 let data = tensor.to_vec()?;
34 let params = self.calculate_params(&data)?;
35 let quantized = params.quantize(tensor)?;
36 Ok((quantized, params))
37 }
38
39 fn calculate_params(&self, data: &[f32]) -> Result<QuantizationParams> {
41 let min_val = data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
42 let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
43
44 match self.scheme {
45 QuantizationScheme::Symmetric => {
46 let scale = self.calculate_symmetric_scale(min_val, max_val)?;
47 Ok(QuantizationParams::symmetric(
48 scale,
49 DType::F32,
50 self.target_dtype,
51 ))
52 }
53 QuantizationScheme::Asymmetric => {
54 let (scale, zero_point) = self.calculate_asymmetric_params(min_val, max_val)?;
55 Ok(QuantizationParams::asymmetric(
56 scale,
57 zero_point,
58 DType::F32,
59 self.target_dtype,
60 ))
61 }
62 QuantizationScheme::Dynamic => {
63 self.dynamic_quantization_params(data)
65 }
66 QuantizationScheme::KLDivergence => self.kl_divergence_params(data),
67 QuantizationScheme::Percentile(percentile) => self.percentile_params(data, percentile),
68 }
69 }
70
71 fn calculate_symmetric_scale(&self, min_val: f32, max_val: f32) -> Result<f32> {
73 let max_abs = max_val.abs().max(min_val.abs());
74 let max_quant = match self.target_dtype {
75 DType::I8 => 127.0,
76 DType::U8 => 127.0, DType::I16 => 32767.0,
78 _ => {
79 return Err(TorshError::InvalidArgument(
80 "Unsupported quantization dtype".to_string(),
81 ))
82 }
83 };
84
85 if max_abs == 0.0 {
86 return Ok(1.0); }
88
89 Ok(max_abs / max_quant)
90 }
91
92 fn calculate_asymmetric_params(&self, min_val: f32, max_val: f32) -> Result<(f32, i32)> {
94 let (qmin, qmax) = match self.target_dtype {
95 DType::U8 => (0.0, 255.0),
96 DType::I8 => (-128.0, 127.0),
97 DType::I16 => (-32768.0, 32767.0),
98 _ => {
99 return Err(TorshError::InvalidArgument(
100 "Unsupported quantization dtype".to_string(),
101 ))
102 }
103 };
104
105 let scale = (max_val - min_val) / (qmax - qmin);
106 let zero_point = (qmin - min_val / scale).round() as i32;
107
108 Ok((scale, zero_point))
109 }
110
111 fn dynamic_quantization_params(&self, data: &[f32]) -> Result<QuantizationParams> {
113 use crate::quantization::calibration::calculate_optimal_scale;
116
117 let scale = calculate_optimal_scale(data, &self.calibration_method, self.target_dtype)?;
118 Ok(QuantizationParams::symmetric(
119 scale,
120 DType::F32,
121 self.target_dtype,
122 ))
123 }
124
125 fn kl_divergence_params(&self, data: &[f32]) -> Result<QuantizationParams> {
127 let scale = self.find_optimal_scale_kl(data)?;
128 Ok(QuantizationParams::symmetric(
129 scale,
130 DType::F32,
131 self.target_dtype,
132 ))
133 }
134
135 fn percentile_params(&self, data: &[f32], percentile: f32) -> Result<QuantizationParams> {
137 let mut sorted_abs: Vec<f32> = data.iter().map(|&x| x.abs()).collect();
138 sorted_abs.sort_by(|a, b| {
139 a.partial_cmp(b)
140 .expect("data comparison should not involve NaN")
141 });
142
143 let index = ((percentile / 100.0) * sorted_abs.len() as f32) as usize;
144 let max_val = sorted_abs
145 .get(index.min(sorted_abs.len() - 1))
146 .copied()
147 .unwrap_or(0.0);
148
149 let scale = self.calculate_symmetric_scale(-max_val, max_val)?;
150 Ok(QuantizationParams::symmetric(
151 scale,
152 DType::F32,
153 self.target_dtype,
154 ))
155 }
156
157 fn find_optimal_scale_kl(&self, data: &[f32]) -> Result<f32> {
159 let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b.abs()));
160 let max_quant = match self.target_dtype {
161 DType::I8 => 127.0,
162 DType::U8 => 255.0,
163 _ => {
164 return Err(TorshError::InvalidArgument(
165 "Unsupported dtype for KL divergence".to_string(),
166 ))
167 }
168 };
169
170 let base_scale = max_val / max_quant;
171 let mut best_scale = base_scale;
172 let mut best_divergence = f32::INFINITY;
173
174 for multiplier in [0.5, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0] {
176 let scale = base_scale * multiplier;
177 let divergence = self.calculate_kl_divergence(data, scale)?;
178
179 if divergence < best_divergence {
180 best_divergence = divergence;
181 best_scale = scale;
182 }
183 }
184
185 Ok(best_scale)
186 }
187
188 fn calculate_kl_divergence(&self, data: &[f32], scale: f32) -> Result<f32> {
190 let max_quant = match self.target_dtype {
191 DType::I8 => 127,
192 DType::U8 => 255,
193 _ => return Err(TorshError::InvalidArgument("Unsupported dtype".to_string())),
194 };
195
196 let num_bins = 256;
198 let min_val = data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
199 let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
200 let bin_width = (max_val - min_val) / num_bins as f32;
201
202 let mut original_hist = vec![0u32; num_bins];
203 let mut quantized_hist = vec![0u32; num_bins];
204
205 for &value in data {
206 let bin_idx = ((value - min_val) / bin_width) as usize;
208 let bin_idx = bin_idx.min(num_bins - 1);
209 original_hist[bin_idx] += 1;
210
211 let quantized = ((value / scale).round() as i32).clamp(-max_quant, max_quant);
213 let dequantized = quantized as f32 * scale;
214 let quant_bin_idx = ((dequantized - min_val) / bin_width) as usize;
215 let quant_bin_idx = quant_bin_idx.min(num_bins - 1);
216 quantized_hist[quant_bin_idx] += 1;
217 }
218
219 let total_count = data.len() as f32;
221 let mut kl_div = 0.0;
222
223 for i in 0..num_bins {
224 let p = original_hist[i] as f32 / total_count;
225 let q = quantized_hist[i] as f32 / total_count;
226
227 if p > 0.0 && q > 0.0 {
228 kl_div += p * (p / q).ln();
229 }
230 }
231
232 Ok(kl_div)
233 }
234}
235
236pub struct QuantizationAwareTraining {
238 fake_quantize: bool,
239 #[allow(dead_code)]
240 scheme: QuantizationScheme,
241 target_dtype: DType,
242}
243
244impl QuantizationAwareTraining {
245 pub fn new(scheme: QuantizationScheme, target_dtype: DType) -> Self {
247 Self {
248 fake_quantize: true,
249 scheme,
250 target_dtype,
251 }
252 }
253
254 pub fn fake_quantize_tensor(
256 &self,
257 tensor: &Tensor,
258 params: &QuantizationParams,
259 ) -> Result<Tensor> {
260 if !self.fake_quantize {
261 return Ok(tensor.clone());
262 }
263
264 let quantized = params.quantize(tensor)?;
266 let dequantized = params.dequantize(&quantized)?;
267
268 Ok(dequantized)
269 }
270
271 pub fn set_fake_quantize(&mut self, enabled: bool) {
273 self.fake_quantize = enabled;
274 }
275
276 pub fn learnable_params(
278 &self,
279 initial_scale: f32,
280 initial_zero_point: i32,
281 ) -> LearnableQuantParams {
282 LearnableQuantParams::new(initial_scale, initial_zero_point, self.target_dtype)
283 }
284}
285
286#[derive(Debug, Clone)]
288pub struct LearnableQuantParams {
289 scale: Tensor,
290 zero_point: Tensor,
291 target_dtype: DType,
292}
293
294impl LearnableQuantParams {
295 pub fn new(initial_scale: f32, initial_zero_point: i32, target_dtype: DType) -> Self {
297 let scale = Tensor::from_data(
298 vec![initial_scale],
299 vec![1],
300 torsh_core::device::DeviceType::Cpu,
301 )
302 .unwrap_or_else(|_| {
303 Tensor::zeros(&[1], torsh_core::device::DeviceType::Cpu)
304 .expect("Failed to create fallback zeros tensor")
305 });
306 let zero_point = Tensor::from_data(
307 vec![initial_zero_point as f32],
308 vec![1],
309 torsh_core::device::DeviceType::Cpu,
310 )
311 .unwrap_or_else(|_| {
312 Tensor::zeros(&[1], torsh_core::device::DeviceType::Cpu)
313 .expect("Failed to create fallback zeros tensor")
314 });
315
316 Self {
317 scale,
318 zero_point,
319 target_dtype,
320 }
321 }
322
323 pub fn current_params(&self) -> Result<QuantizationParams> {
325 let scale_val = self.scale.to_vec()?[0];
326 let zero_point_val = self.zero_point.to_vec()?[0] as i32;
327
328 Ok(QuantizationParams::asymmetric(
329 scale_val,
330 zero_point_val,
331 DType::F32,
332 self.target_dtype,
333 ))
334 }
335
336 pub fn update_scale(&mut self, new_scale: f32) -> Result<()> {
338 self.scale = Tensor::from_data(
339 vec![new_scale],
340 vec![1],
341 torsh_core::device::DeviceType::Cpu,
342 )?;
343 Ok(())
344 }
345
346 pub fn update_zero_point(&mut self, new_zero_point: i32) -> Result<()> {
348 self.zero_point = Tensor::from_data(
349 vec![new_zero_point as f32],
350 vec![1],
351 torsh_core::device::DeviceType::Cpu,
352 )?;
353 Ok(())
354 }
355}
356
357pub struct BlockWiseQuantization {
359 block_size: usize,
360 scheme: QuantizationScheme,
361 target_dtype: DType,
362}
363
364impl BlockWiseQuantization {
365 pub fn new(block_size: usize, scheme: QuantizationScheme, target_dtype: DType) -> Self {
367 Self {
368 block_size,
369 scheme,
370 target_dtype,
371 }
372 }
373
374 pub fn quantize_blocks(&self, tensor: &Tensor) -> Result<(Tensor, Vec<QuantizationParams>)> {
376 let data = tensor.to_vec()?;
377 let mut quantized_data = Vec::new();
378 let mut block_params = Vec::new();
379
380 for chunk in data.chunks(self.block_size) {
381 let ptq = PostTrainingQuantization::new(
382 self.scheme.clone(),
383 self.target_dtype,
384 CalibrationMethod::MinMax,
385 );
386
387 let block_tensor = Tensor::from_data(
388 chunk.to_vec(),
389 vec![chunk.len()],
390 torsh_core::device::DeviceType::Cpu,
391 )?;
392 let (quantized_block, params) = ptq.quantize_tensor(&block_tensor)?;
393
394 let block_data: Vec<u8> = match self.target_dtype {
395 DType::I8 => quantized_block
396 .to_vec()?
397 .into_iter()
398 .map(|x: f32| x as i8 as u8)
399 .collect(),
400 DType::U8 => quantized_block
401 .to_vec()?
402 .into_iter()
403 .map(|x: f32| x as u8)
404 .collect(),
405 _ => {
406 return Err(TorshError::InvalidArgument(
407 "Unsupported block quantization dtype".to_string(),
408 ))
409 }
410 };
411
412 quantized_data.extend(block_data);
413 block_params.push(params);
414 }
415
416 let float_data: Vec<f32> = quantized_data.into_iter().map(|x| x as f32).collect();
417 let quantized_tensor = Tensor::from_data(
418 float_data,
419 tensor.shape().dims().to_vec(),
420 torsh_core::device::DeviceType::Cpu,
421 )?;
422 Ok((quantized_tensor, block_params))
423 }
424}
425
426pub struct MixedPrecisionQuantization {
428 layer_configs: Vec<LayerQuantConfig>,
429}
430
431#[derive(Debug, Clone)]
433pub struct LayerQuantConfig {
434 pub layer_name: String,
435 pub weight_dtype: DType,
436 pub activation_dtype: DType,
437 pub scheme: QuantizationScheme,
438 pub per_channel: bool,
439}
440
441impl MixedPrecisionQuantization {
442 pub fn new(layer_configs: Vec<LayerQuantConfig>) -> Self {
444 Self { layer_configs }
445 }
446
447 pub fn get_layer_config(&self, layer_name: &str) -> Option<&LayerQuantConfig> {
449 self.layer_configs
450 .iter()
451 .find(|config| config.layer_name == layer_name)
452 }
453
454 pub fn quantize_layer(
456 &self,
457 layer_name: &str,
458 weights: &Tensor,
459 activations: &Tensor,
460 ) -> Result<(Tensor, Tensor, QuantizationParams, QuantizationParams)> {
461 let config = self.get_layer_config(layer_name).ok_or_else(|| {
462 TorshError::InvalidArgument(format!(
463 "No quantization config found for layer: {}",
464 layer_name
465 ))
466 })?;
467
468 let weight_ptq = PostTrainingQuantization::new(
470 config.scheme.clone(),
471 config.weight_dtype,
472 CalibrationMethod::MinMax,
473 );
474 let (quantized_weights, weight_params) = weight_ptq.quantize_tensor(weights)?;
475
476 let activation_ptq = PostTrainingQuantization::new(
478 config.scheme.clone(),
479 config.activation_dtype,
480 CalibrationMethod::MinMax,
481 );
482 let (quantized_activations, activation_params) =
483 activation_ptq.quantize_tensor(activations)?;
484
485 Ok((
486 quantized_weights,
487 quantized_activations,
488 weight_params,
489 activation_params,
490 ))
491 }
492}
493
494pub mod utils {
496 use super::*;
497
498 pub fn int8_symmetric() -> PostTrainingQuantization {
500 PostTrainingQuantization::new(
501 QuantizationScheme::Symmetric,
502 DType::I8,
503 CalibrationMethod::MinMax,
504 )
505 }
506
507 pub fn uint8_asymmetric() -> PostTrainingQuantization {
509 PostTrainingQuantization::new(
510 QuantizationScheme::Asymmetric,
511 DType::U8,
512 CalibrationMethod::MinMax,
513 )
514 }
515
516 pub fn dynamic_int8() -> PostTrainingQuantization {
518 PostTrainingQuantization::new(
519 QuantizationScheme::Dynamic,
520 DType::I8,
521 CalibrationMethod::Entropy,
522 )
523 }
524
525 pub fn kl_divergence_int8() -> PostTrainingQuantization {
527 PostTrainingQuantization::new(
528 QuantizationScheme::KLDivergence,
529 DType::I8,
530 CalibrationMethod::Entropy,
531 )
532 }
533
534 pub fn percentile_int8(percentile: f32) -> PostTrainingQuantization {
536 PostTrainingQuantization::new(
537 QuantizationScheme::Percentile(percentile),
538 DType::I8,
539 CalibrationMethod::MinMax,
540 )
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 #[test]
549 fn test_post_training_quantization() {
550 let data = vec![1.0, -1.0, 0.5, -0.5, 2.0, -2.0];
551 let tensor = Tensor::from_data(data, vec![6], torsh_core::device::DeviceType::Cpu)
552 .expect("Tensor should succeed");
553
554 let ptq = PostTrainingQuantization::new(
555 QuantizationScheme::Symmetric,
556 DType::I8,
557 CalibrationMethod::MinMax,
558 );
559
560 let result = ptq.quantize_tensor(&tensor);
561 assert!(result.is_ok());
562
563 let (_quantized, params) = result.expect("operation should succeed");
564 assert!(params.scale > 0.0);
566 }
567
568 #[test]
569 fn test_qat_fake_quantization() {
570 let tensor_data = vec![0.1f32; 10];
571 let tensor = Tensor::from_data(tensor_data, vec![10], torsh_core::device::DeviceType::Cpu)
572 .expect("Tensor should succeed");
573 let params = QuantizationParams::symmetric(0.1, DType::F32, DType::I8);
574
575 let qat = QuantizationAwareTraining::new(QuantizationScheme::Symmetric, DType::I8);
576 let fake_quantized = qat
577 .fake_quantize_tensor(&tensor, ¶ms)
578 .expect("fake quantization should succeed");
579
580 assert_eq!(fake_quantized.shape().dims(), tensor.shape().dims());
581 }
583
584 #[test]
585 fn test_block_wise_quantization() {
586 let data: Vec<f32> = (0..100).map(|x| x as f32 / 10.0).collect();
587 let tensor = Tensor::from_data(data, vec![100], torsh_core::device::DeviceType::Cpu)
588 .expect("Tensor should succeed");
589
590 let block_quant = BlockWiseQuantization::new(25, QuantizationScheme::Symmetric, DType::I8);
591
592 let result = block_quant.quantize_blocks(&tensor);
593 assert!(result.is_ok());
594
595 let (quantized, params) = result.expect("operation should succeed");
596 assert_eq!(quantized.shape().dims(), tensor.shape().dims());
597 assert_eq!(params.len(), 4); }
599
600 #[test]
601 fn test_mixed_precision_quantization() {
602 let layer_configs = vec![
603 LayerQuantConfig {
604 layer_name: "conv1".to_string(),
605 weight_dtype: DType::I8,
606 activation_dtype: DType::I8,
607 scheme: QuantizationScheme::Symmetric,
608 per_channel: true,
609 },
610 LayerQuantConfig {
611 layer_name: "fc1".to_string(),
612 weight_dtype: DType::I8,
613 activation_dtype: DType::U8,
614 scheme: QuantizationScheme::Asymmetric,
615 per_channel: false,
616 },
617 ];
618
619 let mixed_prec = MixedPrecisionQuantization::new(layer_configs);
620 let config = mixed_prec.get_layer_config("conv1");
621 assert!(config.is_some());
622 assert_eq!(
623 config.expect("operation should succeed").weight_dtype,
624 DType::I8
625 );
626 }
627}