Skip to main content

torsh_quantization/
algorithms.rs

1//! Core quantization algorithms and tensor operations
2//!
3//! This module provides the fundamental quantization and dequantization algorithms
4//! for tensor operations, including per-tensor and per-channel quantization schemes.
5//!
6//! # Features
7//!
8//! - **Per-tensor quantization**: Single scale/zero-point for entire tensor
9//! - **Per-channel quantization**: Individual scale/zero-point per channel
10//! - **Dequantization**: Reverse quantization to restore floating point values
11//! - **Multiple schemes**: Affine and symmetric quantization support
12//! - **Configuration-driven**: Integration with QuantConfig for flexible usage
13
14use 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/// Quantization parameters describing how a quantized tensor can be inverted.
30///
31/// Per-channel and group-wise schemes need one scale/zero-point pair *per
32/// channel* (or per group); collapsing them to a single pair — as the legacy
33/// `(Tensor, f32, i32)` return type is forced to do — makes dequantization
34/// impossible for every channel but the first. [`QuantizedTensor`] keeps the
35/// full parameter set so the transform stays invertible.
36#[derive(Debug, Clone, PartialEq)]
37pub enum QParams {
38    /// A single scale/zero-point pair for the whole tensor.
39    PerTensor {
40        /// Quantization step size.
41        scale: f32,
42        /// Integer code mapped to the real value zero.
43        zero_point: i32,
44    },
45    /// One scale/zero-point pair per channel along `axis`.
46    PerChannel {
47        /// Channel axis the parameters are indexed by.
48        axis: usize,
49        /// Per-channel step sizes (length == `shape[axis]`).
50        scales: Vec<f32>,
51        /// Per-channel zero points (length == `shape[axis]`).
52        zero_points: Vec<i32>,
53    },
54    /// One scale/zero-point pair per contiguous group of channels along `axis`.
55    PerGroup {
56        /// Channel axis the groups are formed along.
57        axis: usize,
58        /// Number of channels per group.
59        group_size: usize,
60        /// Per-group step sizes.
61        scales: Vec<f32>,
62        /// Per-group zero points.
63        zero_points: Vec<i32>,
64    },
65}
66
67impl QParams {
68    /// Scale of the first channel/group, for interoperability with the legacy
69    /// scalar-returning API. Only exact for [`QParams::PerTensor`].
70    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    /// Zero point of the first channel/group. Only exact for
80    /// [`QParams::PerTensor`].
81    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/// A quantized tensor bundled with the complete parameter set needed to invert
92/// the quantization.
93#[derive(Debug, Clone)]
94pub struct QuantizedTensor {
95    /// Integer codes stored in an `f32` tensor (values are exact integers).
96    pub tensor: Tensor,
97    /// Parameters required for dequantization.
98    pub params: QParams,
99    /// Target integer dtype the codes belong to.
100    pub dtype: DType,
101}
102
103impl QuantizedTensor {
104    /// Dequantize back to floating point using the full parameter set.
105    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
125/// Row-major strides for a shape.
126fn 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
134/// Dequantize a tensor quantized with one scale/zero-point per channel.
135pub 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
169/// Dequantize a tensor quantized with one scale/zero-point per channel group.
170pub 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
212/// Quantize a tensor using the supplied configuration, preserving the complete
213/// per-channel / per-group parameter set.
214///
215/// Prefer this over [`quantize_with_config`] whenever the configuration uses a
216/// per-channel, INT4-per-channel or group-wise scheme: the scalar-returning
217/// variant can only report the first channel's parameters, which cannot invert
218/// the other channels.
219pub 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
299/// Quantize a tensor using specified configuration.
300///
301/// # Parameter loss for per-channel schemes
302///
303/// The scalar `(scale, zero_point)` return type can only carry a single
304/// parameter pair. For `PerChannelAffine`, `PerChannelSymmetric`,
305/// `Int4PerChannel` and `GroupWise` the returned pair belongs to the **first**
306/// channel/group only and will not dequantize the remaining ones. Use
307/// [`quantize_with_config_full`] for those schemes.
308pub 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            // Return first channel's parameters for compatibility
323            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            // Mixed precision requires different handling
339            Err(TorshError::InvalidArgument(
340                "Mixed precision quantization requires specialized API".to_string(),
341            ))
342        }
343    }
344}
345
346/// Quantize a tensor to INT8 using specified scale and zero point
347pub 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
357/// Dequantize a quantized tensor using scale and zero_point
358pub fn dequantize(tensor: &Tensor, scale: f32, zero_point: i32) -> TorshResult<Tensor> {
359    dequantize_per_tensor_affine(tensor, scale, zero_point)
360}
361
362/// Auto-quantize a tensor using per-tensor scheme
363pub 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    // Calculate min and max values using SIMD acceleration when beneficial
377    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        // Fallback for small tensors
381        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    // Determine quantization parameters based on scheme
387    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
403/// Auto-quantize a tensor using per-channel scheme
404pub 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    // Calculate strides for efficient channel access
423    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    // Process each channel
433    for ch in 0..num_channels {
434        let mut channel_min = f32::INFINITY;
435        let mut channel_max = f32::NEG_INFINITY;
436
437        // Calculate channel statistics
438        let mut indices = vec![0; shape.len()];
439        let channel_size = data.len() / num_channels;
440
441        for i in 0..channel_size {
442            // Calculate multi-dimensional index for this channel
443            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            // Convert multi-dimensional index to flat index
455            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        // Calculate quantization parameters for this channel
469        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        // Quantize channel data
488        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
522/// Quantize a tensor using per-tensor affine quantization (returns I8 tensor)
523pub 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            // Clamp to int8 range and convert to i8
541            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
554/// Quantize a tensor using per-tensor affine quantization into the `I8` range.
555///
556/// This is the `DType::I8` specialisation of
557/// [`quantize_per_tensor_affine_dtype`]; use that function to target `U8`,
558/// `I16` or `I32`, whose ranges differ.
559pub 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
567/// Quantize a tensor using per-tensor affine quantization for a specific target dtype.
568///
569/// The quantized codes are clamped to the target dtype's range (`[0, 255]` for
570/// `U8`, `[-128, 127]` for `I8`, ...) rather than always to the `I8` range, and
571/// the zero point is validated against that same range.
572pub 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    // Use SIMD-accelerated quantization when available and beneficial
594    let mut quantized_data = vec![0.0f32; data.len()];
595    if data.len() > 64 && crate::simd_ops::is_simd_available() {
596        // Use SIMD for larger tensors
597        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        // Fallback to scalar implementation for small tensors
607        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
623/// Dequantize a tensor using per-tensor affine dequantization
624pub 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    // Use SIMD-accelerated dequantization when available and beneficial
638    let mut dequantized_data = vec![0.0f32; data.len()];
639    if data.len() > 64 && crate::simd_ops::is_simd_available() {
640        // Use SIMD for larger tensors
641        crate::simd_ops::dequantize_per_tensor_affine_simd(
642            &data,
643            scale,
644            zero_point,
645            &mut dequantized_data,
646        )?;
647    } else {
648        // Fallback to scalar implementation for small tensors
649        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
663/// Calculate affine quantization parameters (scale and zero_point)
664pub 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    // Handle edge case where min == max
686    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    // Calculate scale
693    let scale = (max_val - min_val) / (qmax - qmin);
694
695    // Calculate zero_point
696    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
702/// Calculate symmetric quantization parameters.
703///
704/// The scale is `max(|min|, |max|) / (qmax - zero_point)`.
705///
706/// * For signed targets (`I8`, `I16`, `I32`) the zero point is `0`, so the
707///   scale reduces to `max_abs / qmax`.
708/// * For unsigned targets (`U8`) a zero point of `0` would clip every negative
709///   value; the zero point is therefore placed at the midpoint of the range
710///   (`128` for `U8`), matching PyTorch's `quint8` symmetric convention.
711pub 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    // Unsigned target types cannot represent negative codes, so the symmetric
726    // zero is placed at the midpoint of the range (the PyTorch `quint8`
727    // convention: zero_point = 128 for U8) instead of at 0.
728    let zero_point = if qmin >= 0 {
729        (((qmin as i64) + (qmax as i64) + 1) / 2) as i32
730    } else {
731        0
732    };
733
734    // Handle edge case where range is zero
735    if abs_max < f32::EPSILON {
736        return Ok((1.0, zero_point));
737    }
738
739    // For symmetric quantization, we use the maximum absolute value
740    // and map it to the largest magnitude representable above the zero point.
741    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
747/// Get the quantization range for a given data type
748pub 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), // Default to int8 range
755    }
756}
757
758/// Convenience function to quantize with automatic parameter calculation
759pub fn quantize_auto(tensor: &Tensor, config: &QuantConfig) -> TorshResult<(Tensor, f32, i32)> {
760    quantize_with_config(tensor, config)
761}
762
763// ===== Cache-Aware Algorithm Enhancements =====
764
765/// Cache-aware quantization parameters for optimal memory access patterns
766#[derive(Debug, Clone)]
767pub struct CacheAwareParams {
768    /// Cache line size in bytes (typically 64 bytes)
769    pub cache_line_size: usize,
770    /// L1 cache size in bytes (typically 32KB)
771    pub l1_cache_size: usize,
772    /// L2 cache size in bytes (typically 256KB)
773    pub l2_cache_size: usize,
774    /// L3 cache size in bytes (typically 8MB)
775    pub l3_cache_size: usize,
776    /// Prefetch distance (elements ahead to prefetch)
777    pub prefetch_distance: usize,
778    /// Enable cache-optimized chunking
779    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,       // 32KB L1
787            l2_cache_size: 256 * 1024,      // 256KB L2
788            l3_cache_size: 8 * 1024 * 1024, // 8MB L3
789            prefetch_distance: 16,
790            enable_chunking: true,
791        }
792    }
793}
794
795/// Cache-aware per-tensor quantization optimized for memory hierarchy
796pub 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        // Direct processing for small arrays
820        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    // Calculate optimal chunk size based on cache hierarchy
828    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    // Process in cache-friendly chunks
833    input
834        .par_chunks(optimal_chunk_size)
835        .zip(output.par_chunks_mut(optimal_chunk_size))
836        .for_each(|(input_chunk, output_chunk)| {
837            // Process chunk with cache-friendly pattern
838            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
847/// Cache-optimized tensor statistics calculation with blocking
848pub 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    // Use blocked algorithm for better cache performance
862    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            // Process block with good cache locality
871            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    // Combine results
884    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
903/// Cache-friendly matrix quantization using tiling for 2D tensors
904pub 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    // Calculate optimal tile sizes based on cache hierarchy
929    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    // Find good tile dimensions that fit in L2 cache
933    let max_tile_size = (l2_elements / 4).min(1024); // Reserve space for other data
934    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    // Process matrix in cache-friendly tiles
940    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            // Process tile with good spatial locality
947            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
960/// Prefetch-aware sequential quantization for streaming data
961pub 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    // Sequential processing with software prefetching
985    for i in 0..input.len() {
986        // Software prefetch hint for future data (compiler may optimize this)
987        if i + prefetch_distance < input.len() {
988            // This is a hint to the processor - actual prefetch intrinsics would be platform-specific
989            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
999/// Get cache-aware optimization recommendations for tensor operations
1000pub 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
1033/// Auto-select optimal quantization algorithm based on cache analysis
1034pub 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        // Small data - use simple sequential processing
1047        quantize_streaming_with_prefetch(input, scale, zero_point, output, params)
1048    } else if total_bytes <= params.l2_cache_size {
1049        // Medium data - use cache-aware chunking
1050        quantize_per_tensor_affine_cache_aware(input, scale, zero_point, output, params)
1051    } else {
1052        // Large data - use parallel processing with cache-friendly chunks
1053        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        // Test normal case
1067        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        // Test edge case: min == max
1074        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        // Test invalid case: min > max
1081        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        // Test normal case
1088        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); // Symmetric always has zero_point = 0
1093
1094        // Test edge case: zero range
1095        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        // Verify quantization: (value / scale) + zero_point
1119        assert_eq!(quantized_data[0], 10.0); // (1.0 / 0.1) + 0 = 10
1120        assert_eq!(quantized_data[1], 20.0); // (2.0 / 0.1) + 0 = 20
1121        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        // Verify dequantization: (quantized_value - zero_point) * scale
1134        assert!((dequantized_data[0] - 1.0).abs() < 1e-6); // (10 - 0) * 0.1 = 1.0
1135        assert!((dequantized_data[1] - 2.0).abs() < 1e-6); // (20 - 0) * 0.1 = 2.0
1136    }
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        // Verify tensor was quantized
1150        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        // Test dequantization with scale 0.5
1178        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        // Test invalid scale
1201        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        // Test empty tensor
1211        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    // ===== Cache-Aware Algorithm Tests =====
1219
1220    #[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); // 1.0 / 0.1 = 10
1260        assert_eq!(output[8], 90.0); // 9.0 / 0.1 = 90
1261    }
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        // 0.1 / 0.01 + 10 = 10 + 10 = 20
1273        assert_eq!(output[0], 20.0);
1274    }
1275
1276    #[test]
1277    fn test_cache_optimization_recommendations() {
1278        let cache_params = CacheAwareParams::default();
1279
1280        // Small tensor (L1 cache)
1281        let recommendations = get_cache_optimization_recommendations(1000, 4, &cache_params);
1282        assert!(!recommendations.is_empty());
1283        assert!(recommendations[0].contains("L1 cache"));
1284
1285        // Large tensor (beyond L3)
1286        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        // Test with default cache parameters
1299        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]; // Wrong size
1322        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        // Test invalid scale
1329        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        // Test empty data for stats
1340        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}