Skip to main content

cubecl_std/quant/
dequantize.rs

1use cubecl::prelude::*;
2use cubecl_common::quant::scheme::*;
3use cubecl_common::{e4m3, e5m2};
4use cubecl_core as cubecl;
5
6/// Dequantize a vector of values, where `vector_size * num_quants` is a power of two.
7/// Unaligned values can't be dequantized in place.
8///
9/// `scale` is the effective scale for these values: how many scale levels the scheme has and how
10/// they combine is the caller's business, folded before the call. This is what keeps the
11/// primitive per-read arithmetic, serving equally a one-level read, a view multiplying the
12/// per-tensor scale in per read, or a tile handing every read one register.
13/// `table` is a lookup scheme's `2^bits`-entry table, and must be present exactly when
14/// `scheme.mode` is [`QuantMode::Lookup`].
15#[cube]
16pub fn dequantize_aligned<Q: Scalar, S: CubePrimitive, F: Numeric, NQ: Size, NF: Size>(
17    value: Vector<Q, NQ>,
18    scale: S,
19    table: ComptimeOption<Box<[f32]>>,
20    #[comptime] scheme: QuantScheme,
21) -> Vector<F, NF> {
22    comptime!(crate::quant::check_table_bindings(&scheme, table.is_some()));
23
24    let q_values = match scheme.store {
25        QuantStore::Native | QuantStore::PackedNative(_) => Vector::<F, NF>::cast_from(value),
26        QuantStore::PackedU32(_) => {
27            unpack_cast_u32::<F, NQ, NF>(Vector::cast_from(value), table.clone(), scheme)
28        }
29    };
30
31    match scheme.mode {
32        // Lookup already resolved the field to its table entry in the unpack; both modes are one
33        // scale multiply from there.
34        QuantMode::Symmetric | QuantMode::Lookup => q_values * Vector::<F, NF>::cast_from(scale),
35    }
36}
37
38/// [`dequantize_aligned`] for a scale a caller folded in f32, forming the product there too and
39/// narrowing only the result.
40///
41/// A folded scale reaches further down than what it produces: one below `F`'s smallest subnormal
42/// still scales quantized values into ordinary `F` ones. Narrowing it to `F` first rounds it to
43/// zero and takes the whole read with it, which is the failure two-level quantization exists to
44/// avoid in the first place.
45#[cube]
46pub fn dequantize_aligned_wide<Q: Scalar, F: Numeric, NQ: Size, NF: Size>(
47    value: Vector<Q, NQ>,
48    scale: f32,
49    table: ComptimeOption<Box<[f32]>>,
50    #[comptime] scheme: QuantScheme,
51) -> Vector<F, NF> {
52    Vector::<F, NF>::cast_from(dequantize_aligned::<Q, f32, f32, NQ, NF>(
53        value, scale, table, scheme,
54    ))
55}
56
57/// The effective scale of values whose per-tensor scale multiplies on top of their block scale.
58///
59/// The two multiply in f32: a block scale is normalized against the per-tensor one, so on its own
60/// it overflows a narrow compute type by orders of magnitude before the global scale can bring the
61/// product back into range.
62#[cube]
63pub fn multiply_global_scale<S: CubePrimitive>(global_scale: f32, scale: S) -> f32 {
64    global_scale * f32::cast_from(scale)
65}
66
67/// Unpack a set of values from u32, and convert to the specified floating point format.
68/// `table` decodes each field under [`QuantMode::Lookup`] and must be `None` otherwise
69/// ([`dequantize_aligned`] checks the pairing).
70#[cube]
71pub fn unpack_cast_u32<F: Numeric, NQ: Size, NF: Size>(
72    value: Vector<u32, NQ>,
73    table: ComptimeOption<Box<[f32]>>,
74    #[comptime] scheme: QuantScheme,
75) -> Vector<F, NF> {
76    let num_quants = scheme.num_quants();
77    let native_packing = scheme.native_packing();
78    let size_bits = scheme.size_bits_value();
79    let mask = comptime![packing_mask(scheme)];
80    let size!(NP) = native_packing;
81
82    let mut out = Vector::<F, NF>::empty();
83
84    #[unroll]
85    for vector_idx in 0..value.vector_size() {
86        let packed_val = value.extract(vector_idx);
87        let out_offset = vector_idx * num_quants;
88        #[unroll]
89        for packed_idx in range_stepped(0, num_quants, native_packing) {
90            let shift = packed_idx * size_bits;
91            let value = (packed_val >> shift as u32) & mask;
92
93            let float_value = cast_masked::<F, NP>(value, table.clone(), scheme);
94
95            #[unroll]
96            for native_idx in 0..native_packing {
97                let out_offset = out_offset + packed_idx + native_idx;
98                out.insert(out_offset, float_value.extract(native_idx));
99            }
100        }
101    }
102
103    out
104}
105
106/// Unpack `NF` consecutive fields of one `u32` word starting at field `first` (a runtime index),
107/// cast but **unscaled** — the caller multiplies by whatever scale its lines carry. The sub-word
108/// counterpart of [`unpack_cast_u32`], for reads whose served line is narrower than a word:
109/// `NF` may be any divisor of the packing factor, and `first` selects which slice of the word
110/// this line is. `e2m1` is refused — its native pairs cannot be split at a field boundary.
111///
112/// **The caller must keep `first + NF <= num_quants`.** `first` is runtime, so nothing here can
113/// check it, and a shift at or past 32 is not an error on most ISAs — the hardware masks the
114/// shift amount to 5 bits and the read silently lands on the wrong fields.
115#[cube]
116pub fn unpack_fields<F: Numeric, NF: Size>(
117    word: u32,
118    first: u32,
119    table: ComptimeOption<Box<[f32]>>,
120    #[comptime] scheme: QuantScheme,
121) -> Vector<F, NF> {
122    comptime!(assert!(
123        !matches!(scheme.value, QuantValue::E2M1),
124        "unpack_fields: e2m1 decodes in native pairs, which a sub-word line would split"
125    ));
126    let size_bits = scheme.size_bits_value();
127    let mask = comptime![packing_mask(scheme)];
128    let size!(N1) = 1usize;
129
130    let mut out = Vector::<F, NF>::empty();
131    #[unroll]
132    for j in 0..NF::value() {
133        let shift = (first + j as u32) * size_bits as u32;
134        let field = (word >> shift) & mask;
135        let value = cast_masked::<F, N1>(field, table.clone(), scheme);
136        out.insert(j, value.extract(0usize));
137    }
138    out
139}
140
141/// The mask required for each packed value, taking into account the native packing required for
142/// `e2m1`.
143fn packing_mask(scheme: QuantScheme) -> u32 {
144    let bits = match scheme.value {
145        QuantValue::E2M1 => 8, // Packed conversion
146        other => other.size_bits(),
147    };
148    (1u32 << bits) - 1
149}
150
151/// Cast a masked-out value in the low `n` bits of a `u32` to the specified float type.
152/// With a `table` the value is an index and the cast is its lookup; otherwise sign conversion
153/// is applied for integer quantization before casting to the float type,
154/// while minifloats are simply truncated to `u8`, reinterpreted and then cast.
155/// `e2m1` decodes its packed pair in software ([`e2m1_packed_bits_to_float`]) rather than through
156/// the `e2m1x2` type: that cast lowers on CUDA alone, and this is the decoder every quantized read
157/// on every backend goes through.
158///
159/// # Returns
160/// Two floating point numbers for `e2m1`, one for all other formats.
161#[cube]
162fn cast_masked<F: Numeric, N: Size>(
163    value: u32,
164    table: ComptimeOption<Box<[f32]>>,
165    #[comptime] scheme: QuantScheme,
166) -> Vector<F, N> {
167    #[comptime]
168    match table {
169        // The field indexes the table; the mask already bounds it to `2^bits`, the table's
170        // required length.
171        ComptimeOption::Some(t) => Vector::<F, N>::cast_from(t[value as usize]),
172        ComptimeOption::None => cast_masked_plain::<F, N>(value, scheme),
173    }
174}
175
176/// The tableless arm of [`cast_masked`]: sign conversion for the integers, bit reinterpretation
177/// for the minifloats.
178#[cube]
179fn cast_masked_plain<F: Numeric, N: Size>(
180    value: u32,
181    #[comptime] scheme: QuantScheme,
182) -> Vector<F, N> {
183    match scheme.value {
184        // For minifloat we can assume if they're supported then u8 is supported
185        QuantValue::E5M2 => Vector::<F, N>::cast_from(e5m2::from_bits(value as u8)),
186        QuantValue::E4M3 => Vector::<F, N>::cast_from(e4m3::from_bits(value as u8)),
187        QuantValue::E2M1 => crate::quant::fp4::e2m1_packed_bits_to_float::<F, N>(value),
188        QuantValue::Q8F
189        | QuantValue::Q4F
190        | QuantValue::Q2F
191        | QuantValue::Q8S
192        | QuantValue::Q4S
193        | QuantValue::Q2S => {
194            let size_quant = scheme.size_bits_value() as u32;
195            let sign_bit = 1u32 << (size_quant - 1);
196
197            // Branchless sign extension: `(raw ^ s) - s` with `s = 2^(n-1)` runs
198            // the identical xor/sub on every lane — two uniform vector ops on
199            // SIMD backends instead of a compare/select chain.
200            let signed_value = (value ^ sign_bit) as i32 - sign_bit as i32;
201            Vector::<F, N>::cast_from(signed_value)
202        }
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use cubecl_core::ir::{ElemType, Scope, UIntKind};
210    use cubecl_core::{define_size, ir::settings::Dim3};
211
212    define_size!(N1);
213
214    /// A root scope carries no typemap; the launcher normally picks the index width.
215    fn test_scope() -> Scope {
216        let scope = Scope::root(KernelSettings::new(
217            Dim3::new_single(),
218            ExecutionMode::Checked,
219            AddressType::U32,
220        ));
221        scope.register_size::<N1>(1);
222        scope.register_type::<usize>(ElemType::UInt(UIntKind::U32));
223        scope
224    }
225
226    /// The primitive is level-agnostic: it takes the effective scale for the values it unpacks,
227    /// and how many levels folded into that scale is the caller's business.
228    #[test]
229    fn expanding_takes_one_scale_whatever_the_levels() {
230        let scope = test_scope();
231        let one = f32::__expand_new(&scope, 1.0);
232        let value = Vector::<f32, N1>::__expand_new(&scope, one);
233
234        for scheme in [
235            QuantScheme::default(),
236            QuantScheme::default().per_block([32], ScaleDtype::F32),
237            QuantScheme::default()
238                .per_block([32], ScaleDtype::F32)
239                .per_tensor(ScaleDtype::F32),
240        ] {
241            dequantize_aligned::expand::<f32, f32, f32, N1, N1>(
242                &scope,
243                value,
244                one,
245                ComptimeOptionExpand::None,
246                scheme,
247            );
248        }
249    }
250}