Skip to main content

cubecl_std/quant/
dequantize.rs

1use cubecl::prelude::*;
2use cubecl_common::quant::scheme::*;
3use cubecl_common::{e2m1x2, 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/// For `e2m1`, casting is done on the packed `e2m1x2` representation.
156///
157/// # Returns
158/// Two floating point numbers for `e2m1`, one for all other formats.
159#[cube]
160fn cast_masked<F: Numeric, N: Size>(
161    value: u32,
162    table: ComptimeOption<Box<[f32]>>,
163    #[comptime] scheme: QuantScheme,
164) -> Vector<F, N> {
165    #[comptime]
166    match table {
167        // The field indexes the table; the mask already bounds it to `2^bits`, the table's
168        // required length.
169        ComptimeOption::Some(t) => Vector::<F, N>::cast_from(t[value as usize]),
170        ComptimeOption::None => cast_masked_plain::<F, N>(value, scheme),
171    }
172}
173
174/// The tableless arm of [`cast_masked`]: sign conversion for the integers, bit reinterpretation
175/// for the minifloats.
176#[cube]
177fn cast_masked_plain<F: Numeric, N: Size>(
178    value: u32,
179    #[comptime] scheme: QuantScheme,
180) -> Vector<F, N> {
181    match scheme.value {
182        // For minifloat we can assume if they're supported then u8 is supported
183        QuantValue::E5M2 => Vector::<F, N>::cast_from(e5m2::from_bits(value as u8)),
184        QuantValue::E4M3 => Vector::<F, N>::cast_from(e4m3::from_bits(value as u8)),
185        QuantValue::E2M1 => Vector::<F, N>::cast_from(e2m1x2::from_bits(value as u8)),
186        QuantValue::Q8F
187        | QuantValue::Q4F
188        | QuantValue::Q2F
189        | QuantValue::Q8S
190        | QuantValue::Q4S
191        | QuantValue::Q2S => {
192            let size_quant = scheme.size_bits_value() as u32;
193            let sign_bit = 1u32 << (size_quant - 1);
194
195            // Branchless sign extension: `(raw ^ s) - s` with `s = 2^(n-1)` runs
196            // the identical xor/sub on every lane — two uniform vector ops on
197            // SIMD backends instead of a compare/select chain.
198            let signed_value = (value ^ sign_bit) as i32 - sign_bit as i32;
199            Vector::<F, N>::cast_from(signed_value)
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207    use cubecl_core::ir::{ElemType, Scope, UIntKind};
208    use cubecl_core::{define_size, ir::settings::Dim3};
209
210    define_size!(N1);
211
212    /// A root scope carries no typemap; the launcher normally picks the index width.
213    fn test_scope() -> Scope {
214        let scope = Scope::root(KernelSettings::new(
215            Dim3::new_single(),
216            ExecutionMode::Checked,
217            AddressType::U32,
218        ));
219        scope.register_size::<N1>(1);
220        scope.register_type::<usize>(ElemType::UInt(UIntKind::U32));
221        scope
222    }
223
224    /// The primitive is level-agnostic: it takes the effective scale for the values it unpacks,
225    /// and how many levels folded into that scale is the caller's business.
226    #[test]
227    fn expanding_takes_one_scale_whatever_the_levels() {
228        let scope = test_scope();
229        let one = f32::__expand_new(&scope, 1.0);
230        let value = Vector::<f32, N1>::__expand_new(&scope, one);
231
232        for scheme in [
233            QuantScheme::default(),
234            QuantScheme::default().per_block([32], ScaleDtype::F32),
235            QuantScheme::default()
236                .per_block([32], ScaleDtype::F32)
237                .per_tensor(ScaleDtype::F32),
238        ] {
239            dequantize_aligned::expand::<f32, f32, f32, N1, N1>(
240                &scope,
241                value,
242                one,
243                ComptimeOptionExpand::None,
244                scheme,
245            );
246        }
247    }
248}