1pub use cubecl_common::quant::scheme::{
5 BlockSize, QuantLevel, QuantMode, QuantParam, QuantScheme, QuantStore, QuantValue,
6};
7
8pub const QPARAM_ALIGN: usize = core::mem::align_of::<f32>();
14
15use alloc::vec::Vec;
16use core::any::TypeId;
17use cubecl_common::e4m3;
18use num_traits::PrimInt;
19use serde::{Deserialize, Serialize};
20
21use crate::{DType, Metadata, Shape, bytes::Bytes};
22
23#[derive(new, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
29pub struct QuantConfig {
30 pub scheme: QuantScheme,
32 pub propagation: QuantPropagation,
34 }
38
39#[derive(
40 Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default,
41)]
42pub enum QuantAcc {
44 #[default]
46 F32,
47 F16,
49 BF16,
51}
52
53pub enum Calibration {
55 MinMax,
57 AbsMean,
62}
63
64#[derive(
67 Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default,
68)]
69pub enum QuantPropagation {
70 Propagate,
72 #[default]
74 Inhibit,
75}
76
77#[derive(Clone, Debug)]
79pub struct QParams<S> {
80 pub scales: S,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct QParamTensor {
87 pub offset_start: usize,
89 pub offset_end: usize,
91 pub metadata: Metadata,
93 pub dtype: DType,
95}
96
97pub fn params_shape(data_shape: &Shape, level: QuantLevel) -> Shape {
99 match level {
100 QuantLevel::Tensor => Shape::new([1]),
101 QuantLevel::Block(block_size) => {
102 let mut params_shape = data_shape.clone();
103 let block_size = block_size.to_dim_vec(data_shape.num_dims());
104
105 for (shape, block_size) in params_shape.iter_mut().zip(block_size) {
106 *shape = (*shape).div_ceil(block_size as usize);
107 }
108
109 params_shape
110 }
111 QuantLevel::BlockTensor { .. } => {
112 unimplemented!("two-level quantization is not supported yet, got {level:?}")
113 }
114 }
115}
116
117pub struct QuantizedBytes {
127 pub bytes: Bytes,
129 pub scheme: QuantScheme,
131 pub num_elements: usize,
133}
134
135impl QuantizedBytes {
136 pub fn new<E: bytemuck::CheckedBitPattern + bytemuck::NoUninit>(
138 value: Vec<E>,
139 scheme: QuantScheme,
140 scales: &[f32],
141 ) -> Self {
142 let num_elements = value.len();
143 if TypeId::of::<E>() != TypeId::of::<i8>() {
145 panic!("Invalid quantized type");
146 }
147
148 let i8s: Vec<i8> = bytemuck::allocation::cast_vec(value);
150 let mut bytes = Bytes::from_elems(i8s);
151
152 let scales = match scheme.level {
153 QuantLevel::Tensor => &scales[..1],
154 QuantLevel::Block(_block_size) => scales,
155 QuantLevel::BlockTensor { .. } => unimplemented!(
156 "two-level quantization is not supported yet, got {:?}",
157 scheme.level
158 ),
159 };
160 let scale_bytes = encode_scales(scales, scheme.param);
161 bytes.extend_from_byte_slice_aligned(scale_bytes.as_slice(), QPARAM_ALIGN);
162
163 Self {
164 bytes,
165 scheme,
166 num_elements,
167 }
168 }
169
170 pub fn into_vec_i8(self) -> (Vec<i8>, QParams<Vec<f32>>) {
172 let param = self.scheme.param;
173 let (values, (qparams, num_params)) = self.split_values_off();
174
175 let scales_size = scale_size(param) * num_params;
181 let scales = decode_scales(&qparams[qparams.len() - scales_size..], param);
182
183 (values, QParams { scales })
184 }
185
186 fn split_i8_values(self, scale_bytes: usize) -> (Vec<i8>, Vec<u8>) {
187 let mut values = read_bytes_to_i8(self.bytes);
188
189 let values_end = values.len() - scale_bytes;
190 let qparams = values.split_off(values_end);
191
192 (values, bytemuck::cast_vec(qparams))
193 }
194
195 fn split_values_off(self) -> (Vec<i8>, (Vec<u8>, usize)) {
200 let num_params = match self.scheme.level {
201 QuantLevel::Tensor => 1,
202 QuantLevel::Block(block_size) => self.num_elements / block_size.num_elements(),
203 QuantLevel::BlockTensor { .. } => unimplemented!(
204 "two-level quantization is not supported yet, got {:?}",
205 self.scheme.level
206 ),
207 };
208 let scale_bytes = scale_size(self.scheme.param) * num_params;
209
210 if let QuantStore::PackedU32(packed_dim) = self.scheme.store {
211 assert_eq!(
212 packed_dim, 0,
213 "Packing must be on innermost dimension for splitting off values"
214 );
215 }
216
217 let (values, qparams) = match self.scheme.store {
218 QuantStore::Native => self.split_i8_values(scale_bytes),
219 QuantStore::PackedU32(_) => match self.scheme.value {
220 QuantValue::Q8F | QuantValue::Q8S => self.split_i8_values(scale_bytes),
221 QuantValue::Q4F | QuantValue::Q4S | QuantValue::Q2F | QuantValue::Q2S => {
222 let split_at = self.bytes.len() - scale_bytes;
223 let qparams = self.bytes[split_at..].to_vec();
224 let values = bytemuck::cast_slice::<_, u32>(&self.bytes[..split_at]);
225 let values = unpack_q_to_i8s(values, self.num_elements, &self.scheme.value);
227 (values, qparams)
228 }
229 QuantValue::E4M3 | QuantValue::E5M2 | QuantValue::E2M1 => {
230 unimplemented!("Not yet supported")
231 }
232 },
233 QuantStore::PackedNative(_) => unimplemented!("Not yet supported"),
234 };
235
236 (values, (qparams, num_params))
237 }
238}
239
240pub fn scale_to_param(scale: f32, param: QuantParam) -> f32 {
250 let nearest = match param {
251 QuantParam::F32 => return scale,
252 QuantParam::F16 => crate::f16::from_f32(scale).to_f32(),
253 QuantParam::BF16 => crate::bf16::from_f32(scale).to_f32(),
254 QuantParam::UE4M3 => e4m3::from_f32(scale).to_f32(),
255 QuantParam::UE8M0 => unimplemented!("UE8M0 scales are not yet supported"),
256 };
257
258 if nearest >= scale || scale.is_nan() {
259 return nearest;
260 }
261
262 let next = match param {
265 QuantParam::F16 => {
266 crate::f16::from_bits(crate::f16::from_f32(nearest).to_bits() + 1).to_f32()
267 }
268 QuantParam::BF16 => {
269 crate::bf16::from_bits(crate::bf16::from_f32(nearest).to_bits() + 1).to_f32()
270 }
271 QuantParam::UE4M3 => e4m3::from_bits(e4m3::from_f32(nearest).to_bits() + 1).to_f32(),
272 QuantParam::F32 | QuantParam::UE8M0 => unreachable!(),
273 };
274
275 if next.is_finite() { next } else { nearest }
278}
279
280fn scale_size(param: QuantParam) -> usize {
282 match param {
283 QuantParam::F32 => 4,
284 QuantParam::F16 | QuantParam::BF16 => 2,
285 QuantParam::UE8M0 | QuantParam::UE4M3 => 1,
286 }
287}
288
289fn decode_scales(bytes: &[u8], param: QuantParam) -> Vec<f32> {
291 match param {
292 QuantParam::F32 => bytes
293 .chunks_exact(4)
294 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
295 .collect(),
296 QuantParam::F16 => bytes
297 .chunks_exact(2)
298 .map(|c| crate::f16::from_ne_bytes([c[0], c[1]]).to_f32())
299 .collect(),
300 QuantParam::BF16 => bytes
301 .chunks_exact(2)
302 .map(|c| crate::bf16::from_ne_bytes([c[0], c[1]]).to_f32())
303 .collect(),
304 QuantParam::UE4M3 => bytes.iter().map(|b| e4m3::from_bits(*b).to_f32()).collect(),
305 QuantParam::UE8M0 => unimplemented!("UE8M0 scales are not yet supported"),
306 }
307}
308
309fn encode_scales(scales: &[f32], param: QuantParam) -> Vec<u8> {
311 match param {
312 QuantParam::F32 => scales.iter().flat_map(|s| s.to_ne_bytes()).collect(),
313 QuantParam::F16 => scales
314 .iter()
315 .flat_map(|s| crate::f16::from_f32(*s).to_ne_bytes())
316 .collect(),
317 QuantParam::BF16 => scales
318 .iter()
319 .flat_map(|s| crate::bf16::from_f32(*s).to_ne_bytes())
320 .collect(),
321 QuantParam::UE4M3 => scales
322 .iter()
323 .map(|s| e4m3::from_f32(*s).to_bits())
324 .collect(),
325 QuantParam::UE8M0 => unimplemented!("UE8M0 scales are not yet supported"),
326 }
327}
328
329fn read_bytes_to_i8(bytes: Bytes) -> Vec<i8> {
330 match bytes.try_into_vec::<i8>() {
331 Ok(val) => val,
332 Err(bytes) => unsafe { core::mem::transmute::<Vec<u8>, Vec<i8>>(bytes.to_vec()) },
336 }
337}
338
339pub fn pack_i8s_to_u32s(values: Vec<i8>) -> Vec<u32> {
341 #[cfg(target_endian = "big")]
345 {
346 values
347 .chunks(4)
348 .map(|x| {
349 x.iter()
350 .enumerate()
351 .fold(0u32, |acc, (i, x)| acc | (*x as u32 & 0xFF) << (i * 8))
352 })
353 .collect()
354 }
355
356 #[cfg(target_endian = "little")]
359 {
360 let mut values = values;
361 let remainder = values.len() % 4;
362 if remainder != 0 {
363 values.extend(core::iter::repeat_n(0, 4 - remainder));
365 }
366
367 let len = values.len() / 4;
368 let capacity = values.capacity() / 4;
369
370 let mut values = core::mem::ManuallyDrop::new(values);
372 let ptr = values.as_mut_ptr() as *mut u32;
373
374 unsafe { Vec::from_raw_parts(ptr, len, capacity) }
375 }
376}
377
378pub(crate) fn unpack_q_to_i8s<Q: PrimInt>(
380 values: &[Q],
381 numel: usize,
382 value: &QuantValue,
383) -> Vec<i8> {
384 let size_store = size_of::<Q>() * 8;
385 let size_quant = value.size_bits();
386 let num_quants = size_store / size_quant;
387 let mask = Q::from((1 << size_quant) - 1).unwrap();
388 let sign_shift = 8 - size_quant; values
390 .iter()
391 .enumerate()
392 .flat_map(|(i, &packed)| {
393 let n = core::cmp::min(num_quants, numel - i * num_quants);
395 (0..n).map(move |i| {
402 let raw = (packed >> (i * size_quant) & mask).to_u8().unwrap();
403 ((raw << sign_shift) as i8) >> sign_shift
404 })
405 })
406 .collect()
407}
408
409#[cfg(test)]
410mod tests {
411
412 use super::*;
413 use alloc::vec;
414
415 #[test]
416 fn should_pack_i8s_to_u32() {
417 let packed = pack_i8s_to_u32s(vec![-128, 2, -3, 127]);
418
419 assert_eq!(packed, vec![2147287680]);
420 }
421
422 #[test]
423 fn should_pack_i8s_to_u32_padded() {
424 let packed = pack_i8s_to_u32s(vec![-128, 2, -3, 127, 55]);
425 let packed_padded = pack_i8s_to_u32s(vec![-128, 2, -3, 127, 55, 0, 0, 0]);
426
427 assert_eq!(packed, vec![2147287680, 55]);
428 assert_eq!(packed, packed_padded);
429 }
430
431 #[test]
432 fn should_unpack_u32s_to_i8s() {
433 let unpacked = unpack_q_to_i8s(&[2147287680u32], 4, &QuantValue::Q8S);
434
435 assert_eq!(unpacked, vec![-128, 2, -3, 127]);
436 }
437
438 #[test]
439 fn should_unpack_u32s_to_i8s_padded() {
440 let unpacked = unpack_q_to_i8s(&[55u32], 1, &QuantValue::Q8S);
441
442 assert_eq!(unpacked, vec![55]);
443 }
444
445 #[test]
446 fn should_unpack_u32s_to_i8s_arange() {
447 let unpacked = unpack_q_to_i8s(
448 &[
449 0u32, 286331136, 286331153, 572657937, 572662306, 857874978, 858993459, 858993459,
450 1145324612, 1145324612, 1431655748, 1431655765, 1717982549, 1717986918, 2003199590,
451 2004318071,
452 ],
453 128,
454 &QuantValue::Q4S,
455 );
456
457 assert_eq!(
458 unpacked,
459 vec![
460 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
461 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
462 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5,
463 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
464 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7
465 ]
466 );
467 }
468
469 #[test]
470 fn should_pack_unpack_quantization_parameters_per_tensor_symmetric() {
471 let scale = 0.03937008;
473 let values = vec![0i8, 25, 51, 76, 102, 127];
474
475 let q_bytes = QuantizedBytes::new(
476 values.clone(),
477 QuantScheme::default()
478 .with_value(QuantValue::Q8S)
479 .with_store(QuantStore::Native),
480 &[scale],
481 );
482
483 let (q_values, qparams) = q_bytes.into_vec_i8();
484
485 assert_eq!(qparams.scales, vec![scale]);
486
487 assert_eq!(q_values, values);
488 }
489
490 #[test]
494 fn scale_to_param_survives_the_codec() {
495 let scales = [0.5f32, 0.3, 1.0 / 3.0, 500.0, 1e-3, 7.7e-4];
498
499 for param in [
500 QuantParam::F32,
501 QuantParam::F16,
502 QuantParam::BF16,
503 QuantParam::UE4M3,
504 ] {
505 let rounded: Vec<f32> = scales.iter().map(|s| scale_to_param(*s, param)).collect();
506 let via_codec = decode_scales(&encode_scales(&rounded, param), param);
507
508 assert_eq!(
509 rounded, via_codec,
510 "the codec moves a scale {param:?} can already represent"
511 );
512 for (scale, rounded) in scales.iter().zip(&rounded).filter(|(s, _)| **s < 500.0) {
514 assert!(
515 rounded >= scale,
516 "{scale} rounded down to {rounded} for {param:?}"
517 );
518 }
519 }
520 }
521
522 #[test]
525 fn encoded_scale_width_matches_scale_size() {
526 let scales = [0.5f32, 0.25, 0.125];
527
528 for param in [
529 QuantParam::F32,
530 QuantParam::F16,
531 QuantParam::BF16,
532 QuantParam::UE4M3,
533 ] {
534 assert_eq!(
535 encode_scales(&scales, param).len(),
536 scale_size(param) * scales.len(),
537 "encoded width disagrees with scale_size for {param:?}"
538 );
539 }
540 }
541
542 #[test]
543 fn should_pack_unpack_ue4m3_block_scales() {
544 let scales = [0.5f32, 0.125];
547 let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
548
549 let q_bytes = QuantizedBytes::new(
550 values.clone(),
551 QuantScheme::default()
552 .with_value(QuantValue::Q8S)
553 .with_store(QuantStore::Native)
554 .with_level(QuantLevel::block([4]))
555 .with_param(QuantParam::UE4M3),
556 &scales,
557 );
558
559 let (q_values, qparams) = q_bytes.into_vec_i8();
560
561 assert_eq!(qparams.scales, scales);
562 assert_eq!(q_values, values);
563 }
564}