1use crate::config::{QScheme, QuantConfig};
15use torsh_core::{
16 dtype::DType,
17 error::{Result as TorshResult, TorshError},
18};
19use torsh_tensor::Tensor;
20
21#[cfg(not(feature = "std"))]
22extern crate alloc;
23
24#[cfg(not(feature = "std"))]
25use alloc::vec::Vec;
26
27use scirs2_core::parallel_ops::*;
28
29#[derive(Debug, Clone, PartialEq)]
37pub enum QParams {
38 PerTensor {
40 scale: f32,
42 zero_point: i32,
44 },
45 PerChannel {
47 axis: usize,
49 scales: Vec<f32>,
51 zero_points: Vec<i32>,
53 },
54 PerGroup {
56 axis: usize,
58 group_size: usize,
60 scales: Vec<f32>,
62 zero_points: Vec<i32>,
64 },
65}
66
67impl QParams {
68 pub fn representative_scale(&self) -> f32 {
71 match self {
72 QParams::PerTensor { scale, .. } => *scale,
73 QParams::PerChannel { scales, .. } | QParams::PerGroup { scales, .. } => {
74 scales.first().copied().unwrap_or(1.0)
75 }
76 }
77 }
78
79 pub fn representative_zero_point(&self) -> i32 {
82 match self {
83 QParams::PerTensor { zero_point, .. } => *zero_point,
84 QParams::PerChannel { zero_points, .. } | QParams::PerGroup { zero_points, .. } => {
85 zero_points.first().copied().unwrap_or(0)
86 }
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
94pub struct QuantizedTensor {
95 pub tensor: Tensor,
97 pub params: QParams,
99 pub dtype: DType,
101}
102
103impl QuantizedTensor {
104 pub fn dequantize(&self) -> TorshResult<Tensor> {
106 match &self.params {
107 QParams::PerTensor { scale, zero_point } => {
108 dequantize_per_tensor_affine(&self.tensor, *scale, *zero_point)
109 }
110 QParams::PerChannel {
111 axis,
112 scales,
113 zero_points,
114 } => dequantize_per_channel(&self.tensor, *axis, scales, zero_points),
115 QParams::PerGroup {
116 axis,
117 group_size,
118 scales,
119 zero_points,
120 } => dequantize_per_group(&self.tensor, *axis, *group_size, scales, zero_points),
121 }
122 }
123}
124
125fn row_major_strides(shape: &[usize]) -> Vec<usize> {
127 let mut strides = vec![1usize; shape.len()];
128 for i in (0..shape.len().saturating_sub(1)).rev() {
129 strides[i] = strides[i + 1] * shape[i + 1];
130 }
131 strides
132}
133
134pub fn dequantize_per_channel(
136 tensor: &Tensor,
137 axis: usize,
138 scales: &[f32],
139 zero_points: &[i32],
140) -> TorshResult<Tensor> {
141 let binding = tensor.shape();
142 let shape = binding.dims();
143
144 if axis >= shape.len() {
145 return Err(TorshError::InvalidArgument(
146 "Axis out of bounds".to_string(),
147 ));
148 }
149 if scales.len() != shape[axis] || zero_points.len() != shape[axis] {
150 return Err(TorshError::InvalidArgument(
151 "Scales and zero_points length must match channel size".to_string(),
152 ));
153 }
154
155 let data = tensor.data()?;
156 let strides = row_major_strides(shape);
157 let dequantized: Vec<f32> = data
158 .iter()
159 .enumerate()
160 .map(|(idx, &q)| {
161 let channel = (idx / strides[axis]) % shape[axis];
162 (q - zero_points[channel] as f32) * scales[channel]
163 })
164 .collect();
165
166 Tensor::from_data(dequantized, shape.to_vec(), tensor.device())
167}
168
169pub fn dequantize_per_group(
171 tensor: &Tensor,
172 axis: usize,
173 group_size: usize,
174 scales: &[f32],
175 zero_points: &[i32],
176) -> TorshResult<Tensor> {
177 let binding = tensor.shape();
178 let shape = binding.dims();
179
180 if axis >= shape.len() {
181 return Err(TorshError::InvalidArgument(
182 "Axis out of bounds".to_string(),
183 ));
184 }
185 if group_size == 0 {
186 return Err(TorshError::InvalidArgument(
187 "Group size must be greater than 0".to_string(),
188 ));
189 }
190
191 let num_groups = shape[axis].div_ceil(group_size);
192 if scales.len() != num_groups || zero_points.len() != num_groups {
193 return Err(TorshError::InvalidArgument(
194 "Scales and zero_points length must match the number of groups".to_string(),
195 ));
196 }
197
198 let data = tensor.data()?;
199 let strides = row_major_strides(shape);
200 let dequantized: Vec<f32> = data
201 .iter()
202 .enumerate()
203 .map(|(idx, &q)| {
204 let group = ((idx / strides[axis]) % shape[axis]) / group_size;
205 (q - zero_points[group] as f32) * scales[group]
206 })
207 .collect();
208
209 Tensor::from_data(dequantized, shape.to_vec(), tensor.device())
210}
211
212pub fn quantize_with_config_full(
220 tensor: &Tensor,
221 config: &QuantConfig,
222) -> TorshResult<QuantizedTensor> {
223 config.validate()?;
224
225 let (quantized, params) = match config.scheme {
226 QScheme::PerTensorAffine | QScheme::PerTensorSymmetric => {
227 let (quantized, scale, zero_point) =
228 quantize_tensor_auto(tensor, config.dtype, config.scheme)?;
229 (quantized, QParams::PerTensor { scale, zero_point })
230 }
231 QScheme::PerChannelAffine | QScheme::PerChannelSymmetric => {
232 let axis = config.ch_axis.unwrap_or(0);
233 let (quantized, scales, zero_points) =
234 quantize_per_channel_auto(tensor, axis, config.dtype, config.scheme)?;
235 (
236 quantized,
237 QParams::PerChannel {
238 axis,
239 scales,
240 zero_points,
241 },
242 )
243 }
244 QScheme::GroupWise => {
245 let axis = config.ch_axis.unwrap_or(0);
246 let group_size = config.group_size.unwrap_or(32);
247 let (quantized, scales, zero_points) =
248 crate::specialized::quantize_group_wise_full(tensor, axis, group_size, config)?;
249 (
250 quantized,
251 QParams::PerGroup {
252 axis,
253 group_size,
254 scales,
255 zero_points,
256 },
257 )
258 }
259 QScheme::Int4PerTensor => {
260 let (quantized, scale, zero_point) =
261 crate::specialized::quantize_int4_per_tensor(tensor, config)?;
262 (quantized, QParams::PerTensor { scale, zero_point })
263 }
264 QScheme::Int4PerChannel => {
265 let axis = config.ch_axis.unwrap_or(0);
266 let (quantized, scales, zero_points) =
267 crate::specialized::quantize_int4_per_channel_full(tensor, axis, config)?;
268 (
269 quantized,
270 QParams::PerChannel {
271 axis,
272 scales,
273 zero_points,
274 },
275 )
276 }
277 QScheme::Binary => {
278 let (quantized, scale, zero_point) = crate::specialized::quantize_binary(tensor)?;
279 (quantized, QParams::PerTensor { scale, zero_point })
280 }
281 QScheme::Ternary => {
282 let (quantized, scale, zero_point) = crate::specialized::quantize_ternary(tensor)?;
283 (quantized, QParams::PerTensor { scale, zero_point })
284 }
285 QScheme::MixedPrecision => {
286 return Err(TorshError::InvalidArgument(
287 "Mixed precision quantization requires specialized API".to_string(),
288 ));
289 }
290 };
291
292 Ok(QuantizedTensor {
293 tensor: quantized,
294 params,
295 dtype: config.dtype,
296 })
297}
298
299pub fn quantize_with_config(
309 tensor: &Tensor,
310 config: &QuantConfig,
311) -> TorshResult<(Tensor, f32, i32)> {
312 config.validate()?;
313
314 match config.scheme {
315 QScheme::PerTensorAffine | QScheme::PerTensorSymmetric => {
316 quantize_tensor_auto(tensor, config.dtype, config.scheme)
317 }
318 QScheme::PerChannelAffine | QScheme::PerChannelSymmetric => {
319 let axis = config.ch_axis.unwrap_or(0);
320 let (quantized, scales, zero_points) =
321 quantize_per_channel_auto(tensor, axis, config.dtype, config.scheme)?;
322 Ok((quantized, scales[0], zero_points[0]))
324 }
325 QScheme::GroupWise => {
326 let axis = config.ch_axis.unwrap_or(0);
327 let group_size = config.group_size.unwrap_or(32);
328 crate::specialized::quantize_group_wise(tensor, axis, group_size, config)
329 }
330 QScheme::Int4PerTensor => crate::specialized::quantize_int4_per_tensor(tensor, config),
331 QScheme::Int4PerChannel => {
332 let axis = config.ch_axis.unwrap_or(0);
333 crate::specialized::quantize_int4_per_channel(tensor, axis, config)
334 }
335 QScheme::Binary => crate::specialized::quantize_binary(tensor),
336 QScheme::Ternary => crate::specialized::quantize_ternary(tensor),
337 QScheme::MixedPrecision => {
338 Err(TorshError::InvalidArgument(
340 "Mixed precision quantization requires specialized API".to_string(),
341 ))
342 }
343 }
344}
345
346pub fn quantize_per_tensor(
348 tensor: &Tensor,
349 scale: f32,
350 zero_point: i32,
351 dtype: DType,
352) -> TorshResult<Tensor> {
353 let (quantized, _, _) = quantize_per_tensor_affine_dtype(tensor, scale, zero_point, dtype)?;
354 Ok(quantized)
355}
356
357pub fn dequantize(tensor: &Tensor, scale: f32, zero_point: i32) -> TorshResult<Tensor> {
359 dequantize_per_tensor_affine(tensor, scale, zero_point)
360}
361
362pub fn quantize_tensor_auto(
364 tensor: &Tensor,
365 dtype: DType,
366 scheme: QScheme,
367) -> TorshResult<(Tensor, f32, i32)> {
368 let data = tensor.data()?;
369
370 if data.is_empty() {
371 return Err(TorshError::InvalidArgument(
372 "Cannot quantize empty tensor".to_string(),
373 ));
374 }
375
376 let (min_val, max_val) = if data.len() > 64 && crate::simd_ops::is_simd_available() {
378 crate::simd_ops::find_min_max_simd(&data)?
379 } else {
380 let min_val = data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
382 let max_val = data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
383 (min_val, max_val)
384 };
385
386 let (scale, zero_point) = match scheme {
388 QScheme::PerTensorAffine => calculate_affine_quantization_params(min_val, max_val, dtype)?,
389 QScheme::PerTensorSymmetric => {
390 calculate_symmetric_quantization_params(min_val, max_val, dtype)?
391 }
392 _ => {
393 return Err(TorshError::InvalidArgument(format!(
394 "Unsupported scheme for auto quantization: {:?}",
395 scheme
396 )));
397 }
398 };
399
400 quantize_per_tensor_affine_dtype(tensor, scale, zero_point, dtype)
401}
402
403pub fn quantize_per_channel_auto(
405 tensor: &Tensor,
406 axis: usize,
407 dtype: DType,
408 scheme: QScheme,
409) -> TorshResult<(Tensor, Vec<f32>, Vec<i32>)> {
410 let binding = tensor.shape();
411 let shape = binding.dims();
412
413 if axis >= shape.len() {
414 return Err(TorshError::InvalidArgument(
415 "Axis out of bounds".to_string(),
416 ));
417 }
418
419 let num_channels = shape[axis];
420 let data = tensor.data()?;
421
422 let mut strides = vec![1; shape.len()];
424 for i in (0..shape.len() - 1).rev() {
425 strides[i] = strides[i + 1] * shape[i + 1];
426 }
427
428 let mut scales = Vec::with_capacity(num_channels);
429 let mut zero_points = Vec::with_capacity(num_channels);
430 let mut quantized_data = vec![0.0f32; data.len()];
431
432 for ch in 0..num_channels {
434 let mut channel_min = f32::INFINITY;
435 let mut channel_max = f32::NEG_INFINITY;
436
437 let mut indices = vec![0; shape.len()];
439 let channel_size = data.len() / num_channels;
440
441 for i in 0..channel_size {
442 let mut temp_i = i;
444 for dim in (0..shape.len()).rev() {
445 if dim == axis {
446 indices[dim] = ch;
447 } else {
448 let other_dim_size = if dim == axis { 1 } else { shape[dim] };
449 indices[dim] = temp_i % other_dim_size;
450 temp_i /= other_dim_size;
451 }
452 }
453
454 let flat_idx = indices
456 .iter()
457 .zip(strides.iter())
458 .map(|(idx, stride)| idx * stride)
459 .sum::<usize>();
460
461 if flat_idx < data.len() {
462 let val = data[flat_idx];
463 channel_min = channel_min.min(val);
464 channel_max = channel_max.max(val);
465 }
466 }
467
468 let (scale, zero_point) = match scheme {
470 QScheme::PerChannelAffine => {
471 calculate_affine_quantization_params(channel_min, channel_max, dtype)?
472 }
473 QScheme::PerChannelSymmetric => {
474 calculate_symmetric_quantization_params(channel_min, channel_max, dtype)?
475 }
476 _ => {
477 return Err(TorshError::InvalidArgument(format!(
478 "Unsupported scheme for per-channel quantization: {:?}",
479 scheme
480 )));
481 }
482 };
483
484 scales.push(scale);
485 zero_points.push(zero_point);
486
487 for i in 0..channel_size {
489 let mut temp_i = i;
490 for dim in (0..shape.len()).rev() {
491 if dim == axis {
492 indices[dim] = ch;
493 } else {
494 let other_dim_size = if dim == axis { 1 } else { shape[dim] };
495 indices[dim] = temp_i % other_dim_size;
496 temp_i /= other_dim_size;
497 }
498 }
499
500 let flat_idx = indices
501 .iter()
502 .zip(strides.iter())
503 .map(|(idx, stride)| idx * stride)
504 .sum::<usize>();
505
506 if flat_idx < data.len() {
507 let val = data[flat_idx];
508 let quantized = ((val / scale).round() + zero_point as f32).clamp(
509 get_dtype_range(dtype).0 as f32,
510 get_dtype_range(dtype).1 as f32,
511 );
512 quantized_data[flat_idx] = quantized;
513 }
514 }
515 }
516
517 let quantized_tensor = Tensor::from_data(quantized_data, shape.to_vec(), tensor.device())?;
518
519 Ok((quantized_tensor, scales, zero_points))
520}
521
522pub fn quantize_per_tensor_affine_i8(
524 tensor: &Tensor,
525 scale: f32,
526 zero_point: i32,
527) -> TorshResult<(Tensor<i8>, f32, i32)> {
528 let data = tensor.data()?;
529
530 if scale <= 0.0 {
531 return Err(TorshError::InvalidArgument(
532 "Scale must be positive".to_string(),
533 ));
534 }
535
536 let quantized_data: Vec<i8> = data
537 .iter()
538 .map(|&x| {
539 let quantized = (x / scale).round() + zero_point as f32;
540 quantized.clamp(-128.0, 127.0) as i8
542 })
543 .collect();
544
545 let quantized_tensor = Tensor::from_data(
546 quantized_data,
547 tensor.shape().dims().to_vec(),
548 tensor.device(),
549 )?;
550
551 Ok((quantized_tensor, scale, zero_point))
552}
553
554pub fn quantize_per_tensor_affine(
560 tensor: &Tensor,
561 scale: f32,
562 zero_point: i32,
563) -> TorshResult<(Tensor, f32, i32)> {
564 quantize_per_tensor_affine_dtype(tensor, scale, zero_point, DType::I8)
565}
566
567pub fn quantize_per_tensor_affine_dtype(
573 tensor: &Tensor,
574 scale: f32,
575 zero_point: i32,
576 dtype: DType,
577) -> TorshResult<(Tensor, f32, i32)> {
578 let data = tensor.data()?;
579
580 if scale <= 0.0 {
581 return Err(TorshError::InvalidArgument(
582 "Scale must be positive".to_string(),
583 ));
584 }
585
586 let (qmin, qmax) = get_dtype_range(dtype);
587 if !(qmin..=qmax).contains(&zero_point) {
588 return Err(TorshError::InvalidArgument(format!(
589 "Zero point {zero_point} must be in range [{qmin}, {qmax}] for {dtype:?}"
590 )));
591 }
592
593 let mut quantized_data = vec![0.0f32; data.len()];
595 if data.len() > 64 && crate::simd_ops::is_simd_available() {
596 crate::simd_ops::quantize_per_tensor_affine_simd_range(
598 &data,
599 scale,
600 zero_point,
601 qmin,
602 qmax,
603 &mut quantized_data,
604 )?;
605 } else {
606 let (qmin_f, qmax_f) = (qmin as f32, qmax as f32);
608 for (i, &x) in data.iter().enumerate() {
609 let quantized = (x / scale).round() + zero_point as f32;
610 quantized_data[i] = quantized.clamp(qmin_f, qmax_f);
611 }
612 }
613
614 let quantized_tensor = Tensor::from_data(
615 quantized_data,
616 tensor.shape().dims().to_vec(),
617 tensor.device(),
618 )?;
619
620 Ok((quantized_tensor, scale, zero_point))
621}
622
623pub fn dequantize_per_tensor_affine(
625 tensor: &Tensor,
626 scale: f32,
627 zero_point: i32,
628) -> TorshResult<Tensor> {
629 let data = tensor.data()?;
630
631 if scale <= 0.0 {
632 return Err(TorshError::InvalidArgument(
633 "Scale must be positive".to_string(),
634 ));
635 }
636
637 let mut dequantized_data = vec![0.0f32; data.len()];
639 if data.len() > 64 && crate::simd_ops::is_simd_available() {
640 crate::simd_ops::dequantize_per_tensor_affine_simd(
642 &data,
643 scale,
644 zero_point,
645 &mut dequantized_data,
646 )?;
647 } else {
648 for (i, &x) in data.iter().enumerate() {
650 dequantized_data[i] = (x - zero_point as f32) * scale;
651 }
652 }
653
654 let dequantized_tensor = Tensor::from_data(
655 dequantized_data,
656 tensor.shape().dims().to_vec(),
657 tensor.device(),
658 )?;
659
660 Ok(dequantized_tensor)
661}
662
663pub fn calculate_affine_quantization_params(
665 min_val: f32,
666 max_val: f32,
667 dtype: DType,
668) -> TorshResult<(f32, i32)> {
669 if !min_val.is_finite() || !max_val.is_finite() {
670 return Err(TorshError::InvalidArgument(
671 "Min and max values must be finite".to_string(),
672 ));
673 }
674
675 if min_val > max_val {
676 return Err(TorshError::InvalidArgument(
677 "Min value must be <= max value".to_string(),
678 ));
679 }
680
681 let (qmin, qmax) = get_dtype_range(dtype);
682 let qmin = qmin as f32;
683 let qmax = qmax as f32;
684
685 if (max_val - min_val).abs() < f32::EPSILON {
687 let scale = 1.0;
688 let zero_point = qmin as i32;
689 return Ok((scale, zero_point));
690 }
691
692 let scale = (max_val - min_val) / (qmax - qmin);
694
695 let zero_point_fp = qmin - min_val / scale;
697 let zero_point = zero_point_fp.round().clamp(qmin, qmax) as i32;
698
699 Ok((scale, zero_point))
700}
701
702pub fn calculate_symmetric_quantization_params(
712 min_val: f32,
713 max_val: f32,
714 dtype: DType,
715) -> TorshResult<(f32, i32)> {
716 if !min_val.is_finite() || !max_val.is_finite() {
717 return Err(TorshError::InvalidArgument(
718 "Min and max values must be finite".to_string(),
719 ));
720 }
721
722 let (qmin, qmax) = get_dtype_range(dtype);
723 let abs_max = min_val.abs().max(max_val.abs());
724
725 let zero_point = if qmin >= 0 {
729 (((qmin as i64) + (qmax as i64) + 1) / 2) as i32
730 } else {
731 0
732 };
733
734 if abs_max < f32::EPSILON {
736 return Ok((1.0, zero_point));
737 }
738
739 let positive_headroom = (qmax - zero_point).max(1) as f32;
742 let scale = abs_max / positive_headroom;
743
744 Ok((scale, zero_point))
745}
746
747pub fn get_dtype_range(dtype: DType) -> (i32, i32) {
749 match dtype {
750 DType::I8 => (-128, 127),
751 DType::U8 => (0, 255),
752 DType::I16 => (-32768, 32767),
753 DType::I32 => (i32::MIN, i32::MAX),
754 _ => (-128, 127), }
756}
757
758pub fn quantize_auto(tensor: &Tensor, config: &QuantConfig) -> TorshResult<(Tensor, f32, i32)> {
760 quantize_with_config(tensor, config)
761}
762
763#[derive(Debug, Clone)]
767pub struct CacheAwareParams {
768 pub cache_line_size: usize,
770 pub l1_cache_size: usize,
772 pub l2_cache_size: usize,
774 pub l3_cache_size: usize,
776 pub prefetch_distance: usize,
778 pub enable_chunking: bool,
780}
781
782impl Default for CacheAwareParams {
783 fn default() -> Self {
784 Self {
785 cache_line_size: 64,
786 l1_cache_size: 32 * 1024, l2_cache_size: 256 * 1024, l3_cache_size: 8 * 1024 * 1024, prefetch_distance: 16,
790 enable_chunking: true,
791 }
792 }
793}
794
795pub fn quantize_per_tensor_affine_cache_aware(
797 input: &[f32],
798 scale: f32,
799 zero_point: i32,
800 output: &mut [f32],
801 cache_params: &CacheAwareParams,
802) -> TorshResult<()> {
803 if input.len() != output.len() {
804 return Err(TorshError::InvalidArgument(
805 "Input and output length mismatch".to_string(),
806 ));
807 }
808
809 if scale <= 0.0 {
810 return Err(TorshError::InvalidArgument(
811 "Scale must be positive".to_string(),
812 ));
813 }
814
815 let inv_scale = 1.0 / scale;
816 let zero_point_f32 = zero_point as f32;
817
818 if !cache_params.enable_chunking || input.len() < cache_params.cache_line_size {
819 for (inp, out) in input.iter().zip(output.iter_mut()) {
821 let quantized = (*inp * inv_scale).round() + zero_point_f32;
822 *out = quantized.clamp(-128.0, 127.0);
823 }
824 return Ok(());
825 }
826
827 let _elements_per_cache_line = cache_params.cache_line_size / std::mem::size_of::<f32>();
829 let optimal_chunk_size =
830 (cache_params.l2_cache_size / std::mem::size_of::<f32>() / 4).min(input.len());
831
832 input
834 .par_chunks(optimal_chunk_size)
835 .zip(output.par_chunks_mut(optimal_chunk_size))
836 .for_each(|(input_chunk, output_chunk)| {
837 for (inp, out) in input_chunk.iter().zip(output_chunk.iter_mut()) {
839 let quantized = (*inp * inv_scale).round() + zero_point_f32;
840 *out = quantized.clamp(-128.0, 127.0);
841 }
842 });
843
844 Ok(())
845}
846
847pub fn calculate_tensor_stats_cache_optimized(
849 data: &[f32],
850 cache_params: &CacheAwareParams,
851) -> TorshResult<(f32, f32, f32, f32)> {
852 if data.is_empty() {
853 return Err(TorshError::InvalidArgument(
854 "Cannot calculate stats of empty tensor".to_string(),
855 ));
856 }
857
858 let optimal_block_size = cache_params.l2_cache_size / std::mem::size_of::<f32>();
859 let block_size = optimal_block_size.min(data.len());
860
861 let results: Vec<(f32, f32, f64, f64)> = data
863 .par_chunks(block_size)
864 .map(|chunk| {
865 let mut local_min = f32::INFINITY;
866 let mut local_max = f32::NEG_INFINITY;
867 let mut local_sum = 0.0f64;
868 let mut local_sum_sq = 0.0f64;
869
870 for &val in chunk {
872 local_min = local_min.min(val);
873 local_max = local_max.max(val);
874 let val_f64 = val as f64;
875 local_sum += val_f64;
876 local_sum_sq += val_f64 * val_f64;
877 }
878
879 (local_min, local_max, local_sum, local_sum_sq)
880 })
881 .collect();
882
883 let mut min_val = f32::INFINITY;
885 let mut max_val = f32::NEG_INFINITY;
886 let mut total_sum = 0.0f64;
887 let mut total_sum_sq = 0.0f64;
888
889 for (local_min, local_max, local_sum, local_sum_sq) in results {
890 min_val = min_val.min(local_min);
891 max_val = max_val.max(local_max);
892 total_sum += local_sum;
893 total_sum_sq += local_sum_sq;
894 }
895
896 let n = data.len() as f64;
897 let mean = (total_sum / n) as f32;
898 let variance = ((total_sum_sq / n) - (mean as f64).powi(2)) as f32;
899
900 Ok((min_val, max_val, mean, variance.sqrt()))
901}
902
903pub fn quantize_matrix_cache_friendly(
905 matrix: &[f32],
906 rows: usize,
907 cols: usize,
908 scale: f32,
909 zero_point: i32,
910 output: &mut [f32],
911 cache_params: &CacheAwareParams,
912) -> TorshResult<()> {
913 if matrix.len() != rows * cols || output.len() != rows * cols {
914 return Err(TorshError::InvalidArgument(
915 "Matrix dimensions don't match buffer sizes".to_string(),
916 ));
917 }
918
919 if scale <= 0.0 {
920 return Err(TorshError::InvalidArgument(
921 "Scale must be positive".to_string(),
922 ));
923 }
924
925 let inv_scale = 1.0 / scale;
926 let zero_point_f32 = zero_point as f32;
927
928 let elements_per_cache_line = cache_params.cache_line_size / std::mem::size_of::<f32>();
930 let l2_elements = cache_params.l2_cache_size / std::mem::size_of::<f32>();
931
932 let max_tile_size = (l2_elements / 4).min(1024); let tile_rows = (max_tile_size / cols).max(1).min(rows);
935 let tile_cols = (max_tile_size / tile_rows)
936 .max(elements_per_cache_line)
937 .min(cols);
938
939 for row_start in (0..rows).step_by(tile_rows) {
941 let row_end = (row_start + tile_rows).min(rows);
942
943 for col_start in (0..cols).step_by(tile_cols) {
944 let col_end = (col_start + tile_cols).min(cols);
945
946 for row in row_start..row_end {
948 for col in col_start..col_end {
949 let idx = row * cols + col;
950 let quantized = (matrix[idx] * inv_scale).round() + zero_point_f32;
951 output[idx] = quantized.clamp(-128.0, 127.0);
952 }
953 }
954 }
955 }
956
957 Ok(())
958}
959
960pub fn quantize_streaming_with_prefetch(
962 input: &[f32],
963 scale: f32,
964 zero_point: i32,
965 output: &mut [f32],
966 cache_params: &CacheAwareParams,
967) -> TorshResult<()> {
968 if input.len() != output.len() {
969 return Err(TorshError::InvalidArgument(
970 "Input and output length mismatch".to_string(),
971 ));
972 }
973
974 if scale <= 0.0 {
975 return Err(TorshError::InvalidArgument(
976 "Scale must be positive".to_string(),
977 ));
978 }
979
980 let inv_scale = 1.0 / scale;
981 let zero_point_f32 = zero_point as f32;
982 let prefetch_distance = cache_params.prefetch_distance;
983
984 for i in 0..input.len() {
986 if i + prefetch_distance < input.len() {
988 let _prefetch_addr = &input[i + prefetch_distance];
990 }
991
992 let quantized = (input[i] * inv_scale).round() + zero_point_f32;
993 output[i] = quantized.clamp(-128.0, 127.0);
994 }
995
996 Ok(())
997}
998
999pub fn get_cache_optimization_recommendations(
1001 tensor_size: usize,
1002 element_size: usize,
1003 cache_params: &CacheAwareParams,
1004) -> Vec<String> {
1005 let mut recommendations = Vec::new();
1006 let total_bytes = tensor_size * element_size;
1007
1008 if total_bytes <= cache_params.l1_cache_size {
1009 recommendations.push("Tensor fits in L1 cache - use simple sequential access".to_string());
1010 } else if total_bytes <= cache_params.l2_cache_size {
1011 recommendations.push("Tensor fits in L2 cache - consider blocked algorithms".to_string());
1012 } else if total_bytes <= cache_params.l3_cache_size {
1013 recommendations
1014 .push("Tensor fits in L3 cache - use tiled processing with medium blocks".to_string());
1015 } else {
1016 recommendations
1017 .push("Large tensor - use streaming algorithms with prefetching".to_string());
1018 recommendations
1019 .push("Consider parallel processing to utilize multiple cache hierarchies".to_string());
1020 }
1021
1022 let elements_per_cache_line = cache_params.cache_line_size / element_size;
1023 if tensor_size % elements_per_cache_line != 0 {
1024 recommendations.push(format!(
1025 "Consider padding to align with cache lines ({}B boundaries)",
1026 cache_params.cache_line_size
1027 ));
1028 }
1029
1030 recommendations
1031}
1032
1033pub fn quantize_with_cache_optimization(
1035 input: &[f32],
1036 scale: f32,
1037 zero_point: i32,
1038 output: &mut [f32],
1039 cache_params: Option<&CacheAwareParams>,
1040) -> TorshResult<()> {
1041 let default_params = CacheAwareParams::default();
1042 let params = cache_params.unwrap_or(&default_params);
1043 let total_bytes = std::mem::size_of_val(input);
1044
1045 if total_bytes <= params.l1_cache_size {
1046 quantize_streaming_with_prefetch(input, scale, zero_point, output, params)
1048 } else if total_bytes <= params.l2_cache_size {
1049 quantize_per_tensor_affine_cache_aware(input, scale, zero_point, output, params)
1051 } else {
1052 quantize_per_tensor_affine_cache_aware(input, scale, zero_point, output, params)
1054 }
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use super::*;
1060 use crate::config::{QScheme, QuantConfig};
1061
1062 use torsh_tensor::creation::tensor_1d;
1063
1064 #[test]
1065 fn test_calculate_affine_quantization_params() {
1066 let (scale, zero_point) =
1068 calculate_affine_quantization_params(-1.0, 1.0, DType::I8).unwrap();
1069
1070 assert!(scale > 0.0);
1071 assert!(zero_point >= -128 && zero_point <= 127);
1072
1073 let (scale, zero_point) =
1075 calculate_affine_quantization_params(1.0, 1.0, DType::I8).unwrap();
1076
1077 assert_eq!(scale, 1.0);
1078 assert_eq!(zero_point, -128);
1079
1080 let result = calculate_affine_quantization_params(2.0, 1.0, DType::I8);
1082 assert!(result.is_err());
1083 }
1084
1085 #[test]
1086 fn test_calculate_symmetric_quantization_params() {
1087 let (scale, zero_point) =
1089 calculate_symmetric_quantization_params(-2.0, 1.0, DType::I8).unwrap();
1090
1091 assert!(scale > 0.0);
1092 assert_eq!(zero_point, 0); let (scale, zero_point) =
1096 calculate_symmetric_quantization_params(0.0, 0.0, DType::I8).unwrap();
1097
1098 assert_eq!(scale, 1.0);
1099 assert_eq!(zero_point, 0);
1100 }
1101
1102 #[test]
1103 fn test_get_dtype_range() {
1104 assert_eq!(get_dtype_range(DType::I8), (-128, 127));
1105 assert_eq!(get_dtype_range(DType::U8), (0, 255));
1106 assert_eq!(get_dtype_range(DType::I16), (-32768, 32767));
1107 }
1108
1109 #[test]
1110 fn test_quantize_per_tensor_affine() {
1111 let data = vec![1.0, 2.0, 3.0, 4.0];
1112 let tensor = tensor_1d(&data).unwrap();
1113
1114 let (quantized, scale, zero_point) = quantize_per_tensor_affine(&tensor, 0.1, 0).unwrap();
1115
1116 let quantized_data = quantized.data().unwrap();
1117
1118 assert_eq!(quantized_data[0], 10.0); assert_eq!(quantized_data[1], 20.0); assert_eq!(scale, 0.1);
1122 assert_eq!(zero_point, 0);
1123 }
1124
1125 #[test]
1126 fn test_dequantize_per_tensor_affine() {
1127 let quantized_data = vec![10.0, 20.0, 30.0, 40.0];
1128 let quantized_tensor = tensor_1d(&quantized_data).unwrap();
1129
1130 let dequantized = dequantize_per_tensor_affine(&quantized_tensor, 0.1, 0).unwrap();
1131 let dequantized_data = dequantized.data().unwrap();
1132
1133 assert!((dequantized_data[0] - 1.0).abs() < 1e-6); assert!((dequantized_data[1] - 2.0).abs() < 1e-6); }
1137
1138 #[test]
1139 fn test_quantize_tensor_auto() {
1140 let data = vec![-1.0, 0.0, 1.0, 2.0];
1141 let tensor = tensor_1d(&data).unwrap();
1142
1143 let (quantized, scale, zero_point) =
1144 quantize_tensor_auto(&tensor, DType::I8, QScheme::PerTensorAffine).unwrap();
1145
1146 assert!(scale > 0.0);
1147 assert!(zero_point >= -128 && zero_point <= 127);
1148
1149 let quantized_data = quantized.data().unwrap();
1151 assert_eq!(quantized_data.len(), data.len());
1152 }
1153
1154 #[test]
1155 fn test_quantize_with_config() {
1156 let data = vec![1.0, 2.0, 3.0, 4.0];
1157 let tensor = tensor_1d(&data).unwrap();
1158 let config = QuantConfig::int8();
1159
1160 let result = quantize_with_config(&tensor, &config);
1161 assert!(result.is_ok());
1162
1163 let (quantized, scale, zero_point) = result.unwrap();
1164 assert!(scale > 0.0);
1165 assert!(zero_point >= -128 && zero_point <= 127);
1166 assert_eq!(quantized.shape().dims(), tensor.shape().dims());
1167 }
1168
1169 #[test]
1170 fn test_dequantize() {
1171 let quantized_data = vec![64.0, 128.0, -64.0, 0.0];
1172 let quantized_tensor = tensor_1d(&quantized_data).unwrap();
1173
1174 let dequantized = dequantize(&quantized_tensor, 0.5, 0).unwrap();
1175 let dequantized_data = dequantized.data().unwrap();
1176
1177 assert!((dequantized_data[0] - 32.0).abs() < 1e-6);
1179 assert!((dequantized_data[1] - 64.0).abs() < 1e-6);
1180 assert!((dequantized_data[2] + 32.0).abs() < 1e-6);
1181 assert!((dequantized_data[3] - 0.0).abs() < 1e-6);
1182 }
1183
1184 #[test]
1185 fn test_quantize_auto() {
1186 let data = vec![0.5, 1.0, 1.5, 2.0];
1187 let tensor = tensor_1d(&data).unwrap();
1188 let config = QuantConfig::int8();
1189
1190 let result = quantize_auto(&tensor, &config);
1191 assert!(result.is_ok());
1192
1193 let (quantized, scale, _zero_point) = result.unwrap();
1194 assert!(scale > 0.0);
1195 assert_eq!(quantized.shape().dims(), tensor.shape().dims());
1196 }
1197
1198 #[test]
1199 fn test_error_cases() {
1200 let data = vec![1.0, 2.0];
1202 let tensor = tensor_1d(&data).unwrap();
1203
1204 let result = quantize_per_tensor_affine(&tensor, -1.0, 0);
1205 assert!(result.is_err());
1206
1207 let result = dequantize_per_tensor_affine(&tensor, 0.0, 0);
1208 assert!(result.is_err());
1209
1210 let empty_data: Vec<f32> = vec![];
1212 let empty_tensor = tensor_1d(&empty_data).unwrap();
1213
1214 let result = quantize_tensor_auto(&empty_tensor, DType::I8, QScheme::PerTensorAffine);
1215 assert!(result.is_err());
1216 }
1217
1218 #[test]
1221 fn test_cache_aware_quantization() {
1222 let input = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1223 let mut output = vec![0.0; 8];
1224 let cache_params = CacheAwareParams::default();
1225
1226 let result =
1227 quantize_per_tensor_affine_cache_aware(&input, 0.1, 0, &mut output, &cache_params);
1228
1229 assert!(result.is_ok());
1230 assert_eq!(output[0], 10.0);
1231 assert_eq!(output[7], 80.0);
1232 }
1233
1234 #[test]
1235 fn test_cache_optimized_stats() {
1236 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
1237 let cache_params = CacheAwareParams::default();
1238
1239 let result = calculate_tensor_stats_cache_optimized(&data, &cache_params);
1240 assert!(result.is_ok());
1241
1242 let (min_val, max_val, mean, std_dev) = result.unwrap();
1243 assert_eq!(min_val, 1.0);
1244 assert_eq!(max_val, 10.0);
1245 assert!((mean - 5.5).abs() < 0.001);
1246 assert!(std_dev > 0.0);
1247 }
1248
1249 #[test]
1250 fn test_matrix_cache_friendly_quantization() {
1251 let matrix = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
1252 let mut output = vec![0.0; 9];
1253 let cache_params = CacheAwareParams::default();
1254
1255 let result =
1256 quantize_matrix_cache_friendly(&matrix, 3, 3, 0.1, 0, &mut output, &cache_params);
1257
1258 assert!(result.is_ok());
1259 assert_eq!(output[0], 10.0); assert_eq!(output[8], 90.0); }
1262
1263 #[test]
1264 fn test_streaming_with_prefetch() {
1265 let input = vec![0.1, 0.2, 0.3, 0.4, 0.5];
1266 let mut output = vec![0.0; 5];
1267 let cache_params = CacheAwareParams::default();
1268
1269 let result = quantize_streaming_with_prefetch(&input, 0.01, 10, &mut output, &cache_params);
1270
1271 assert!(result.is_ok());
1272 assert_eq!(output[0], 20.0);
1274 }
1275
1276 #[test]
1277 fn test_cache_optimization_recommendations() {
1278 let cache_params = CacheAwareParams::default();
1279
1280 let recommendations = get_cache_optimization_recommendations(1000, 4, &cache_params);
1282 assert!(!recommendations.is_empty());
1283 assert!(recommendations[0].contains("L1 cache"));
1284
1285 let large_recommendations =
1287 get_cache_optimization_recommendations(10_000_000, 4, &cache_params);
1288 assert!(large_recommendations
1289 .iter()
1290 .any(|r| r.contains("streaming")));
1291 }
1292
1293 #[test]
1294 fn test_auto_cache_optimization() {
1295 let input = vec![1.0, 2.0, 3.0, 4.0];
1296 let mut output = vec![0.0; 4];
1297
1298 let result = quantize_with_cache_optimization(&input, 0.1, 0, &mut output, None);
1300
1301 assert!(result.is_ok());
1302 assert_eq!(output[0], 10.0);
1303 assert_eq!(output[3], 40.0);
1304 }
1305
1306 #[test]
1307 fn test_cache_params_default() {
1308 let params = CacheAwareParams::default();
1309
1310 assert_eq!(params.cache_line_size, 64);
1311 assert_eq!(params.l1_cache_size, 32 * 1024);
1312 assert_eq!(params.l2_cache_size, 256 * 1024);
1313 assert_eq!(params.l3_cache_size, 8 * 1024 * 1024);
1314 assert_eq!(params.prefetch_distance, 16);
1315 assert!(params.enable_chunking);
1316 }
1317
1318 #[test]
1319 fn test_cache_aware_error_cases() {
1320 let input = vec![1.0, 2.0];
1321 let mut output = vec![0.0; 3]; let cache_params = CacheAwareParams::default();
1323
1324 let result =
1325 quantize_per_tensor_affine_cache_aware(&input, 0.1, 0, &mut output, &cache_params);
1326 assert!(result.is_err());
1327
1328 let mut output_correct = vec![0.0; 2];
1330 let result = quantize_per_tensor_affine_cache_aware(
1331 &input,
1332 -0.1,
1333 0,
1334 &mut output_correct,
1335 &cache_params,
1336 );
1337 assert!(result.is_err());
1338
1339 let empty_data: Vec<f32> = vec![];
1341 let result = calculate_tensor_stats_cache_optimized(&empty_data, &cache_params);
1342 assert!(result.is_err());
1343 }
1344}