Skip to main content

ruda_tensor_host/ops/
qtensor.rs

1//! Quantized tensor operations for the Host backend.
2
3use alloc::vec::Vec;
4#[cfg(not(feature = "std"))]
5#[allow(unused_imports)]
6use num_traits::Float;
7
8use ruda_tensor::{
9    DType, ExecutionError, FloatDType, TensorData, TensorMetadata,
10    ops::QTensorOps,
11    quantization::{
12        QuantLevel, QuantScheme, QuantStore, QuantizationParametersPrimitive,
13    },
14    tensor::{Device, FloatTensor, IntTensor, QuantizedTensor},
15};
16use ruda_tensor::{Bytes, Shape, Slice, bf16, f16};
17
18use crate::{Host, HostTensor, Layout};
19
20impl QTensorOps<Host> for Host {
21    fn q_from_data(data: TensorData, _device: &Device<Host>) -> QuantizedTensor<Host> {
22        ruprim_host::quantization::q_from_data(data)
23    }
24
25    fn quantize_dynamic(tensor: FloatTensor<Host>, scheme: &QuantScheme) -> QuantizedTensor<Host> {
26        ruprim_host::quantization::quantize_dynamic(tensor, scheme)
27    }
28
29    fn quantize(
30        tensor: FloatTensor<Host>,
31        scheme: &QuantScheme,
32        qparams: QuantizationParametersPrimitive<Host>,
33    ) -> QuantizedTensor<Host> {
34        ruprim_host::quantization::quantize(tensor, scheme, ruda_core::tensor::quantization::QParams { scales: qparams.scales })
35    }
36
37    fn dequantize(tensor: QuantizedTensor<Host>, dtype: FloatDType) -> FloatTensor<Host> {
38        ruprim_host::quantization::dequantize(tensor, dtype)
39    }
40
41    fn q_device(_tensor: &QuantizedTensor<Host>) -> Device<Host> {
42        Default::default()
43    }
44
45    fn q_to_device(tensor: QuantizedTensor<Host>, _device: &Device<Host>) -> QuantizedTensor<Host> {
46        tensor
47    }
48
49    fn q_reshape(tensor: QuantizedTensor<Host>, shape: Shape) -> QuantizedTensor<Host> {
50        ruprim_host::quantization::q_reshape(tensor, shape)
51    }
52
53    async fn q_into_data(tensor: QuantizedTensor<Host>) -> Result<TensorData, ExecutionError> {
54        ruprim_host::quantization::q_into_data(tensor).await
55    }
56
57    fn q_swap_dims(
58        tensor: QuantizedTensor<Host>,
59        dim1: usize,
60        dim2: usize,
61    ) -> QuantizedTensor<Host> {
62        ruprim_host::quantization::q_swap_dims(tensor, dim1, dim2)
63    }
64
65    fn q_permute(tensor: QuantizedTensor<Host>, axes: &[usize]) -> QuantizedTensor<Host> {
66        ruprim_host::quantization::q_permute(tensor, axes)
67    }
68
69    fn q_flip(tensor: QuantizedTensor<Host>, axes: &[usize]) -> QuantizedTensor<Host> {
70        ruprim_host::quantization::q_flip(tensor, axes)
71    }
72
73    fn q_expand(tensor: QuantizedTensor<Host>, shape: Shape) -> QuantizedTensor<Host> {
74        ruprim_host::quantization::q_expand(tensor, shape)
75    }
76
77    fn q_select(
78        tensor: QuantizedTensor<Host>,
79        dim: usize,
80        indices: IntTensor<Host>,
81    ) -> QuantizedTensor<Host> {
82        ruprim_host::quantization::q_select(tensor, dim, indices)
83    }
84
85    fn q_slice(tensor: QuantizedTensor<Host>, slices: &[Slice]) -> QuantizedTensor<Host> {
86        ruprim_host::quantization::q_slice(tensor, slices)
87    }
88
89    fn q_gather(
90        dim: usize,
91        tensor: QuantizedTensor<Host>,
92        indices: IntTensor<Host>,
93    ) -> QuantizedTensor<Host> {
94        ruprim_host::quantization::q_gather(dim, tensor, indices)
95    }
96}
97
98// Tests kept here exercise flex-specific behavior: quantization scheme
99// roundtrips, per-block / dynamic quantization, block-quantized layout
100// ops (transpose / select / flip dequantize), and f16/f64 dequantize
101// dtype paths. Plain layout-preservation / select / slice / argmax /
102// argmin / gather tests are covered generically in
103// crates/ruda-backend-tests/tests/tensor/float/quantization/ops/extended/
104// so they run on every backend.
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use ruda_tensor::{TensorMetadata, quantization::QuantValue};
109
110    #[test]
111    fn test_quantize_dequantize_roundtrip() {
112        // Create a float tensor
113        let values = vec![0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0];
114        let tensor = HostTensor::from_data(TensorData::new(values.clone(), [2, 3]));
115
116        let scheme = QuantScheme::default()
117            .with_value(QuantValue::Q8S)
118            .with_store(QuantStore::Native);
119
120        // Compute scale: symmetric, so scale = 2 * max(|min|, |max|) / (b - a)
121        // max_abs = 5.0, range = 127 - (-127) = 254
122        // scale = 2 * 5.0 / 254 = 0.03937008
123        let scale: f32 = 2.0 * 5.0 / 254.0;
124        let scales_tensor = HostTensor::from_data(TensorData::new(vec![scale], [1]));
125
126        let qparams = QuantizationParametersPrimitive {
127            scales: scales_tensor,
128        };
129
130        // Quantize
131        let qtensor = Host::quantize(tensor, &scheme, qparams);
132        assert_eq!(qtensor.tensor().shape().to_vec(), vec![2, 3]);
133        assert_eq!(qtensor.tensor().dtype(), DType::I8);
134
135        // Check quantized values
136        let q_vals: &[i8] = qtensor.tensor().storage();
137        // 0 / 0.03937 = 0, 1 / 0.03937 = 25.4 -> 25, etc.
138        assert_eq!(q_vals[0], 0);
139        assert_eq!(q_vals[1], 25);
140        assert_eq!(q_vals[5], 127);
141
142        // Dequantize
143        let result = Host::dequantize(qtensor, FloatDType::F32);
144        assert_eq!(result.shape().to_vec(), vec![2, 3]);
145        assert_eq!(result.dtype(), DType::F32);
146
147        let result_vals: &[f32] = result.storage();
148        // Values should be approximately equal (quantization introduces small errors)
149        for (orig, deq) in values.iter().zip(result_vals.iter()) {
150            assert!((orig - deq).abs() < 0.05, "orig={orig}, dequantized={deq}");
151        }
152    }
153
154    #[test]
155    fn test_quantize_dequantize_negative_values() {
156        let values = vec![-3.0f32, -1.5, 0.0, 1.5, 3.0];
157        let tensor = HostTensor::from_data(TensorData::new(values.clone(), [5]));
158
159        let scheme = QuantScheme::default()
160            .with_value(QuantValue::Q8S)
161            .with_store(QuantStore::Native);
162
163        let scale: f32 = 2.0 * 3.0 / 254.0;
164        let scales_tensor = HostTensor::from_data(TensorData::new(vec![scale], [1]));
165
166        let qparams = QuantizationParametersPrimitive {
167            scales: scales_tensor,
168        };
169
170        let qtensor = Host::quantize(tensor, &scheme, qparams);
171        let result = Host::dequantize(qtensor, FloatDType::F32);
172        let result_vals: &[f32] = result.storage();
173
174        for (orig, deq) in values.iter().zip(result_vals.iter()) {
175            assert!((orig - deq).abs() < 0.05, "orig={orig}, dequantized={deq}");
176        }
177    }
178
179    #[test]
180    fn test_q_from_data_into_data_roundtrip() {
181        // Create quantized TensorData the standard way
182        let values = vec![0i8, 25, 51, 76, 102, 127];
183        let scale = 0.03937008f32;
184        let scheme = QuantScheme::default()
185            .with_value(QuantValue::Q8S)
186            .with_store(QuantStore::Native);
187
188        let data = TensorData::quantized(values.clone(), [2, 3], scheme, &[scale]);
189
190        // Load into HostQTensor
191        let qtensor = Host::q_from_data(data, &Default::default());
192        assert_eq!(qtensor.tensor().shape().to_vec(), vec![2, 3]);
193        assert_eq!(qtensor.scales(), vec![scale]);
194
195        // Dequantize and check values
196        let float_tensor = Host::dequantize(qtensor, FloatDType::F32);
197        let result: &[f32] = float_tensor.storage();
198        assert!((result[0]).abs() < 0.01); // 0 * scale ~ 0
199        assert!((result[5] - 5.0).abs() < 0.05); // 127 * scale ~ 5.0
200    }
201
202    #[test]
203    fn test_quantize_zero_tensor() {
204        let values = vec![0.0f32; 4];
205        let tensor = HostTensor::from_data(TensorData::new(values, [4]));
206
207        let scheme = QuantScheme::default()
208            .with_value(QuantValue::Q8S)
209            .with_store(QuantStore::Native);
210
211        // Scale of 0 should be handled gracefully
212        let scales_tensor = HostTensor::from_data(TensorData::new(vec![0.0f32], [1]));
213        let qparams = QuantizationParametersPrimitive {
214            scales: scales_tensor,
215        };
216
217        let qtensor = Host::quantize(tensor, &scheme, qparams);
218        let q_vals: &[i8] = qtensor.tensor().storage();
219        assert_eq!(q_vals, &[0, 0, 0, 0]);
220    }
221
222    #[test]
223    fn test_quantize_dynamic_roundtrip() {
224        let values = vec![-3.0f32, -1.5, 0.0, 1.5, 3.0, 4.5];
225        let tensor = HostTensor::from_data(TensorData::new(values.clone(), [2, 3]));
226
227        let scheme = QuantScheme::default()
228            .with_value(QuantValue::Q8S)
229            .with_store(QuantStore::Native);
230
231        let qtensor = Host::quantize_dynamic(tensor, &scheme);
232        assert_eq!(qtensor.tensor().shape().to_vec(), vec![2, 3]);
233        assert_eq!(qtensor.scales().len(), 1);
234
235        // Scale should be 2 * 4.5 / 254
236        let expected_scale: f32 = 2.0 * 4.5 / 254.0;
237        assert!(
238            (qtensor.scales()[0] - expected_scale).abs() < 1e-6,
239            "scale={}, expected={}",
240            qtensor.scales()[0],
241            expected_scale
242        );
243
244        let result = Host::dequantize(qtensor, FloatDType::F32);
245        let result_vals: &[f32] = result.storage();
246        for (orig, deq) in values.iter().zip(result_vals.iter()) {
247            assert!((orig - deq).abs() < 0.1, "orig={orig}, dequantized={deq}");
248        }
249    }
250
251    #[test]
252    fn test_per_block_quantize_dequantize() {
253        use ruda_tensor::quantization::BlockSize;
254
255        let values = vec![0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
256        let tensor = HostTensor::from_data(TensorData::new(values.clone(), [8]));
257
258        let block_size = BlockSize::new([4]);
259        let scheme = QuantScheme::default()
260            .with_value(QuantValue::Q8S)
261            .with_level(QuantLevel::Block(block_size))
262            .with_store(QuantStore::Native);
263
264        // Block 1: [0, 1, 2, 3] -> max_abs=3, scale = 6/254
265        // Block 2: [4, 5, 6, 7] -> max_abs=7, scale = 14/254
266        let scale_1: f32 = 2.0 * 3.0 / 254.0;
267        let scale_2: f32 = 2.0 * 7.0 / 254.0;
268        let scales_tensor = HostTensor::from_data(TensorData::new(vec![scale_1, scale_2], [2]));
269
270        let qparams = QuantizationParametersPrimitive {
271            scales: scales_tensor,
272        };
273
274        let qtensor = Host::quantize(tensor, &scheme, qparams);
275        assert_eq!(qtensor.scales().len(), 2);
276
277        let result = Host::dequantize(qtensor, FloatDType::F32);
278        let result_vals: &[f32] = result.storage();
279
280        for (orig, deq) in values.iter().zip(result_vals.iter()) {
281            assert!((orig - deq).abs() < 0.1, "orig={orig}, dequantized={deq}");
282        }
283    }
284
285    #[test]
286    fn test_quantize_dynamic_block() {
287        use ruda_tensor::quantization::BlockSize;
288
289        let values = vec![-2.0f32, -1.0, 0.0, 1.0, 4.0, 5.0, 6.0, 7.0];
290        let tensor = HostTensor::from_data(TensorData::new(values.clone(), [8]));
291
292        let block_size = BlockSize::new([4]);
293        let scheme = QuantScheme::default()
294            .with_value(QuantValue::Q8S)
295            .with_level(QuantLevel::Block(block_size))
296            .with_store(QuantStore::Native);
297
298        let qtensor = Host::quantize_dynamic(tensor, &scheme);
299        assert_eq!(qtensor.scales().len(), 2);
300
301        // Block 1: [-2, -1, 0, 1] -> alpha=2, scale = 4/254
302        // Block 2: [4, 5, 6, 7] -> alpha=7, scale = 14/254
303        let expected_scale_1: f32 = 2.0 * 2.0 / 254.0;
304        let expected_scale_2: f32 = 2.0 * 7.0 / 254.0;
305        assert!((qtensor.scales()[0] - expected_scale_1).abs() < 1e-6);
306        assert!((qtensor.scales()[1] - expected_scale_2).abs() < 1e-6);
307
308        let result = Host::dequantize(qtensor, FloatDType::F32);
309        let result_vals: &[f32] = result.storage();
310        for (orig, deq) in values.iter().zip(result_vals.iter()) {
311            assert!((orig - deq).abs() < 0.1, "orig={orig}, dequantized={deq}");
312        }
313    }
314
315    #[test]
316    fn test_quantize_dynamic_q8f() {
317        // Q8F uses asymmetric range [-128, 127]
318        let values = vec![-5.0f32, -2.5, 0.0, 2.5, 5.0, 7.5];
319        let tensor = HostTensor::from_data(TensorData::new(values.clone(), [6]));
320
321        let scheme = QuantScheme::default()
322            .with_value(QuantValue::Q8F)
323            .with_store(QuantStore::Native);
324
325        let qtensor = Host::quantize_dynamic(tensor, &scheme);
326
327        // Q8F range: [-128, 127], so range = 255
328        // alpha = 7.5, scale = 2 * 7.5 / 255
329        let expected_scale: f32 = 2.0 * 7.5 / 255.0;
330        assert!(
331            (qtensor.scales()[0] - expected_scale).abs() < 1e-6,
332            "scale={}, expected={}",
333            qtensor.scales()[0],
334            expected_scale
335        );
336
337        let result = Host::dequantize(qtensor, FloatDType::F32);
338        let result_vals: &[f32] = result.storage();
339        for (orig, deq) in values.iter().zip(result_vals.iter()) {
340            assert!((orig - deq).abs() < 0.1, "orig={orig}, dequantized={deq}");
341        }
342    }
343
344    #[test]
345    fn test_block_quantized_transpose_dequantize() {
346        use ruda_tensor::quantization::BlockSize;
347
348        // 2x4 tensor, 2 blocks of 4
349        let values = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
350        let tensor = HostTensor::from_data(TensorData::new(values, [2, 4]));
351
352        let block_size = BlockSize::new([4]);
353        let scheme = QuantScheme::default()
354            .with_value(QuantValue::Q8S)
355            .with_level(QuantLevel::Block(block_size))
356            .with_store(QuantStore::Native);
357
358        let qtensor = Host::quantize_dynamic(tensor, &scheme);
359
360        // Transpose to [4, 2], then dequantize
361        let transposed = Host::q_swap_dims(qtensor, 0, 1);
362        assert_eq!(transposed.tensor().shape().to_vec(), vec![4, 2]);
363
364        let result = Host::dequantize(transposed, FloatDType::F32);
365        let result_vals: &[f32] = result.storage();
366
367        // Original [[1,2,3,4],[5,6,7,8]] transposed to [[1,5],[2,6],[3,7],[4,8]]
368        let expected = [1.0f32, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0];
369        for (exp, deq) in expected.iter().zip(result_vals.iter()) {
370            assert!(
371                (exp - deq).abs() < 0.15,
372                "expected={exp}, dequantized={deq}"
373            );
374        }
375    }
376
377    #[test]
378    fn test_block_quantized_select() {
379        use ruda_tensor::quantization::BlockSize;
380
381        // 2x4 tensor, 2 blocks of 4
382        let values = vec![1.0f32, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0];
383        let tensor = HostTensor::from_data(TensorData::new(values, [2, 4]));
384
385        let block_size = BlockSize::new([4]);
386        let scheme = QuantScheme::default()
387            .with_value(QuantValue::Q8S)
388            .with_level(QuantLevel::Block(block_size))
389            .with_store(QuantStore::Native);
390
391        let qtensor = Host::quantize_dynamic(tensor, &scheme);
392
393        // Select row 1 -> [10, 20, 30, 40]
394        let indices = HostTensor::from_data(TensorData::new(vec![1i64], [1]));
395        let selected = Host::q_select(qtensor, 0, indices);
396        assert_eq!(selected.tensor().shape().to_vec(), vec![1, 4]);
397
398        let result = Host::dequantize(selected, FloatDType::F32);
399        let result_vals: &[f32] = result.storage();
400        let expected = [10.0f32, 20.0, 30.0, 40.0];
401        for (exp, deq) in expected.iter().zip(result_vals.iter()) {
402            assert!((exp - deq).abs() < 0.5, "expected={exp}, dequantized={deq}");
403        }
404    }
405
406    #[test]
407    fn test_block_quantized_flip_dequantize() {
408        use ruda_tensor::quantization::BlockSize;
409
410        let values = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
411        let tensor = HostTensor::from_data(TensorData::new(values, [2, 4]));
412
413        let block_size = BlockSize::new([4]);
414        let scheme = QuantScheme::default()
415            .with_value(QuantValue::Q8S)
416            .with_level(QuantLevel::Block(block_size))
417            .with_store(QuantStore::Native);
418
419        let qtensor = Host::quantize_dynamic(tensor, &scheme);
420
421        // Flip along axis 0: [[5,6,7,8],[1,2,3,4]]
422        let flipped = Host::q_flip(qtensor, &[0]);
423        assert_eq!(flipped.tensor().shape().to_vec(), vec![2, 4]);
424
425        let result = Host::dequantize(flipped, FloatDType::F32);
426        let result_vals: &[f32] = result.storage();
427        let expected = [5.0f32, 6.0, 7.0, 8.0, 1.0, 2.0, 3.0, 4.0];
428        for (exp, deq) in expected.iter().zip(result_vals.iter()) {
429            assert!(
430                (exp - deq).abs() < 0.15,
431                "expected={exp}, dequantized={deq}"
432            );
433        }
434    }
435
436    #[test]
437    fn test_quantize_dynamic_f64_tensor() {
438        use ruda_tensor::quantization::QuantValue;
439
440        let values = vec![0.0f64, 1.0, 2.0, 3.0, 4.0, 5.0];
441        let tensor = HostTensor::new(
442            Bytes::from_elems(values),
443            Layout::contiguous([6].into()),
444            DType::F64,
445        );
446        assert_eq!(tensor.dtype(), DType::F64);
447
448        let scheme = QuantScheme::default()
449            .with_value(QuantValue::Q8S)
450            .with_store(QuantStore::Native);
451
452        let qtensor = Host::quantize_dynamic(tensor, &scheme);
453        assert_eq!(qtensor.tensor().dtype(), DType::I8);
454
455        // Dequantize and verify round-trip accuracy
456        let result = Host::dequantize(qtensor, FloatDType::F32);
457        let result_vals: &[f32] = result.storage();
458        let expected = [0.0f32, 1.0, 2.0, 3.0, 4.0, 5.0];
459        for (exp, deq) in expected.iter().zip(result_vals.iter()) {
460            assert!(
461                (exp - deq).abs() < 0.15,
462                "expected={exp}, dequantized={deq}"
463            );
464        }
465    }
466
467    #[test]
468    fn test_dequantize_f64() {
469        let values = vec![0.0f32, 1.0, 2.0, 3.0];
470        let tensor = HostTensor::from_data(TensorData::new(values.clone(), [4]));
471
472        let scheme = QuantScheme::default()
473            .with_value(QuantValue::Q8S)
474            .with_store(QuantStore::Native);
475
476        let qtensor = Host::quantize_dynamic(tensor, &scheme);
477        let result = Host::dequantize(qtensor, FloatDType::F64);
478        assert_eq!(result.dtype(), DType::F64);
479        let result_vals: &[f64] = result.storage();
480        for (orig, deq) in values.iter().zip(result_vals.iter()) {
481            assert!(
482                (*orig as f64 - deq).abs() < 0.05,
483                "orig={orig}, dequantized={deq}"
484            );
485        }
486    }
487
488    #[test]
489    fn test_dequantize_f16() {
490        let values = vec![0.0f32, 1.0, 2.0, 3.0];
491        let tensor = HostTensor::from_data(TensorData::new(values.clone(), [4]));
492
493        let scheme = QuantScheme::default()
494            .with_value(QuantValue::Q8S)
495            .with_store(QuantStore::Native);
496
497        let qtensor = Host::quantize_dynamic(tensor, &scheme);
498        let result = Host::dequantize(qtensor, FloatDType::F16);
499        assert_eq!(result.dtype(), DType::F16);
500        let result_vals: &[f16] = result.storage();
501        for (orig, deq) in values.iter().zip(result_vals.iter()) {
502            assert!(
503                (*orig - f32::from(*deq)).abs() < 0.05,
504                "orig={orig}, dequantized={deq}"
505            );
506        }
507    }
508}