Skip to main content

burn_flex/ops/
qtensor.rs

1//! Quantized tensor operations for the Flex backend.
2
3use alloc::vec::Vec;
4#[cfg(not(feature = "std"))]
5#[allow(unused_imports)]
6use num_traits::Float;
7
8use burn_backend::{
9    DType, ExecutionError, FloatDType, TensorData, TensorMetadata,
10    ops::{IntTensorOps, QTensorOps},
11    quantization::{
12        BlockLayout, BlockSize, QuantScheme, QuantizationParametersPrimitive, QuantizedBytes,
13        ScaleDtype, global_scale_dtype, scale_to_dtype,
14    },
15    tensor::{Device, FloatTensor, IntTensor, QuantizedTensor},
16};
17use burn_std::{Bytes, Shape, Slice, bf16, f16};
18
19use super::float_storage_as_f32;
20use crate::{Flex, FlexQTensor, FlexTensor, Layout, quant_codes::QuantCodes};
21
22/// The blocks over `shape`, which must be a whole number of blocks along every axis.
23fn block_layout(shape: &Shape, block: &BlockSize) -> BlockLayout {
24    let blocks = BlockLayout::new(shape, block);
25    assert!(
26        blocks.divides(),
27        "tensor {shape:?} is not a whole number of {block:?} blocks"
28    );
29    blocks
30}
31
32/// The largest magnitude in each block of `values`, laid out as `blocks`.
33fn block_max_abs(values: &[f32], blocks: &BlockLayout) -> Vec<f32> {
34    let mut peaks = alloc::vec![0.0f32; blocks.num_blocks()];
35    for (index, &x) in values.iter().enumerate() {
36        let peak = &mut peaks[blocks.block_of(index)];
37        *peak = peak.max(x.abs());
38    }
39    peaks
40}
41
42impl QTensorOps<Flex> for Flex {
43    fn q_from_data(data: TensorData, _device: &Device<Flex>) -> QuantizedTensor<Flex> {
44        let scheme = match data.dtype() {
45            DType::QFloat(scheme) => scheme,
46            _ => panic!("Expected quantized dtype, got {:?}", data.dtype()),
47        };
48
49        let shape = data.shape().clone();
50
51        let q_bytes = QuantizedBytes {
52            shape: shape.clone(),
53            bytes: data.into_bytes(),
54            scheme,
55        };
56
57        let (values, qparams) = q_bytes.into_vec_i8();
58        let tensor_data = TensorData::new(values, shape);
59        let tensor = FlexTensor::from_data(tensor_data);
60
61        FlexQTensor::new(tensor, scheme, qparams.block, qparams.global)
62    }
63
64    fn quantize_dynamic(tensor: FloatTensor<Flex>, scheme: &QuantScheme) -> QuantizedTensor<Flex> {
65        let shape = tensor.shape();
66        let tensor = tensor.to_contiguous();
67        let float_data = float_storage_as_f32(&tensor);
68        let (min, max) = scheme.value.range();
69        let range = max - min;
70
71        let (quantized, scales, global) = match (scheme.block_size(), global_scale_dtype(scheme)) {
72            (Some(block), Some(global_dtype)) => {
73                let blocks = block_layout(&shape, &block);
74                let raw: Vec<f32> = block_max_abs(&float_data, &blocks)
75                    .into_iter()
76                    .map(|alpha| 2.0 * alpha / range)
77                    .collect();
78                let peak = raw.iter().copied().fold(0.0f32, f32::max);
79
80                let global = validated_scale(
81                    peak / scheme.scale_dtype().max_representable(),
82                    global_dtype,
83                );
84
85                let scales: Vec<f32> = raw
86                    .iter()
87                    .map(|&raw| validated_scale(raw / global, scheme.scale_dtype()))
88                    .collect();
89                let quantized = scheme
90                    .value
91                    .encode_all(&float_data, |index| global * scales[blocks.block_of(index)]);
92
93                (quantized, scales, Some(global))
94            }
95            (None, _) => {
96                let scale = validated_scale(
97                    block_max_abs_scale(&float_data, range),
98                    scheme.scale_dtype(),
99                );
100                let quantized = scheme.value.encode_all(&float_data, move |_| scale);
101
102                (quantized, alloc::vec![scale], None)
103            }
104            (Some(block_size), None) => {
105                let blocks = block_layout(&shape, &block_size);
106                let scales: Vec<f32> = block_max_abs(&float_data, &blocks)
107                    .into_iter()
108                    .map(|alpha| validated_scale(2.0 * alpha / range, scheme.scale_dtype()))
109                    .collect();
110                let quantized = scheme
111                    .value
112                    .encode_all(&float_data, |index| scales[blocks.block_of(index)]);
113
114                (quantized, scales, None)
115            }
116        };
117
118        let bytes = Bytes::from_elems(quantized);
119        let layout = Layout::contiguous(shape);
120        let qt = FlexTensor::new(bytes, layout, DType::I8);
121
122        FlexQTensor::new(qt, *scheme, scales, global)
123    }
124
125    fn quantize(
126        tensor: FloatTensor<Flex>,
127        scheme: &QuantScheme,
128        qparams: QuantizationParametersPrimitive<Flex>,
129    ) -> QuantizedTensor<Flex> {
130        let shape = tensor.shape();
131        let tensor = tensor.to_contiguous();
132        let float_data = float_storage_as_f32(&tensor);
133
134        // Extract and validate scales from the qparams tensor. The scales tensor
135        // shares its dtype with the float element type, which can be any of
136        // f32/f64/f16/bf16, so we normalise via float_storage_as_f32 instead of
137        // assuming f32 storage.
138        let scales_tensor = qparams.scales.to_contiguous();
139        let scales_data = float_storage_as_f32(&scales_tensor);
140        let scales: Vec<f32> = scales_data
141            .iter()
142            .copied()
143            .map(|s| validated_scale(s, scheme.scale_dtype()))
144            .collect();
145
146        let global = qparams.global.map(|global| {
147            let dtype = global_scale_dtype(scheme)
148                .expect("a per-tensor scale should come with a two-level scheme");
149            let global = global.to_contiguous();
150            validated_scale(float_storage_as_f32(&global)[0], dtype)
151        });
152
153        let quantized = match scheme.block_size() {
154            None => {
155                let scale = scales[0];
156                scheme.value.encode_all(&float_data, move |_| scale)
157            }
158            Some(block_size) => {
159                let blocks = block_layout(&shape, &block_size);
160                let multiplier = global.unwrap_or(1.0);
161                scheme.value.encode_all(&float_data, |index| {
162                    multiplier * scales[blocks.block_of(index)]
163                })
164            }
165        };
166
167        let bytes = Bytes::from_elems(quantized);
168        let layout = Layout::contiguous(shape);
169        let qt = FlexTensor::new(bytes, layout, DType::I8);
170
171        FlexQTensor::new(qt, *scheme, scales, global)
172    }
173
174    fn dequantize(tensor: QuantizedTensor<Flex>, dtype: FloatDType) -> FloatTensor<Flex> {
175        let shape = tensor.tensor.shape();
176        let qt = tensor.tensor.to_contiguous();
177        let q_data: &[i8] = qt.storage();
178
179        let dequantized = match tensor.scheme.block_size() {
180            None => {
181                let scale = tensor.scales[0];
182                tensor.scheme.value.decode_all(q_data, move |_| scale)
183            }
184            Some(block_size) => {
185                let blocks = BlockLayout::new(&shape, &block_size);
186                let multiplier = tensor.global.unwrap_or(1.0);
187                tensor.scheme.value.decode_all(q_data, |index| {
188                    multiplier * tensor.scales[blocks.block_of(index)]
189                })
190            }
191        };
192
193        let layout = Layout::contiguous(shape);
194        match dtype {
195            FloatDType::F32 | FloatDType::Flex32 => {
196                FlexTensor::new(Bytes::from_elems(dequantized), layout, DType::F32)
197            }
198            FloatDType::F64 => {
199                let data: Vec<f64> = dequantized.iter().map(|&v| v as f64).collect();
200                FlexTensor::new(Bytes::from_elems(data), layout, DType::F64)
201            }
202            FloatDType::F16 => {
203                let data: Vec<f16> = dequantized.iter().map(|&v| f16::from_f32(v)).collect();
204                FlexTensor::new(Bytes::from_elems(data), layout, DType::F16)
205            }
206            FloatDType::BF16 => {
207                let data: Vec<bf16> = dequantized.iter().map(|&v| bf16::from_f32(v)).collect();
208                FlexTensor::new(Bytes::from_elems(data), layout, DType::BF16)
209            }
210        }
211    }
212
213    fn q_to_device(tensor: QuantizedTensor<Flex>, _device: &Device<Flex>) -> QuantizedTensor<Flex> {
214        tensor
215    }
216
217    fn q_reshape(tensor: QuantizedTensor<Flex>, shape: Shape) -> QuantizedTensor<Flex> {
218        let mut scheme = tensor.scheme;
219        scheme.reshape_packing_dim(shape.num_dims());
220        block_safe_layout_op(tensor, scheme, |t| t.reshape(shape))
221    }
222
223    async fn q_into_data(tensor: QuantizedTensor<Flex>) -> Result<TensorData, ExecutionError> {
224        let shape = tensor.tensor.shape();
225        let scheme = tensor.scheme;
226        let qt = tensor.tensor.to_contiguous();
227        let values: Vec<i8> = qt.storage::<i8>().to_vec();
228
229        Ok(TensorData::quantized(
230            values,
231            shape.to_vec(),
232            scheme,
233            &tensor.scales,
234            tensor.global,
235        ))
236    }
237
238    fn q_swap_dims(
239        tensor: QuantizedTensor<Flex>,
240        dim1: usize,
241        dim2: usize,
242    ) -> QuantizedTensor<Flex> {
243        let mut scheme = tensor.scheme;
244        scheme.swap_dims(tensor.tensor.shape().num_dims(), dim1, dim2);
245        block_safe_layout_op(tensor, scheme, |t| t.transpose(dim1, dim2))
246    }
247
248    fn q_permute(tensor: QuantizedTensor<Flex>, axes: &[usize]) -> QuantizedTensor<Flex> {
249        let mut scheme = tensor.scheme;
250        scheme.permute_dims(axes);
251        block_safe_layout_op(tensor, scheme, |t| t.permute(axes))
252    }
253
254    fn q_flip(tensor: QuantizedTensor<Flex>, axes: &[usize]) -> QuantizedTensor<Flex> {
255        let scheme = tensor.scheme;
256        block_safe_layout_op(tensor, scheme, |t| crate::ops::flip::flip(t, axes))
257    }
258
259    fn q_expand(tensor: QuantizedTensor<Flex>, shape: Shape) -> QuantizedTensor<Flex> {
260        let scheme = tensor.scheme;
261        block_safe_layout_op(tensor, scheme, |t| crate::ops::expand::expand(t, shape))
262    }
263
264    fn q_select(
265        tensor: QuantizedTensor<Flex>,
266        dim: usize,
267        indices: IntTensor<Flex>,
268    ) -> QuantizedTensor<Flex> {
269        match tensor.scheme.block_size() {
270            None => FlexQTensor::new(
271                crate::ops::gather_scatter::select::<i8>(tensor.tensor, dim, indices),
272                tensor.scheme,
273                tensor.scales,
274                tensor.global,
275            ),
276            Some(_) => {
277                let scheme = tensor.scheme;
278                let float_tensor = Flex::dequantize(tensor, FloatDType::F32);
279                let result = crate::ops::gather_scatter::select::<f32>(float_tensor, dim, indices);
280                Flex::quantize_dynamic(result, &scheme)
281            }
282        }
283    }
284
285    fn q_slice(tensor: QuantizedTensor<Flex>, slices: &[Slice]) -> QuantizedTensor<Flex> {
286        let scheme = tensor.scheme;
287        block_safe_layout_op(tensor, scheme, |t| crate::ops::slice::slice(t, slices))
288    }
289
290    fn q_argmax(
291        tensor: QuantizedTensor<Flex>,
292        dim: usize,
293        out_dtype: burn_std::IntDType,
294    ) -> IntTensor<Flex> {
295        let result = if codes_order_as_values(&tensor) {
296            crate::ops::reduce::argmax(tensor.tensor, dim)
297        } else {
298            crate::ops::reduce::argmax(Flex::dequantize(tensor, FloatDType::F32), dim)
299        };
300        if result.dtype() != DType::from(out_dtype) {
301            Flex::int_cast(result, out_dtype)
302        } else {
303            result
304        }
305    }
306
307    fn q_argmin(
308        tensor: QuantizedTensor<Flex>,
309        dim: usize,
310        out_dtype: burn_std::IntDType,
311    ) -> IntTensor<Flex> {
312        let result = if codes_order_as_values(&tensor) {
313            crate::ops::reduce::argmin(tensor.tensor, dim)
314        } else {
315            crate::ops::reduce::argmin(Flex::dequantize(tensor, FloatDType::F32), dim)
316        };
317        if result.dtype() != DType::from(out_dtype) {
318            Flex::int_cast(result, out_dtype)
319        } else {
320            result
321        }
322    }
323
324    fn q_gather(
325        dim: usize,
326        tensor: QuantizedTensor<Flex>,
327        indices: IntTensor<Flex>,
328    ) -> QuantizedTensor<Flex> {
329        match tensor.scheme.block_size() {
330            None => FlexQTensor::new(
331                crate::ops::gather_scatter::gather::<i8>(tensor.tensor, dim, indices),
332                tensor.scheme,
333                tensor.scales,
334                tensor.global,
335            ),
336            Some(_) => {
337                let scheme = tensor.scheme;
338                let float_tensor = Flex::dequantize(tensor, FloatDType::F32);
339                let result = crate::ops::gather_scatter::gather::<f32>(float_tensor, dim, indices);
340                Flex::quantize_dynamic(result, &scheme)
341            }
342        }
343    }
344}
345
346/// Apply a layout operation to a quantized tensor, which takes `scheme`, already rewritten by the
347/// caller to follow the move as every other backend does. A block-quantized tensor is dequantized,
348/// moved and requantized, since its blocks would no longer line up with its scales.
349fn block_safe_layout_op(
350    qtensor: FlexQTensor,
351    scheme: QuantScheme,
352    op: impl FnOnce(FlexTensor) -> FlexTensor,
353) -> FlexQTensor {
354    match qtensor.scheme.block_size() {
355        None => FlexQTensor::new(op(qtensor.tensor), scheme, qtensor.scales, qtensor.global),
356        Some(_) => {
357            let float_tensor = Flex::dequantize(qtensor, FloatDType::F32);
358            let result = op(float_tensor);
359            Flex::quantize_dynamic(result, &scheme)
360        }
361    }
362}
363
364/// Whether comparing `tensor`'s codes compares its values: one positive scale over integer codes.
365fn codes_order_as_values(tensor: &FlexQTensor) -> bool {
366    tensor.scheme.block_size().is_none() && !tensor.scheme.value.is_float()
367}
368
369/// Unrounded; callers round separately.
370fn block_max_abs_scale(block: &[f32], range: f32) -> f32 {
371    let alpha = block.iter().fold(0.0f32, |alpha, &x| alpha.max(x.abs()));
372    2.0 * alpha / range
373}
374
375/// Only an exactly zero scale is replaced: the dtype's subnormals carry real information for a
376/// small tensor, and the replacement goes back through the dtype because `f32::MIN_POSITIVE`
377/// would itself encode to zero in a narrow one.
378fn validated_scale(scale: f32, dtype: ScaleDtype) -> f32 {
379    let scale = scale_to_dtype(scale, dtype);
380    if scale > 0.0 && scale.is_finite() {
381        scale
382    } else {
383        scale_to_dtype(f32::MIN_POSITIVE, dtype)
384    }
385}
386
387// Tests kept here exercise flex-specific behavior: quantization scheme
388// roundtrips, per-block / dynamic quantization, block-quantized layout
389// ops (transpose / select / flip dequantize), and f16/f64 dequantize
390// dtype paths. Plain layout-preservation / select / slice / argmax /
391// argmin / gather tests are covered generically in
392// crates/burn-backend-tests/tests/tensor/float/quantization/ops/extended/
393// so they run on every backend.
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use burn_backend::{
398        TensorMetadata,
399        quantization::{QuantStore, QuantValue},
400    };
401
402    fn data_of(tensor: QuantizedTensor<Flex>) -> TensorData {
403        burn_std::reader::try_read_sync(Flex::q_into_data(tensor))
404            .expect("flex reads synchronously")
405            .unwrap()
406    }
407
408    fn packed_q4() -> QuantScheme {
409        QuantScheme::default()
410            .with_value(QuantValue::Q4S)
411            .with_store(QuantStore::PackedU32(0))
412            .per_block([8], ScaleDtype::F32)
413    }
414
415    fn ramp(shape: [usize; 2]) -> FlexTensor {
416        let values = (0..shape[0] * shape[1])
417            .map(|i| (i as f32 * 0.37).sin())
418            .collect::<Vec<_>>();
419        FlexTensor::from_data(TensorData::new(values, shape))
420    }
421
422    #[test]
423    fn a_packed_tensor_keeps_its_scheme_through_its_data() {
424        let scheme = packed_q4();
425        let quantized = Flex::quantize_dynamic(ramp([4, 16]), &scheme);
426        assert_eq!(quantized.scheme, scheme);
427
428        let data = data_of(quantized.clone());
429        assert_eq!(data.dtype(), DType::QFloat(scheme));
430
431        let reloaded = Flex::q_from_data(data.clone(), &Default::default());
432        assert_eq!(reloaded.scheme, scheme);
433        assert_eq!(data_of(reloaded).as_bytes(), data.as_bytes());
434    }
435
436    #[test]
437    fn swapping_the_packed_axis_moves_the_store_with_it() {
438        let per_tensor = QuantScheme::default()
439            .with_value(QuantValue::Q4S)
440            .with_store(QuantStore::PackedU32(0));
441        for scheme in [per_tensor, packed_q4()] {
442            let quantized = Flex::quantize_dynamic(ramp([16, 8]), &scheme);
443
444            let swapped = Flex::q_swap_dims(quantized, 0, 1);
445            assert_eq!(swapped.scheme.store, QuantStore::PackedU32(1));
446
447            let direct = Flex::dequantize(swapped.clone(), FloatDType::F32);
448            let reloaded = Flex::q_from_data(data_of(swapped), &Default::default());
449            let reloaded = Flex::dequantize(reloaded, FloatDType::F32);
450            assert_eq!(
451                reloaded.to_contiguous().storage::<f32>(),
452                direct.to_contiguous().storage::<f32>(),
453                "{scheme:?}"
454            );
455        }
456    }
457
458    #[test]
459    fn a_reshape_that_drops_the_packed_axis_packs_innermost() {
460        let q8 = QuantScheme::default().with_value(QuantValue::Q8S);
461        for (scheme, flattened) in [
462            (
463                q8.with_store(QuantStore::PackedU32(0)),
464                QuantStore::PackedU32(0),
465            ),
466            (
467                q8.with_value(QuantValue::E2M1)
468                    .with_store(QuantStore::PackedNative(0)),
469                QuantStore::PackedNative(0),
470            ),
471        ] {
472            let swapped = Flex::q_swap_dims(Flex::quantize_dynamic(ramp([4, 8]), &scheme), 0, 1);
473            let expected = Flex::dequantize(swapped.clone(), FloatDType::F32);
474
475            let reshaped = Flex::q_reshape(swapped, Shape::new([32]));
476
477            assert_eq!(reshaped.scheme.store, flattened);
478            let reloaded = Flex::q_from_data(data_of(reshaped), &Default::default());
479            assert_eq!(
480                Flex::dequantize(reloaded, FloatDType::F32).storage::<f32>(),
481                expected.to_contiguous().storage::<f32>(),
482                "{scheme:?}"
483            );
484        }
485    }
486
487    #[test]
488    fn global_reductions_follow_a_swap_of_the_packed_axis() {
489        let scheme = QuantScheme::default()
490            .with_value(QuantValue::Q8S)
491            .with_store(QuantStore::PackedU32(0));
492        let swapped = Flex::q_swap_dims(Flex::quantize_dynamic(ramp([4, 8]), &scheme), 0, 1);
493        let values = Flex::dequantize(swapped.clone(), FloatDType::F32)
494            .to_contiguous()
495            .storage::<f32>()
496            .to_vec();
497        let reduced = |tensor: QuantizedTensor<Flex>| {
498            Flex::dequantize(tensor, FloatDType::F32).storage::<f32>()[0]
499        };
500
501        assert_eq!(
502            reduced(Flex::q_max(swapped.clone())),
503            values.iter().copied().fold(f32::MIN, f32::max)
504        );
505        assert_eq!(
506            reduced(Flex::q_min(swapped.clone())),
507            values.iter().copied().fold(f32::MAX, f32::min)
508        );
509        assert_eq!(
510            reduced(Flex::q_max_abs(swapped)).abs(),
511            values.iter().map(|value| value.abs()).fold(0.0, f32::max)
512        );
513    }
514
515    #[test]
516    fn float_formats_reconstruct_values_on_their_grid() {
517        for (value, values) in [
518            (
519                QuantValue::E2M1,
520                [0.0, 0.5, -1.5, 6.0, -3.0, 2.0, 4.0, -6.0],
521            ),
522            (
523                QuantValue::E4M3,
524                [448.0, -2.5, 0.125, 1.0, -0.0, 3.5, -448.0, 64.0],
525            ),
526        ] {
527            let scheme = QuantScheme::default()
528                .with_value(value)
529                .with_store(QuantStore::Native);
530            let input = FlexTensor::from_data(TensorData::new(values.to_vec(), [8]));
531
532            let output = Flex::dequantize(Flex::quantize_dynamic(input, &scheme), FloatDType::F32);
533
534            assert_eq!(output.to_contiguous().storage::<f32>(), values, "{value:?}");
535        }
536    }
537
538    #[test]
539    fn argmax_over_float_codes_follows_the_values() {
540        let scheme = QuantScheme::default()
541            .with_value(QuantValue::E2M1)
542            .with_store(QuantStore::Native);
543        let input = FlexTensor::from_data(TensorData::new(vec![-6.0f32, -1.0, 0.5, -0.5], [1, 4]));
544        let quantized = Flex::quantize_dynamic(input, &scheme);
545
546        let argmax = Flex::q_argmax(quantized.clone(), 1, burn_std::IntDType::I64);
547        let argmin = Flex::q_argmin(quantized, 1, burn_std::IntDType::I64);
548
549        assert_eq!(argmax.to_contiguous().storage::<i64>(), [2]);
550        assert_eq!(argmin.to_contiguous().storage::<i64>(), [0]);
551    }
552
553    #[test]
554    fn float_schemes_keep_their_layout_through_their_data() {
555        let mxfp4 = QuantScheme::default()
556            .with_value(QuantValue::E2M1)
557            .per_block([32], ScaleDtype::UE8M0);
558        for scheme in [
559            QuantScheme::default().with_value(QuantValue::E4M3),
560            QuantScheme::default()
561                .with_value(QuantValue::E5M2)
562                .with_store(QuantStore::Native),
563            mxfp4,
564            mxfp4.with_store(QuantStore::PackedNative(0)),
565        ] {
566            let quantized = Flex::quantize_dynamic(ramp([4, 32]), &scheme);
567            let data = data_of(quantized.clone());
568            assert_eq!(data.dtype(), DType::QFloat(scheme));
569
570            let reloaded = Flex::q_from_data(data.clone(), &Default::default());
571            assert_eq!(
572                data_of(reloaded.clone()).as_bytes(),
573                data.as_bytes(),
574                "{scheme:?}"
575            );
576            assert_eq!(
577                Flex::dequantize(reloaded, FloatDType::F32)
578                    .to_contiguous()
579                    .storage::<f32>(),
580                Flex::dequantize(quantized, FloatDType::F32)
581                    .to_contiguous()
582                    .storage::<f32>(),
583                "{scheme:?}"
584            );
585        }
586    }
587
588    #[test]
589    fn test_quantize_dequantize_roundtrip() {
590        // Create a float tensor
591        let values = vec![0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0];
592        let tensor = FlexTensor::from_data(TensorData::new(values.clone(), [2, 3]));
593
594        let scheme = QuantScheme::default()
595            .with_value(QuantValue::Q8S)
596            .with_store(QuantStore::Native);
597
598        // Compute scale: symmetric, so scale = 2 * max(|min|, |max|) / (b - a)
599        // max_abs = 5.0, range = 127 - (-127) = 254
600        // scale = 2 * 5.0 / 254 = 0.03937008
601        let scale: f32 = 2.0 * 5.0 / 254.0;
602        let scales_tensor = FlexTensor::from_data(TensorData::new(vec![scale], [1]));
603
604        let qparams = QuantizationParametersPrimitive {
605            scales: scales_tensor,
606            global: None,
607        };
608
609        // Quantize
610        let qtensor = Flex::quantize(tensor, &scheme, qparams);
611        assert_eq!(qtensor.tensor.shape().to_vec(), vec![2, 3]);
612        assert_eq!(qtensor.tensor.dtype(), DType::I8);
613
614        // Check quantized values
615        let q_vals: &[i8] = qtensor.tensor.storage();
616        // 0 / 0.03937 = 0, 1 / 0.03937 = 25.4 -> 25, etc.
617        assert_eq!(q_vals[0], 0);
618        assert_eq!(q_vals[1], 25);
619        assert_eq!(q_vals[5], 127);
620
621        // Dequantize
622        let result = Flex::dequantize(qtensor, FloatDType::F32);
623        assert_eq!(result.shape().to_vec(), vec![2, 3]);
624        assert_eq!(result.dtype(), DType::F32);
625
626        let result_vals: &[f32] = result.storage();
627        // Values should be approximately equal (quantization introduces small errors)
628        for (orig, deq) in values.iter().zip(result_vals.iter()) {
629            assert!((orig - deq).abs() < 0.05, "orig={orig}, dequantized={deq}");
630        }
631    }
632
633    #[test]
634    fn test_quantize_dequantize_negative_values() {
635        let values = vec![-3.0f32, -1.5, 0.0, 1.5, 3.0];
636        let tensor = FlexTensor::from_data(TensorData::new(values.clone(), [5]));
637
638        let scheme = QuantScheme::default()
639            .with_value(QuantValue::Q8S)
640            .with_store(QuantStore::Native);
641
642        let scale: f32 = 2.0 * 3.0 / 254.0;
643        let scales_tensor = FlexTensor::from_data(TensorData::new(vec![scale], [1]));
644
645        let qparams = QuantizationParametersPrimitive {
646            scales: scales_tensor,
647            global: None,
648        };
649
650        let qtensor = Flex::quantize(tensor, &scheme, qparams);
651        let result = Flex::dequantize(qtensor, FloatDType::F32);
652        let result_vals: &[f32] = result.storage();
653
654        for (orig, deq) in values.iter().zip(result_vals.iter()) {
655            assert!((orig - deq).abs() < 0.05, "orig={orig}, dequantized={deq}");
656        }
657    }
658
659    #[test]
660    fn test_q_from_data_into_data_roundtrip() {
661        // Create quantized TensorData the standard way
662        let values = vec![0i8, 25, 51, 76, 102, 127];
663        let scale = 0.03937008f32;
664        let scheme = QuantScheme::default()
665            .with_value(QuantValue::Q8S)
666            .with_store(QuantStore::Native);
667
668        let data = TensorData::quantized(values.clone(), [2, 3], scheme, &[scale], None);
669
670        // Load into FlexQTensor
671        let qtensor = Flex::q_from_data(data, &Default::default());
672        assert_eq!(qtensor.tensor.shape().to_vec(), vec![2, 3]);
673        assert_eq!(qtensor.scales, vec![scale]);
674
675        // Dequantize and check values
676        let float_tensor = Flex::dequantize(qtensor, FloatDType::F32);
677        let result: &[f32] = float_tensor.storage();
678        assert!((result[0]).abs() < 0.01); // 0 * scale ~ 0
679        assert!((result[5] - 5.0).abs() < 0.05); // 127 * scale ~ 5.0
680    }
681
682    #[test]
683    fn test_quantize_zero_tensor() {
684        let values = vec![0.0f32; 4];
685        let tensor = FlexTensor::from_data(TensorData::new(values, [4]));
686
687        let scheme = QuantScheme::default()
688            .with_value(QuantValue::Q8S)
689            .with_store(QuantStore::Native);
690
691        // Scale of 0 should be handled gracefully
692        let scales_tensor = FlexTensor::from_data(TensorData::new(vec![0.0f32], [1]));
693        let qparams = QuantizationParametersPrimitive {
694            scales: scales_tensor,
695            global: None,
696        };
697
698        let qtensor = Flex::quantize(tensor, &scheme, qparams);
699        let q_vals: &[i8] = qtensor.tensor.storage();
700        assert_eq!(q_vals, &[0, 0, 0, 0]);
701    }
702
703    /// The scale a narrow dtype can represent is coarser than the exact one, so the stored scale
704    /// must reflect the dtype rather than staying at full `f32` precision. Before scales were
705    /// rounded, every scale dtype produced byte-identical results here.
706    #[test]
707    fn test_quantize_dynamic_honors_scale_dtype_precision() {
708        // 4.5 / 127 is not representable in e4m3, so rounding is observable.
709        let values = vec![-3.0f32, -1.5, 0.0, 1.5, 3.0, 4.5];
710        let scheme = QuantScheme::default()
711            .with_value(QuantValue::Q8S)
712            .with_store(QuantStore::Native);
713
714        let quantize_with = |dtype| {
715            let tensor = FlexTensor::from_data(TensorData::new(values.clone(), [2, 3]));
716            Flex::quantize_dynamic(tensor, &scheme.per_tensor(dtype)).scales[0]
717        };
718
719        let exact = quantize_with(ScaleDtype::F32);
720        let coarse = quantize_with(ScaleDtype::UE4M3);
721
722        assert_ne!(
723            exact, coarse,
724            "UE4M3 scale should differ from the exact f32 scale"
725        );
726        assert_eq!(
727            coarse,
728            scale_to_dtype(exact, ScaleDtype::UE4M3),
729            "stored scale should be the exact scale rounded to the scale dtype"
730        );
731    }
732
733    #[test]
734    fn test_quantize_dynamic_roundtrip() {
735        let values = vec![-3.0f32, -1.5, 0.0, 1.5, 3.0, 4.5];
736        let tensor = FlexTensor::from_data(TensorData::new(values.clone(), [2, 3]));
737
738        let scheme = QuantScheme::default()
739            .with_value(QuantValue::Q8S)
740            .with_store(QuantStore::Native);
741
742        let qtensor = Flex::quantize_dynamic(tensor, &scheme);
743        assert_eq!(qtensor.tensor.shape().to_vec(), vec![2, 3]);
744        assert_eq!(qtensor.scales.len(), 1);
745
746        // Scale should be 2 * 4.5 / 254
747        let expected_scale: f32 = 2.0 * 4.5 / 254.0;
748        assert!(
749            (qtensor.scales[0] - expected_scale).abs() < 1e-6,
750            "scale={}, expected={}",
751            qtensor.scales[0],
752            expected_scale
753        );
754
755        let result = Flex::dequantize(qtensor, FloatDType::F32);
756        let result_vals: &[f32] = result.storage();
757        for (orig, deq) in values.iter().zip(result_vals.iter()) {
758            assert!((orig - deq).abs() < 0.1, "orig={orig}, dequantized={deq}");
759        }
760    }
761
762    #[test]
763    fn test_per_block_quantize_dequantize() {
764        use burn_std::quantization::BlockSize;
765
766        let values = vec![0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
767        let tensor = FlexTensor::from_data(TensorData::new(values.clone(), [8]));
768
769        let block_size = BlockSize::new([4]);
770        let scheme = QuantScheme::default()
771            .with_value(QuantValue::Q8S)
772            .per_block(block_size.as_slice(), ScaleDtype::F32)
773            .with_store(QuantStore::Native);
774
775        // Block 1: [0, 1, 2, 3] -> max_abs=3, scale = 6/254
776        // Block 2: [4, 5, 6, 7] -> max_abs=7, scale = 14/254
777        let scale_1: f32 = 2.0 * 3.0 / 254.0;
778        let scale_2: f32 = 2.0 * 7.0 / 254.0;
779        let scales_tensor = FlexTensor::from_data(TensorData::new(vec![scale_1, scale_2], [2]));
780
781        let qparams = QuantizationParametersPrimitive {
782            scales: scales_tensor,
783            global: None,
784        };
785
786        let qtensor = Flex::quantize(tensor, &scheme, qparams);
787        assert_eq!(qtensor.scales.len(), 2);
788
789        let result = Flex::dequantize(qtensor, FloatDType::F32);
790        let result_vals: &[f32] = result.storage();
791
792        for (orig, deq) in values.iter().zip(result_vals.iter()) {
793            assert!((orig - deq).abs() < 0.1, "orig={orig}, dequantized={deq}");
794        }
795    }
796
797    #[test]
798    fn test_quantize_dynamic_block() {
799        use burn_std::quantization::BlockSize;
800
801        let values = vec![-2.0f32, -1.0, 0.0, 1.0, 4.0, 5.0, 6.0, 7.0];
802        let tensor = FlexTensor::from_data(TensorData::new(values.clone(), [8]));
803
804        let block_size = BlockSize::new([4]);
805        let scheme = QuantScheme::default()
806            .with_value(QuantValue::Q8S)
807            .per_block(block_size.as_slice(), ScaleDtype::F32)
808            .with_store(QuantStore::Native);
809
810        let qtensor = Flex::quantize_dynamic(tensor, &scheme);
811        assert_eq!(qtensor.scales.len(), 2);
812
813        // Block 1: [-2, -1, 0, 1] -> alpha=2, scale = 4/254
814        // Block 2: [4, 5, 6, 7] -> alpha=7, scale = 14/254
815        let expected_scale_1: f32 = 2.0 * 2.0 / 254.0;
816        let expected_scale_2: f32 = 2.0 * 7.0 / 254.0;
817        assert!((qtensor.scales[0] - expected_scale_1).abs() < 1e-6);
818        assert!((qtensor.scales[1] - expected_scale_2).abs() < 1e-6);
819
820        let result = Flex::dequantize(qtensor, FloatDType::F32);
821        let result_vals: &[f32] = result.storage();
822        for (orig, deq) in values.iter().zip(result_vals.iter()) {
823            assert!((orig - deq).abs() < 0.1, "orig={orig}, dequantized={deq}");
824        }
825    }
826
827    #[test]
828    fn test_quantize_dynamic_q8f() {
829        // Q8F uses asymmetric range [-128, 127]
830        let values = vec![-5.0f32, -2.5, 0.0, 2.5, 5.0, 7.5];
831        let tensor = FlexTensor::from_data(TensorData::new(values.clone(), [6]));
832
833        let scheme = QuantScheme::default()
834            .with_value(QuantValue::Q8F)
835            .with_store(QuantStore::Native);
836
837        let qtensor = Flex::quantize_dynamic(tensor, &scheme);
838
839        // Q8F range: [-128, 127], so range = 255
840        // alpha = 7.5, scale = 2 * 7.5 / 255
841        let expected_scale: f32 = 2.0 * 7.5 / 255.0;
842        assert!(
843            (qtensor.scales[0] - expected_scale).abs() < 1e-6,
844            "scale={}, expected={}",
845            qtensor.scales[0],
846            expected_scale
847        );
848
849        let result = Flex::dequantize(qtensor, FloatDType::F32);
850        let result_vals: &[f32] = result.storage();
851        for (orig, deq) in values.iter().zip(result_vals.iter()) {
852            assert!((orig - deq).abs() < 0.1, "orig={orig}, dequantized={deq}");
853        }
854    }
855
856    #[test]
857    fn test_block_quantized_transpose_dequantize() {
858        use burn_std::quantization::BlockSize;
859
860        // 2x4 tensor, 2 blocks of 4
861        let values = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
862        let tensor = FlexTensor::from_data(TensorData::new(values, [2, 4]));
863
864        let block_size = BlockSize::new([4]);
865        let scheme = QuantScheme::default()
866            .with_value(QuantValue::Q8S)
867            .per_block(block_size.as_slice(), ScaleDtype::F32)
868            .with_store(QuantStore::Native);
869
870        let qtensor = Flex::quantize_dynamic(tensor, &scheme);
871
872        // Transpose to [4, 2], then dequantize
873        let transposed = Flex::q_swap_dims(qtensor, 0, 1);
874        assert_eq!(transposed.tensor.shape().to_vec(), vec![4, 2]);
875
876        let result = Flex::dequantize(transposed, FloatDType::F32);
877        let result_vals: &[f32] = result.storage();
878
879        // Original [[1,2,3,4],[5,6,7,8]] transposed to [[1,5],[2,6],[3,7],[4,8]]
880        let expected = [1.0f32, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0];
881        for (exp, deq) in expected.iter().zip(result_vals.iter()) {
882            assert!(
883                (exp - deq).abs() < 0.15,
884                "expected={exp}, dequantized={deq}"
885            );
886        }
887    }
888
889    #[test]
890    fn test_block_quantized_select() {
891        use burn_std::quantization::BlockSize;
892
893        // 2x4 tensor, 2 blocks of 4
894        let values = vec![1.0f32, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0];
895        let tensor = FlexTensor::from_data(TensorData::new(values, [2, 4]));
896
897        let block_size = BlockSize::new([4]);
898        let scheme = QuantScheme::default()
899            .with_value(QuantValue::Q8S)
900            .per_block(block_size.as_slice(), ScaleDtype::F32)
901            .with_store(QuantStore::Native);
902
903        let qtensor = Flex::quantize_dynamic(tensor, &scheme);
904
905        // Select row 1 -> [10, 20, 30, 40]
906        let indices = FlexTensor::from_data(TensorData::new(vec![1i64], [1]));
907        let selected = Flex::q_select(qtensor, 0, indices);
908        assert_eq!(selected.tensor.shape().to_vec(), vec![1, 4]);
909
910        let result = Flex::dequantize(selected, FloatDType::F32);
911        let result_vals: &[f32] = result.storage();
912        let expected = [10.0f32, 20.0, 30.0, 40.0];
913        for (exp, deq) in expected.iter().zip(result_vals.iter()) {
914            assert!((exp - deq).abs() < 0.5, "expected={exp}, dequantized={deq}");
915        }
916    }
917
918    #[test]
919    fn test_block_quantized_flip_dequantize() {
920        use burn_std::quantization::BlockSize;
921
922        let values = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
923        let tensor = FlexTensor::from_data(TensorData::new(values, [2, 4]));
924
925        let block_size = BlockSize::new([4]);
926        let scheme = QuantScheme::default()
927            .with_value(QuantValue::Q8S)
928            .per_block(block_size.as_slice(), ScaleDtype::F32)
929            .with_store(QuantStore::Native);
930
931        let qtensor = Flex::quantize_dynamic(tensor, &scheme);
932
933        // Flip along axis 0: [[5,6,7,8],[1,2,3,4]]
934        let flipped = Flex::q_flip(qtensor, &[0]);
935        assert_eq!(flipped.tensor.shape().to_vec(), vec![2, 4]);
936
937        let result = Flex::dequantize(flipped, FloatDType::F32);
938        let result_vals: &[f32] = result.storage();
939        let expected = [5.0f32, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0];
940        for (exp, deq) in expected.iter().zip(result_vals.iter()) {
941            assert!(
942                (exp - deq).abs() < 0.15,
943                "expected={exp}, dequantized={deq}"
944            );
945        }
946    }
947
948    #[test]
949    fn test_quantize_dynamic_f64_tensor() {
950        use burn_backend::quantization::QuantValue;
951
952        let values = vec![0.0f64, 1.0, 2.0, 3.0, 4.0, 5.0];
953        let tensor = FlexTensor::new(
954            Bytes::from_elems(values),
955            Layout::contiguous([6].into()),
956            DType::F64,
957        );
958        assert_eq!(tensor.dtype(), DType::F64);
959
960        let scheme = QuantScheme::default()
961            .with_value(QuantValue::Q8S)
962            .with_store(QuantStore::Native);
963
964        let qtensor = Flex::quantize_dynamic(tensor, &scheme);
965        assert_eq!(qtensor.tensor.dtype(), DType::I8);
966
967        // Dequantize and verify round-trip accuracy
968        let result = Flex::dequantize(qtensor, FloatDType::F32);
969        let result_vals: &[f32] = result.storage();
970        let expected = [0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0];
971        for (exp, deq) in expected.iter().zip(result_vals.iter()) {
972            assert!(
973                (exp - deq).abs() < 0.15,
974                "expected={exp}, dequantized={deq}"
975            );
976        }
977    }
978
979    #[test]
980    fn test_dequantize_f64() {
981        let values = vec![0.0f32, 1.0, 2.0, 3.0];
982        let tensor = FlexTensor::from_data(TensorData::new(values.clone(), [4]));
983
984        let scheme = QuantScheme::default()
985            .with_value(QuantValue::Q8S)
986            .with_store(QuantStore::Native);
987
988        let qtensor = Flex::quantize_dynamic(tensor, &scheme);
989        let result = Flex::dequantize(qtensor, FloatDType::F64);
990        assert_eq!(result.dtype(), DType::F64);
991        let result_vals: &[f64] = result.storage();
992        for (orig, deq) in values.iter().zip(result_vals.iter()) {
993            assert!(
994                (*orig as f64 - deq).abs() < 0.05,
995                "orig={orig}, dequantized={deq}"
996            );
997        }
998    }
999
1000    #[test]
1001    fn test_dequantize_f16() {
1002        let values = vec![0.0f32, 1.0, 2.0, 3.0];
1003        let tensor = FlexTensor::from_data(TensorData::new(values.clone(), [4]));
1004
1005        let scheme = QuantScheme::default()
1006            .with_value(QuantValue::Q8S)
1007            .with_store(QuantStore::Native);
1008
1009        let qtensor = Flex::quantize_dynamic(tensor, &scheme);
1010        let result = Flex::dequantize(qtensor, FloatDType::F16);
1011        assert_eq!(result.dtype(), DType::F16);
1012        let result_vals: &[f16] = result.storage();
1013        for (orig, deq) in values.iter().zip(result_vals.iter()) {
1014            assert!(
1015                (*orig - f32::from(*deq)).abs() < 0.05,
1016                "orig={orig}, dequantized={deq}"
1017            );
1018        }
1019    }
1020}