Skip to main content

torsh_quantization/
specialized.rs

1//! Specialized quantization algorithms for advanced use cases
2//!
3//! This module provides advanced quantization techniques beyond standard INT8 quantization,
4//! including low-bit quantization, extreme compression methods, and adaptive precision.
5//!
6//! # Features
7//!
8//! - **INT4 Quantization**: 4-bit quantization for extreme compression
9//! - **Binary Quantization**: 1-bit quantization using {-1, +1} values
10//! - **Ternary Quantization**: 2-bit quantization using {-1, 0, +1} values
11//! - **Group-wise Quantization**: Channel grouping for improved accuracy
12//! - **Mixed Precision**: Layer-specific precision assignment
13//! - **Adaptive Thresholding**: Smart threshold selection for extreme quantization
14
15use crate::config::{MixedPrecisionConfig, QuantConfig};
16
17#[cfg(feature = "std")]
18use std::collections::HashMap;
19
20#[cfg(not(feature = "std"))]
21extern crate alloc;
22
23#[cfg(not(feature = "std"))]
24use alloc::{collections::BTreeMap as HashMap, string::String, vec::Vec};
25
26use torsh_core::{
27    dtype::DType,
28    error::{Result as TorshResult, TorshError},
29};
30use torsh_tensor::Tensor;
31
32/// INT4 quantization (4-bit per tensor)
33pub fn quantize_int4_per_tensor(
34    tensor: &Tensor,
35    _config: &QuantConfig,
36) -> TorshResult<(Tensor, f32, i32)> {
37    let data = tensor.data()?;
38    let min_val = data.iter().fold(f32::INFINITY, |a, &b| a.min(b)).min(0.0);
39    let max_val = data
40        .iter()
41        .fold(f32::NEG_INFINITY, |a, &b| a.max(b))
42        .max(0.0);
43
44    // INT4 range: -8 to 7
45    let scale = (max_val - min_val) / 15.0; // 15 = 7 - (-8)
46    let scale = if scale == 0.0 { 1.0 } else { scale };
47
48    let zero_point = (-8.0 - min_val / scale).round().clamp(-8.0, 7.0) as i32;
49
50    let quantized_data: Vec<f32> = data
51        .iter()
52        .map(|&x| {
53            let quantized = (x / scale).round() + zero_point as f32;
54            quantized.clamp(-8.0, 7.0) // Store as f32 for compatibility
55        })
56        .collect();
57
58    let quantized_tensor = Tensor::from_data(
59        quantized_data,
60        tensor.shape().dims().to_vec(),
61        tensor.device(),
62    )?;
63
64    Ok((quantized_tensor, scale, zero_point))
65}
66
67/// INT4 per-channel quantization preserving the full per-channel parameter set.
68///
69/// Returns the quantized tensor together with one scale and one zero point per
70/// channel along `axis`, so the transform can be inverted with
71/// [`crate::algorithms::dequantize_per_channel`].
72pub fn quantize_int4_per_channel_full(
73    tensor: &Tensor,
74    axis: usize,
75    _config: &QuantConfig,
76) -> TorshResult<(Tensor, Vec<f32>, Vec<i32>)> {
77    let binding = tensor.shape();
78    let shape = binding.dims();
79
80    if axis >= shape.len() {
81        return Err(TorshError::InvalidArgument(
82            "Axis out of bounds".to_string(),
83        ));
84    }
85
86    let num_channels = shape[axis];
87    let data = tensor.data()?;
88
89    // Calculate strides for efficient channel access
90    let mut strides = vec![1; shape.len()];
91    for i in (0..shape.len().saturating_sub(1)).rev() {
92        strides[i] = strides[i + 1] * shape[i + 1];
93    }
94
95    // Single pass over the data to gather per-channel statistics.
96    let mut channel_mins = vec![f32::INFINITY; num_channels];
97    let mut channel_maxs = vec![f32::NEG_INFINITY; num_channels];
98    for (i, &val) in data.iter().enumerate() {
99        let ch = (i / strides[axis]) % num_channels;
100        channel_mins[ch] = channel_mins[ch].min(val);
101        channel_maxs[ch] = channel_maxs[ch].max(val);
102    }
103
104    let mut scales = Vec::with_capacity(num_channels);
105    let mut zero_points = Vec::with_capacity(num_channels);
106
107    for ch in 0..num_channels {
108        // Ensure the range always contains zero
109        let channel_min = channel_mins[ch].min(0.0);
110        let channel_max = channel_maxs[ch].max(0.0);
111
112        // Calculate INT4 quantization parameters for this channel
113        let scale = (channel_max - channel_min) / 15.0; // INT4 range: -8 to 7
114        let scale = if scale == 0.0 { 1.0 } else { scale };
115        let zero_point = (-8.0 - channel_min / scale).round().clamp(-8.0, 7.0) as i32;
116
117        scales.push(scale);
118        zero_points.push(zero_point);
119    }
120
121    // Quantize using each element's own channel parameters
122    let quantized_data: Vec<f32> = data
123        .iter()
124        .enumerate()
125        .map(|(i, &val)| {
126            let ch = (i / strides[axis]) % num_channels;
127            let quantized = (val / scales[ch]).round() + zero_points[ch] as f32;
128            quantized.clamp(-8.0, 7.0)
129        })
130        .collect();
131
132    let quantized_tensor = Tensor::from_data(quantized_data, shape.to_vec(), tensor.device())?;
133
134    Ok((quantized_tensor, scales, zero_points))
135}
136
137/// INT4 per-channel quantization (legacy scalar-parameter API).
138///
139/// # Parameter loss
140///
141/// The returned `(scale, zero_point)` pair is the arithmetic **mean** of the
142/// per-channel parameters and cannot invert the quantization. Use
143/// [`quantize_int4_per_channel_full`] when the result has to be dequantized.
144pub fn quantize_int4_per_channel(
145    tensor: &Tensor,
146    axis: usize,
147    config: &QuantConfig,
148) -> TorshResult<(Tensor, f32, i32)> {
149    let (quantized_tensor, scales, zero_points) =
150        quantize_int4_per_channel_full(tensor, axis, config)?;
151
152    // Return average parameters for compatibility
153    let avg_scale = scales.iter().sum::<f32>() / scales.len() as f32;
154    let avg_zero_point =
155        (zero_points.iter().sum::<i32>() as f32 / zero_points.len() as f32).round() as i32;
156
157    Ok((quantized_tensor, avg_scale, avg_zero_point))
158}
159
160/// Binary quantization (-1, +1)
161pub fn quantize_binary(tensor: &Tensor) -> TorshResult<(Tensor, f32, i32)> {
162    let data = tensor.data()?;
163
164    if data.is_empty() {
165        return Err(TorshError::InvalidArgument(
166            "Cannot quantize empty tensor".to_string(),
167        ));
168    }
169
170    // Calculate scale as the mean of absolute values
171    let scale = data.iter().map(|&x| x.abs()).sum::<f32>() / data.len() as f32;
172    let scale = if scale == 0.0 { 1.0 } else { scale };
173
174    let quantized_data: Vec<f32> = data
175        .iter()
176        .map(|&x| if x >= 0.0 { 1.0 } else { -1.0 })
177        .collect();
178
179    let quantized_tensor = Tensor::from_data(
180        quantized_data,
181        tensor.shape().dims().to_vec(),
182        tensor.device(),
183    )?;
184
185    Ok((quantized_tensor, scale, 0)) // Binary is symmetric, so zero_point = 0
186}
187
188/// Ternary quantization (-1, 0, +1)
189pub fn quantize_ternary(tensor: &Tensor) -> TorshResult<(Tensor, f32, i32)> {
190    let data = tensor.data()?;
191
192    if data.is_empty() {
193        return Err(TorshError::InvalidArgument(
194            "Cannot quantize empty tensor".to_string(),
195        ));
196    }
197
198    // Calculate threshold as fraction of max absolute value
199    let max_abs = data.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
200    let threshold = max_abs * 0.7; // Threshold parameter
201
202    // Calculate scale from non-zero values
203    let non_zero_sum: f32 = data
204        .iter()
205        .filter(|&&x| x.abs() > threshold)
206        .map(|&x| x.abs())
207        .sum();
208    let non_zero_count = data.iter().filter(|&&x| x.abs() > threshold).count();
209
210    let scale = if non_zero_count > 0 {
211        non_zero_sum / non_zero_count as f32
212    } else {
213        1.0
214    };
215
216    let quantized_data: Vec<f32> = data
217        .iter()
218        .map(|&x| {
219            if x.abs() <= threshold {
220                0.0
221            } else if x > 0.0 {
222                1.0
223            } else {
224                -1.0
225            }
226        })
227        .collect();
228
229    let quantized_tensor = Tensor::from_data(
230        quantized_data,
231        tensor.shape().dims().to_vec(),
232        tensor.device(),
233    )?;
234
235    Ok((quantized_tensor, scale, 0)) // Ternary is symmetric, so zero_point = 0
236}
237
238/// Group-wise quantization preserving the full per-group parameter set.
239///
240/// Channels along `axis` are split into contiguous groups of `group_size`
241/// channels. The returned vectors hold one scale and one zero point per group
242/// (`ceil(shape[axis] / group_size)` entries), so the result can be inverted
243/// with [`crate::algorithms::dequantize_per_group`].
244pub fn quantize_group_wise_full(
245    tensor: &Tensor,
246    axis: usize,
247    group_size: usize,
248    config: &QuantConfig,
249) -> TorshResult<(Tensor, Vec<f32>, Vec<i32>)> {
250    let binding = tensor.shape();
251    let shape = binding.dims();
252
253    if axis >= shape.len() {
254        return Err(TorshError::InvalidArgument(
255            "Axis out of bounds".to_string(),
256        ));
257    }
258
259    if group_size == 0 {
260        return Err(TorshError::InvalidArgument(
261            "Group size must be greater than 0".to_string(),
262        ));
263    }
264
265    let num_channels = shape[axis];
266    let num_groups = num_channels.div_ceil(group_size); // Ceiling division
267
268    let data = tensor.data()?;
269
270    // Calculate strides for indexing (optimized version)
271    let mut strides = vec![1; shape.len()];
272    for i in (0..shape.len().saturating_sub(1)).rev() {
273        strides[i] = strides[i + 1] * shape[i + 1];
274    }
275
276    // Single pass to gather per-group statistics.
277    let mut group_mins = vec![f32::INFINITY; num_groups];
278    let mut group_maxs = vec![f32::NEG_INFINITY; num_groups];
279    for (i, &val) in data.iter().enumerate() {
280        let group = ((i / strides[axis]) % num_channels) / group_size;
281        group_mins[group] = group_mins[group].min(val);
282        group_maxs[group] = group_maxs[group].max(val);
283    }
284
285    let (qmin, qmax) = config.get_qint_range();
286    let mut group_scales = Vec::with_capacity(num_groups);
287    let mut group_zero_points = Vec::with_capacity(num_groups);
288
289    for group in 0..num_groups {
290        // Empty groups (possible only for an empty tensor) get identity params.
291        let (min_val, max_val) = if group_mins[group].is_finite() {
292            (group_mins[group].min(0.0), group_maxs[group].max(0.0))
293        } else {
294            (0.0, 0.0)
295        };
296
297        let scale = (max_val - min_val) / (qmax - qmin) as f32;
298        let scale = if scale == 0.0 { 1.0 } else { scale };
299
300        let zero_point = (qmin as f32 - min_val / scale)
301            .round()
302            .max(qmin as f32)
303            .min(qmax as f32) as i32;
304
305        group_scales.push(scale);
306        group_zero_points.push(zero_point);
307    }
308
309    // Quantize each element with its own group's parameters.
310    let quantized_data: Vec<f32> = data
311        .iter()
312        .enumerate()
313        .map(|(i, &val)| {
314            let group = ((i / strides[axis]) % num_channels) / group_size;
315            let quantized = (val / group_scales[group]).round() + group_zero_points[group] as f32;
316            quantized.clamp(qmin as f32, qmax as f32)
317        })
318        .collect();
319
320    let quantized_tensor = Tensor::from_data(quantized_data, shape.to_vec(), tensor.device())?;
321
322    Ok((quantized_tensor, group_scales, group_zero_points))
323}
324
325/// Group-wise quantization (legacy scalar-parameter API).
326///
327/// # Parameter loss
328///
329/// The returned `(scale, zero_point)` pair is the arithmetic **mean** of the
330/// per-group parameters and cannot invert the quantization. Use
331/// [`quantize_group_wise_full`] when the result has to be dequantized.
332pub fn quantize_group_wise(
333    tensor: &Tensor,
334    axis: usize,
335    group_size: usize,
336    config: &QuantConfig,
337) -> TorshResult<(Tensor, f32, i32)> {
338    let (quantized_tensor, group_scales, group_zero_points) =
339        quantize_group_wise_full(tensor, axis, group_size, config)?;
340
341    // Return average scale and zero_point for compatibility
342    let avg_scale = if group_scales.is_empty() {
343        1.0
344    } else {
345        group_scales.iter().sum::<f32>() / group_scales.len() as f32
346    };
347    let avg_zero_point = if group_zero_points.is_empty() {
348        0
349    } else {
350        (group_zero_points.iter().sum::<i32>() as f32 / group_zero_points.len() as f32).round()
351            as i32
352    };
353
354    Ok((quantized_tensor, avg_scale, avg_zero_point))
355}
356
357/// Mixed precision quantization for different layers
358pub fn quantize_mixed_precision(
359    tensors: &HashMap<String, Tensor>,
360    config: &MixedPrecisionConfig,
361) -> TorshResult<HashMap<String, (Tensor, f32, i32)>> {
362    let mut results = HashMap::new();
363
364    for (layer_name, tensor) in tensors {
365        // Determine precision for this layer
366        let precision = determine_layer_precision(layer_name, config);
367
368        // Create quantization config for this precision
369        let layer_config = create_precision_config(precision);
370
371        // Quantize using appropriate scheme
372        let result = crate::algorithms::quantize_with_config(tensor, &layer_config)?;
373        results.insert(layer_name.clone(), result);
374    }
375
376    Ok(results)
377}
378
379/// Determine precision for a layer based on mixed precision config
380pub fn determine_layer_precision(layer_name: &str, config: &MixedPrecisionConfig) -> DType {
381    // Check exact matches first
382    for (pattern, precision) in &config.layer_precision {
383        if layer_name.contains(pattern) {
384            return *precision;
385        }
386    }
387
388    // Return default precision
389    config.default_precision
390}
391
392/// Create quantization config for specific precision
393pub fn create_precision_config(precision: DType) -> QuantConfig {
394    match precision {
395        DType::I8 => QuantConfig::int8(),
396        DType::U8 => QuantConfig::uint8(),
397        DType::F16 => {
398            // For FP16, we don't quantize but use reduced precision
399            QuantConfig {
400                dtype: DType::F16,
401                enable_fake_quant: false,
402                ..Default::default()
403            }
404        }
405        DType::F32 => {
406            // Keep full precision
407            QuantConfig {
408                dtype: DType::F32,
409                enable_fake_quant: false,
410                ..Default::default()
411            }
412        }
413        _ => QuantConfig::int8(), // Default fallback
414    }
415}
416
417/// Advanced binary quantization with learned threshold
418pub fn quantize_binary_learned_threshold(
419    tensor: &Tensor,
420    threshold: Option<f32>,
421) -> TorshResult<(Tensor, f32, i32, f32)> {
422    let data = tensor.data()?;
423
424    if data.is_empty() {
425        return Err(TorshError::InvalidArgument(
426            "Cannot quantize empty tensor".to_string(),
427        ));
428    }
429
430    // Use provided threshold or learn it
431    let threshold = threshold.unwrap_or_else(|| {
432        // Simple threshold learning: mean of absolute values
433        let abs_sum: f32 = data.iter().map(|&x| x.abs()).sum();
434        abs_sum / data.len() as f32
435    });
436
437    // Calculate scale from values above threshold
438    let above_threshold: Vec<f32> = data
439        .iter()
440        .filter(|&&x| x.abs() > threshold)
441        .cloned()
442        .collect();
443
444    let scale = if above_threshold.is_empty() {
445        1.0
446    } else {
447        above_threshold.iter().map(|&x| x.abs()).sum::<f32>() / above_threshold.len() as f32
448    };
449
450    let quantized_data: Vec<f32> = data
451        .iter()
452        .map(|&x| {
453            if x.abs() <= threshold {
454                0.0
455            } else if x >= 0.0 {
456                1.0
457            } else {
458                -1.0
459            }
460        })
461        .collect();
462
463    let quantized_tensor = Tensor::from_data(
464        quantized_data,
465        tensor.shape().dims().to_vec(),
466        tensor.device(),
467    )?;
468
469    Ok((quantized_tensor, scale, 0, threshold))
470}
471
472/// Adaptive ternary quantization with optimal threshold selection
473pub fn quantize_ternary_adaptive(tensor: &Tensor) -> TorshResult<(Tensor, f32, i32, f32)> {
474    let data = tensor.data()?;
475
476    if data.is_empty() {
477        return Err(TorshError::InvalidArgument(
478            "Cannot quantize empty tensor".to_string(),
479        ));
480    }
481
482    // Find optimal threshold using search
483    let max_abs = data.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
484    let mut best_threshold = 0.0;
485    let mut best_error = f32::INFINITY;
486
487    // Search for optimal threshold
488    for i in 1..=10 {
489        let threshold = max_abs * (i as f32 * 0.1);
490        let error = calculate_ternary_error(&data, threshold);
491        if error < best_error {
492            best_error = error;
493            best_threshold = threshold;
494        }
495    }
496
497    // Apply quantization with best threshold
498    let non_zero_sum: f32 = data
499        .iter()
500        .filter(|&&x| x.abs() > best_threshold)
501        .map(|&x| x.abs())
502        .sum();
503    let non_zero_count = data.iter().filter(|&&x| x.abs() > best_threshold).count();
504
505    let scale = if non_zero_count > 0 {
506        non_zero_sum / non_zero_count as f32
507    } else {
508        1.0
509    };
510
511    let quantized_data: Vec<f32> = data
512        .iter()
513        .map(|&x| {
514            if x.abs() <= best_threshold {
515                0.0
516            } else if x > 0.0 {
517                1.0
518            } else {
519                -1.0
520            }
521        })
522        .collect();
523
524    let quantized_tensor = Tensor::from_data(
525        quantized_data,
526        tensor.shape().dims().to_vec(),
527        tensor.device(),
528    )?;
529
530    Ok((quantized_tensor, scale, 0, best_threshold))
531}
532
533/// Calculate quantization error for ternary quantization with given threshold
534fn calculate_ternary_error(data: &[f32], threshold: f32) -> f32 {
535    data.iter()
536        .map(|&x| {
537            let quantized = if x.abs() <= threshold {
538                0.0
539            } else if x > 0.0 {
540                1.0
541            } else {
542                -1.0
543            };
544            (x - quantized).powi(2)
545        })
546        .sum::<f32>()
547        / data.len() as f32
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553    use torsh_core::device::DeviceType;
554    use torsh_tensor::creation::tensor_1d;
555
556    #[test]
557    fn test_quantize_int4_per_tensor() {
558        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
559        let tensor = tensor_1d(&data).unwrap();
560        let config = QuantConfig::int4();
561
562        let result = quantize_int4_per_tensor(&tensor, &config);
563        assert!(result.is_ok());
564
565        let (quantized, scale, zero_point) = result.unwrap();
566        assert!(scale > 0.0);
567        assert!(zero_point >= -8 && zero_point <= 7);
568
569        let quantized_data = quantized.data().unwrap();
570        assert_eq!(quantized_data.len(), data.len());
571
572        // Check that values are in INT4 range
573        for &val in &quantized_data {
574            assert!(val >= -8.0 && val <= 7.0);
575        }
576    }
577
578    #[test]
579    fn test_quantize_binary() {
580        let data = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
581        let tensor = tensor_1d(&data).unwrap();
582
583        let result = quantize_binary(&tensor);
584        assert!(result.is_ok());
585
586        let (quantized, scale, zero_point) = result.unwrap();
587        assert!(scale > 0.0);
588        assert_eq!(zero_point, 0); // Binary is symmetric
589
590        let quantized_data = quantized.data().unwrap();
591        assert_eq!(quantized_data.len(), data.len());
592
593        // Check that all values are either -1.0 or 1.0
594        for &val in &quantized_data {
595            assert!(val == -1.0 || val == 1.0);
596        }
597    }
598
599    #[test]
600    fn test_quantize_ternary() {
601        let data = vec![-3.0, -1.0, 0.1, 1.0, 3.0];
602        let tensor = tensor_1d(&data).unwrap();
603
604        let result = quantize_ternary(&tensor);
605        assert!(result.is_ok());
606
607        let (quantized, scale, zero_point) = result.unwrap();
608        assert!(scale > 0.0);
609        assert_eq!(zero_point, 0); // Ternary is symmetric
610
611        let quantized_data = quantized.data().unwrap();
612        assert_eq!(quantized_data.len(), data.len());
613
614        // Check that all values are -1.0, 0.0, or 1.0
615        for &val in &quantized_data {
616            assert!(val == -1.0 || val == 0.0 || val == 1.0);
617        }
618    }
619
620    #[test]
621    fn test_quantize_group_wise() {
622        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
623        let tensor = Tensor::from_data(data, vec![2, 3], DeviceType::Cpu).unwrap();
624        let config = QuantConfig::group_wise(1, 2);
625
626        let result = quantize_group_wise(&tensor, 1, 2, &config);
627        assert!(result.is_ok());
628
629        let (quantized, scale, _zero_point) = result.unwrap();
630        assert!(scale > 0.0);
631        assert_eq!(quantized.shape().dims(), tensor.shape().dims());
632    }
633
634    #[test]
635    fn test_mixed_precision() {
636        let mut tensors = HashMap::new();
637        tensors.insert(
638            "embedding".to_string(),
639            tensor_1d(&[1.0, 2.0, 3.0]).unwrap(),
640        );
641        tensors.insert(
642            "attention".to_string(),
643            tensor_1d(&[4.0, 5.0, 6.0]).unwrap(),
644        );
645
646        let config = MixedPrecisionConfig::default();
647
648        let result = quantize_mixed_precision(&tensors, &config);
649        assert!(result.is_ok());
650
651        let results = result.unwrap();
652        assert_eq!(results.len(), 2);
653        assert!(results.contains_key("embedding"));
654        assert!(results.contains_key("attention"));
655    }
656
657    #[test]
658    fn test_determine_layer_precision() {
659        let config = MixedPrecisionConfig::default();
660
661        let embedding_precision = determine_layer_precision("layer.embedding.weight", &config);
662        assert_eq!(embedding_precision, DType::I8);
663
664        let attention_precision = determine_layer_precision("layer.attention.query", &config);
665        assert_eq!(attention_precision, DType::F16);
666
667        let unknown_precision = determine_layer_precision("layer.unknown.weight", &config);
668        assert_eq!(unknown_precision, DType::I8); // Default
669    }
670
671    #[test]
672    fn test_binary_learned_threshold() {
673        let data = vec![-2.0, -0.1, 0.1, 0.5, 2.0];
674        let tensor = tensor_1d(&data).unwrap();
675
676        let result = quantize_binary_learned_threshold(&tensor, Some(0.3));
677        assert!(result.is_ok());
678
679        let (quantized, scale, zero_point, threshold) = result.unwrap();
680        assert!(scale > 0.0);
681        assert_eq!(zero_point, 0);
682        assert_eq!(threshold, 0.3);
683
684        let quantized_data = quantized.data().unwrap();
685
686        // Values below threshold should become 0, others ±1
687        for (i, &original) in data.iter().enumerate() {
688            let expected = if original.abs() <= 0.3 {
689                0.0
690            } else if original >= 0.0 {
691                1.0
692            } else {
693                -1.0
694            };
695            assert_eq!(quantized_data[i], expected);
696        }
697    }
698
699    #[test]
700    fn test_ternary_adaptive() {
701        let data = vec![-3.0, -0.5, 0.0, 0.5, 3.0];
702        let tensor = tensor_1d(&data).unwrap();
703
704        let result = quantize_ternary_adaptive(&tensor);
705        assert!(result.is_ok());
706
707        let (quantized, scale, zero_point, threshold) = result.unwrap();
708        assert!(scale > 0.0);
709        assert_eq!(zero_point, 0);
710        assert!(threshold > 0.0);
711
712        let quantized_data = quantized.data().unwrap();
713        assert_eq!(quantized_data.len(), data.len());
714
715        // All values should be -1, 0, or 1
716        for &val in &quantized_data {
717            assert!(val == -1.0 || val == 0.0 || val == 1.0);
718        }
719    }
720
721    #[test]
722    fn test_error_cases() {
723        // Test empty tensor
724        let empty_data: Vec<f32> = vec![];
725        let empty_tensor = tensor_1d(&empty_data).unwrap();
726
727        assert!(quantize_binary(&empty_tensor).is_err());
728        assert!(quantize_ternary(&empty_tensor).is_err());
729
730        // Test invalid axis
731        let data = vec![1.0, 2.0, 3.0];
732        let tensor = tensor_1d(&data).unwrap();
733        let config = QuantConfig::group_wise(0, 2);
734
735        let result = quantize_group_wise(&tensor, 5, 2, &config);
736        assert!(result.is_err());
737
738        // Test zero group size
739        let result = quantize_group_wise(&tensor, 0, 0, &config);
740        assert!(result.is_err());
741    }
742}