1use crate::quantization::{
4 BackendQuantConfig, CalibrationConfig, CalibrationMethod, DeploymentPlatform,
5 QuantizationConfig, QuantizationParams, QuantizationScheme,
6};
7use torsh_core::{
8 dtype::DType,
9 error::{Result, TorshError},
10};
11use torsh_tensor::Tensor;
12
13#[cfg(feature = "std")]
15use std::collections::{BTreeMap, HashMap};
16
17#[cfg(not(feature = "std"))]
18use alloc::collections::BTreeMap;
19#[cfg(not(feature = "std"))]
20use hashbrown::HashMap;
21
22pub fn calculate_quantization_error(
24 original: &Tensor,
25 quantized: &Tensor,
26 params: &QuantizationParams,
27) -> Result<QuantizationError> {
28 let dequantized = params.dequantize(quantized)?;
30
31 let orig_data = original.to_vec()?;
32 let deq_data = dequantized.to_vec()?;
33
34 if orig_data.len() != deq_data.len() {
35 return Err(TorshError::InvalidArgument(
36 "Tensor sizes do not match for error calculation".to_string(),
37 ));
38 }
39
40 let mut mse = 0.0;
41 let mut mae = 0.0;
42 let mut max_error: f32 = 0.0;
43 let mut dot_product = 0.0;
44 let mut norm_orig = 0.0;
45 let mut norm_deq = 0.0;
46
47 for (&orig, &deq) in orig_data.iter().zip(deq_data.iter()) {
48 let error = orig - deq;
49 let abs_error = error.abs();
50
51 mse += error * error;
52 mae += abs_error;
53 max_error = max_error.max(abs_error);
54
55 dot_product += orig * deq;
56 norm_orig += orig * orig;
57 norm_deq += deq * deq;
58 }
59
60 let n = orig_data.len() as f32;
61 mse /= n;
62 mae /= n;
63
64 let cosine_similarity = if norm_orig > 0.0 && norm_deq > 0.0 {
65 dot_product / (norm_orig.sqrt() * norm_deq.sqrt())
66 } else {
67 0.0
68 };
69
70 let snr = if mse > 0.0 {
71 let signal_power = norm_orig / n;
72 10.0 * (signal_power / mse).log10()
73 } else {
74 f32::INFINITY
75 };
76
77 Ok(QuantizationError {
78 mse,
79 mae,
80 max_error,
81 cosine_similarity,
82 snr,
83 })
84}
85
86#[derive(Debug, Clone)]
88pub struct QuantizationError {
89 pub mse: f32,
91 pub mae: f32,
93 pub max_error: f32,
95 pub cosine_similarity: f32,
97 pub snr: f32,
99}
100
101impl QuantizationError {
102 pub fn is_acceptable(&self, max_mse: f32, min_cosine: f32, min_snr: f32) -> bool {
104 self.mse <= max_mse && self.cosine_similarity >= min_cosine && self.snr >= min_snr
105 }
106}
107
108pub fn create_deployment_config(platform: DeploymentPlatform) -> QuantizationConfig {
110 match platform {
111 DeploymentPlatform::Mobile => {
112 QuantizationConfig {
113 dtype: DType::U8,
114 scheme: QuantizationScheme::Asymmetric,
115 backend_config: BackendQuantConfig {
116 use_hardware_acceleration: true,
117 enable_kernel_fusion: true,
118 optimize_memory_layout: true,
119 target_platform: DeploymentPlatform::Mobile,
120 },
121 calibration: CalibrationConfig {
122 num_samples: 50, method: CalibrationMethod::MinMax,
124 outlier_percentile: 99.9,
125 use_moving_average: true,
126 momentum: 0.9,
127 },
128 per_channel: true,
129 quantize_weights: true,
130 quantize_activations: true,
131 }
132 }
133 DeploymentPlatform::Edge => {
134 QuantizationConfig {
135 dtype: DType::I8,
136 scheme: QuantizationScheme::Symmetric,
137 backend_config: BackendQuantConfig {
138 use_hardware_acceleration: false, enable_kernel_fusion: false,
140 optimize_memory_layout: true,
141 target_platform: DeploymentPlatform::Edge,
142 },
143 calibration: CalibrationConfig {
144 num_samples: 25, method: CalibrationMethod::MinMax,
146 outlier_percentile: 99.5,
147 use_moving_average: false,
148 momentum: 0.9,
149 },
150 per_channel: false, quantize_weights: true,
152 quantize_activations: false, }
154 }
155 DeploymentPlatform::Server => {
156 QuantizationConfig {
157 dtype: DType::I8,
158 scheme: QuantizationScheme::KLDivergence,
159 backend_config: BackendQuantConfig {
160 use_hardware_acceleration: true,
161 enable_kernel_fusion: true,
162 optimize_memory_layout: true,
163 target_platform: DeploymentPlatform::Server,
164 },
165 calibration: CalibrationConfig {
166 num_samples: 500, method: CalibrationMethod::Entropy,
168 outlier_percentile: 99.99,
169 use_moving_average: true,
170 momentum: 0.95,
171 },
172 per_channel: true,
173 quantize_weights: true,
174 quantize_activations: true,
175 }
176 }
177 DeploymentPlatform::WASM => {
178 QuantizationConfig {
179 dtype: DType::U8,
180 scheme: QuantizationScheme::Asymmetric,
181 backend_config: BackendQuantConfig {
182 use_hardware_acceleration: false, enable_kernel_fusion: false,
184 optimize_memory_layout: true,
185 target_platform: DeploymentPlatform::WASM,
186 },
187 calibration: CalibrationConfig {
188 num_samples: 100,
189 method: CalibrationMethod::MinMax,
190 outlier_percentile: 99.9,
191 use_moving_average: true,
192 momentum: 0.9,
193 },
194 per_channel: false, quantize_weights: true,
196 quantize_activations: true,
197 }
198 }
199 _ => QuantizationConfig::default(),
200 }
201}
202
203pub fn analyze_tensor_distribution(tensor: &Tensor) -> Result<TensorDistributionStats> {
205 let data = tensor.to_vec()?;
206
207 let mut min_val = f32::INFINITY;
208 let mut max_val = f32::NEG_INFINITY;
209 let mut sum = 0.0;
210 let mut sum_squares = 0.0;
211
212 for &value in &data {
213 min_val = min_val.min(value);
214 max_val = max_val.max(value);
215 sum += value;
216 sum_squares += value * value;
217 }
218
219 let n = data.len() as f32;
220 let mean = sum / n;
221 let variance = (sum_squares / n) - (mean * mean);
222 let std_dev = variance.sqrt();
223
224 let mut sorted_data = data.clone();
226 sorted_data.sort_by(|a, b| {
227 a.partial_cmp(b)
228 .expect("data comparison should not involve NaN")
229 });
230
231 let percentiles = calculate_percentiles(&sorted_data);
232
233 let zero_count = data.iter().filter(|&&x| x.abs() < 1e-7).count();
235 let sparsity = zero_count as f32 / n;
236
237 let q1 = percentiles.get(&25).copied().unwrap_or(min_val);
239 let q3 = percentiles.get(&75).copied().unwrap_or(max_val);
240 let iqr = q3 - q1;
241 let lower_bound = q1 - 1.5 * iqr;
242 let upper_bound = q3 + 1.5 * iqr;
243
244 let outlier_count = data
245 .iter()
246 .filter(|&&x| x < lower_bound || x > upper_bound)
247 .count();
248 let outlier_ratio = outlier_count as f32 / n;
249
250 Ok(TensorDistributionStats {
251 min_val,
252 max_val,
253 mean,
254 std_dev,
255 percentiles,
256 sparsity,
257 outlier_ratio,
258 num_elements: data.len(),
259 })
260}
261
262fn calculate_percentiles(sorted_data: &[f32]) -> BTreeMap<i32, f32> {
264 let mut percentiles = BTreeMap::new();
265 let percentile_points = vec![1, 5, 10, 25, 50, 75, 90, 95, 99];
266
267 for p in percentile_points {
268 let p_frac = p as f32 / 100.0;
269 let index = (p_frac * (sorted_data.len() - 1) as f32) as usize;
270 let index = index.min(sorted_data.len() - 1);
271 percentiles.insert(p, sorted_data[index]);
272 }
273
274 percentiles
275}
276
277#[derive(Debug, Clone)]
279pub struct TensorDistributionStats {
280 pub min_val: f32,
281 pub max_val: f32,
282 pub mean: f32,
283 pub std_dev: f32,
284 pub percentiles: BTreeMap<i32, f32>,
285 pub sparsity: f32,
286 pub outlier_ratio: f32,
287 pub num_elements: usize,
288}
289
290impl TensorDistributionStats {
291 pub fn recommend_quantization_scheme(&self) -> QuantizationScheme {
293 if self.outlier_ratio > 0.05 {
295 return QuantizationScheme::Percentile(99.9);
296 }
297
298 if self.sparsity > 0.7 {
300 return QuantizationScheme::Dynamic;
301 }
302
303 if self.min_val < 0.0
305 && self.max_val > 0.0
306 && (self.min_val.abs() - self.max_val.abs()).abs() / self.max_val.abs() < 0.2
307 {
308 return QuantizationScheme::Symmetric;
309 }
310
311 QuantizationScheme::Asymmetric
313 }
314
315 pub fn is_quantizable(&self, min_dynamic_range: f32) -> bool {
317 let dynamic_range = self.max_val - self.min_val;
318 dynamic_range > min_dynamic_range && self.std_dev > 1e-6
319 }
320}
321
322pub fn batch_analyze_tensors(tensors: &[&Tensor]) -> Result<Vec<TensorDistributionStats>> {
324 tensors
325 .iter()
326 .map(|tensor| analyze_tensor_distribution(tensor))
327 .collect()
328}
329
330pub fn find_optimal_bitwidth(
332 tensor: &Tensor,
333 max_error: f32,
334 test_bitwidths: &[usize],
335) -> Result<Option<usize>> {
336 let mut best_bitwidth = None;
337
338 for &bitwidth in test_bitwidths {
339 let dtype = match bitwidth {
340 8 => DType::I8,
341 16 => DType::I16,
342 _ => continue, };
344
345 let data = tensor.to_vec()?;
347 let min_val = data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
348 let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
349
350 let scale = match dtype {
351 DType::I8 => max_val.abs().max(min_val.abs()) / 127.0,
352 DType::I16 => max_val.abs().max(min_val.abs()) / 32767.0,
353 _ => continue,
354 };
355
356 let params = QuantizationParams::symmetric(scale, DType::F32, dtype);
357
358 let quantized = params.quantize(tensor)?;
360 let error = calculate_quantization_error(tensor, &quantized, ¶ms)?;
361
362 if error.mse <= max_error * max_error {
363 best_bitwidth = Some(bitwidth);
364 break; }
366 }
367
368 Ok(best_bitwidth)
369}
370
371pub fn create_mixed_precision_config(
373 layer_sensitivities: &HashMap<String, f32>,
374 error_threshold: f32,
375) -> Vec<(String, DType)> {
376 let mut config = Vec::new();
377
378 for (layer_name, &sensitivity) in layer_sensitivities {
379 let dtype = if sensitivity > error_threshold * 2.0 {
380 DType::F16 } else if sensitivity > error_threshold {
382 DType::I16 } else {
384 DType::I8 };
386
387 config.push((layer_name.clone(), dtype));
388 }
389
390 config
391}
392
393pub fn estimate_size_reduction(
395 original_dtype: DType,
396 quantized_dtype: DType,
397 num_parameters: usize,
398) -> SizeReduction {
399 let original_bits = match original_dtype {
400 DType::F32 => 32,
401 DType::F16 => 16,
402 _ => 32,
403 };
404
405 let quantized_bits = match quantized_dtype {
406 DType::I8 | DType::U8 => 8,
407 DType::I16 => 16,
408 DType::F16 => 16,
409 DType::F32 => 32,
410 _ => 32,
411 };
412
413 let original_size = (num_parameters * original_bits) / 8; let quantized_size = (num_parameters * quantized_bits) / 8; let reduction_bytes = original_size - quantized_size;
416 let reduction_ratio = original_size as f32 / quantized_size as f32;
417
418 SizeReduction {
419 original_size_bytes: original_size,
420 quantized_size_bytes: quantized_size,
421 reduction_bytes,
422 reduction_ratio,
423 compression_percentage: (1.0 - (quantized_size as f32 / original_size as f32)) * 100.0,
424 }
425}
426
427#[derive(Debug, Clone)]
429pub struct SizeReduction {
430 pub original_size_bytes: usize,
431 pub quantized_size_bytes: usize,
432 pub reduction_bytes: usize,
433 pub reduction_ratio: f32,
434 pub compression_percentage: f32,
435}
436
437impl SizeReduction {
438 pub fn format_size(&self) -> (String, String) {
440 let original = format_bytes(self.original_size_bytes);
441 let quantized = format_bytes(self.quantized_size_bytes);
442 (original, quantized)
443 }
444}
445
446fn format_bytes(bytes: usize) -> String {
448 const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
449 let mut size = bytes as f64;
450 let mut unit_index = 0;
451
452 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
453 size /= 1024.0;
454 unit_index += 1;
455 }
456
457 format!("{:.2} {}", size, UNITS[unit_index])
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463 use torsh_tensor::Tensor;
464
465 #[test]
466 fn test_tensor_distribution_analysis() {
467 let data = vec![1.0, -1.0, 0.5, -0.5, 2.0, -2.0, 0.0, 0.1, -0.1];
468 let tensor = Tensor::from_data(data, vec![9], torsh_core::device::DeviceType::Cpu)
469 .expect("Tensor should succeed");
470
471 let stats = analyze_tensor_distribution(&tensor)
472 .expect("analyze tensor distribution should succeed");
473
474 assert!(stats.min_val <= -2.0);
475 assert!(stats.max_val >= 2.0);
476 assert!(stats.mean.abs() < 0.5); assert!(stats.num_elements == 9);
478 assert!(stats.sparsity >= 0.0 && stats.sparsity <= 1.0);
479 }
480
481 #[test]
482 fn test_quantization_error_calculation() {
483 let original_data = vec![1.0, 2.0, 3.0, 4.0];
484 let original =
485 Tensor::from_data(original_data, vec![4], torsh_core::device::DeviceType::Cpu)
486 .expect("Tensor should succeed");
487
488 let params = QuantizationParams::symmetric(4.0 / 127.0, DType::F32, DType::I8);
489 let quantized = params
490 .quantize(&original)
491 .expect("quantization should succeed");
492
493 let error = calculate_quantization_error(&original, &quantized, ¶ms)
494 .expect("calculate quantization error should succeed");
495
496 assert!(error.mse >= 0.0);
497 assert!(error.mae >= 0.0);
498 assert!(error.max_error >= 0.0);
499 assert!(error.cosine_similarity >= 0.0 && error.cosine_similarity <= 1.0);
500 }
501
502 #[test]
503 fn test_deployment_config_creation() {
504 let mobile_config = create_deployment_config(DeploymentPlatform::Mobile);
505 assert_eq!(mobile_config.dtype, DType::U8);
506 assert_eq!(mobile_config.scheme, QuantizationScheme::Asymmetric);
507 assert!(mobile_config.per_channel);
508
509 let edge_config = create_deployment_config(DeploymentPlatform::Edge);
510 assert_eq!(edge_config.dtype, DType::I8);
511 assert_eq!(edge_config.scheme, QuantizationScheme::Symmetric);
512 assert!(!edge_config.per_channel);
513 }
514
515 #[test]
516 fn test_size_reduction_estimation() {
517 let reduction = estimate_size_reduction(DType::F32, DType::I8, 1000);
518
519 assert_eq!(reduction.original_size_bytes, 4000); assert_eq!(reduction.quantized_size_bytes, 1000); assert_eq!(reduction.reduction_bytes, 3000);
522 assert_eq!(reduction.reduction_ratio, 4.0);
523 assert_eq!(reduction.compression_percentage, 75.0);
524 }
525
526 #[test]
527 fn test_optimal_bitwidth_finding() {
528 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
529 let tensor = Tensor::from_data(data, vec![5], torsh_core::device::DeviceType::Cpu)
530 .expect("Tensor should succeed");
531
532 let bitwidths = vec![8, 16];
533 let result = find_optimal_bitwidth(&tensor, 0.1, &bitwidths)
534 .expect("find optimal bitwidth should succeed");
535
536 assert!(result.is_some());
538 }
539}