Skip to main content

cubecl_tile/tile/packing/
base.rs

1//! How an operand's values sit in memory.
2
3use cubecl::client::Client;
4use cubecl::e2m1x2;
5use cubecl::ir::features::TypeUsage;
6use cubecl::ir::types::Fp8Format;
7use cubecl::ir::{ElemType, FloatKind, IntKind};
8use cubecl::post_processing::fp4::E2M1_F16_LIFT;
9use cubecl::post_processing::minifloat::fp8_f16_lift;
10use cubecl::prelude::Scalar;
11use cubecl::quant::scheme::QuantValue;
12use cubecl::quant::scheme::ScaleDtype;
13
14/// How an operand's values sit in memory.
15#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
16pub(crate) enum Packing {
17    /// Served as stored.
18    Plain,
19    /// Several `field`-wide values per stored `u32`, unpacked at the read.
20    Packed {
21        /// The slot one value occupies.
22        field: Field,
23    },
24}
25
26/// The slot one packed value occupies.
27#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
28pub enum Field {
29    Quant(QuantValue),
30    Fp8(Fp8Format),
31    Float(FloatKind),
32    /// An index `bits` wide into a table, its codebook
33    /// ([`TileArgLaunch::codebook`](crate::TileArgLaunch::codebook)), read as raw bits.
34    Index {
35        bits: usize,
36    },
37    /// An `e2m1` code on a device that converts `e2m1x2` itself: a word is read as four of them
38    /// and cast, the decode left to the compiler. What [`read_on`](Field::read_on) makes of
39    /// `Quant(E2M1)` there.
40    ConvertedE2M1,
41    /// A float code (`e2m1`, `e4m3` or `e5m2`) on a device that emulates converting it: a word is
42    /// decoded as `f16` pairs from its bits, lifted to its values, or, where `lifted` is false,
43    /// each placed `lift` short of its value for a reader whose factor carries the
44    /// lift (a tile read placed). What [`read_on`](Field::read_on) makes of `Quant(value)` there,
45    /// lifted.
46    Placed {
47        value: QuantValue,
48        lifted: bool,
49    },
50}
51
52impl From<QuantValue> for Field {
53    fn from(value: QuantValue) -> Self {
54        Field::Quant(value)
55    }
56}
57
58impl From<Fp8Format> for Field {
59    fn from(format: Fp8Format) -> Self {
60        Field::Fp8(format)
61    }
62}
63
64/// An 8-bit float is a [`Fp8`](Field::Fp8) field, a wider one a [`Float`](Field::Float) field.
65impl From<FloatKind> for Field {
66    fn from(kind: FloatKind) -> Self {
67        Field::of_float(kind)
68    }
69}
70
71impl Field {
72    /// The slot's width in bits.
73    pub fn size_bits(self) -> usize {
74        match self {
75            Field::Quant(value) => value.size_bits(),
76            Field::Fp8(_) => 8,
77            Field::Float(kind) => Field::float_bits(kind),
78            Field::Index { bits } => bits,
79            Field::ConvertedE2M1 => QuantValue::E2M1.size_bits(),
80            Field::Placed { value, .. } => value.size_bits(),
81        }
82    }
83
84    /// Values one `u32` holds.
85    pub fn per_word(self) -> usize {
86        u32::BITS as usize / self.size_bits()
87    }
88
89    /// Whether the slot holds an `e2m1` code, however the device reads it
90    /// ([`read_on`](Field::read_on)): what a reader that takes the codes' values whole asks.
91    pub fn holds_e2m1(self) -> bool {
92        matches!(
93            self,
94            Field::Quant(QuantValue::E2M1)
95                | Field::ConvertedE2M1
96                | Field::Placed {
97                    value: QuantValue::E2M1,
98                    ..
99                }
100        )
101    }
102}
103
104impl Field {
105    /// This field as the device `client` reads it: a float code (`e2m1`, `e4m3`, `e5m2`) decoded
106    /// from its bits where the device emulates converting it anyway, so a reader's factor can
107    /// carry the decode's lift; an `e2m1` code converted by the device where it converts
108    /// `e2m1x2` itself; every other field as it is. A device's own conversion is the one its
109    /// compiler lowers best, so where it exists a software decode is only ever slower.
110    pub fn read_on(self, client: &Client) -> Field {
111        let converted_as = match self {
112            Field::Quant(QuantValue::E2M1) => Some(FloatKind::E2M1x2),
113            Field::Quant(QuantValue::E4M3) => Some(FloatKind::E4M3),
114            Field::Quant(QuantValue::E5M2) => Some(FloatKind::E5M2),
115            _ => None,
116        };
117        let emulated = converted_as.is_some_and(|kind| {
118            client
119                .features()
120                .conversion_is_emulated(ElemType::Float(kind))
121        });
122        let converts = e2m1x2::supported_uses(client).contains(TypeUsage::Conversion);
123        match self {
124            Field::Quant(value) if emulated => Field::Placed {
125                value,
126                lifted: true,
127            },
128            Field::Quant(QuantValue::E2M1) if converts => Field::ConvertedE2M1,
129            other => other,
130        }
131    }
132
133    /// This field read placed ([`Tile::placed`](crate::Tile::placed)): a float code decoded
134    /// from its bits, its lift left to the reader's factor; every other field as it is.
135    pub(crate) fn placed(self) -> Field {
136        match self {
137            Field::Placed { value, .. } => Field::Placed {
138                value,
139                lifted: false,
140            },
141            other => other,
142        }
143    }
144
145    /// What a value read this way is short of: [`E2M1_F16_LIFT`] for an `e2m1` code placed, the
146    /// format's [`fp8_f16_lift`] for an `e4m3` or `e5m2` one, one for every value read as itself.
147    pub(crate) fn lift(self) -> f64 {
148        match self.decode() {
149            FieldDecode::Placed { lifted: false } => E2M1_F16_LIFT,
150            FieldDecode::PlacedByte {
151                format,
152                lifted: false,
153            } => fp8_f16_lift(format),
154            _ => 1.0,
155        }
156    }
157
158    /// The field a float of `kind` occupies.
159    pub fn of_float(kind: FloatKind) -> Field {
160        match kind {
161            FloatKind::E4M3 => Field::Fp8(Fp8Format::E4M3),
162            FloatKind::E5M2 => Field::Fp8(Fp8Format::E5M2),
163            FloatKind::UE8M0 => Field::Fp8(Fp8Format::UE8M0),
164            other => Field::Float(other),
165        }
166    }
167
168    /// The field a scale of `dtype` occupies in its stored word.
169    pub fn of_scale(dtype: ScaleDtype) -> Field {
170        match ElemType::from_scale_dtype(dtype) {
171            ElemType::Float(kind) => Field::of_float(kind),
172            other => panic!("Field: a scale is stored as a float, and {dtype:?} is {other:?}"),
173        }
174    }
175
176    /// The number format a value of this field is written in: what an encoding copy rounds it to
177    /// ([`Tile::quantize_from`](crate::Tile::quantize_from)). A float code reads back from its
178    /// bits or through the device's conversion, but is written the same way either way.
179    ///
180    /// # Panics
181    ///
182    /// For a field no value is quantized to: an index, a whole float, an 8-bit scale code.
183    pub(crate) fn quantized_value(self) -> QuantValue {
184        match self {
185            Field::Quant(value) | Field::Placed { value, .. } => value,
186            Field::ConvertedE2M1 => QuantValue::E2M1,
187            other => panic!("Field: a value is quantized to a number format, not to {other:?}"),
188        }
189    }
190
191    /// The scale dtype a scale of this field is stored as: the inverse of
192    /// [`of_scale`](Field::of_scale).
193    ///
194    /// # Panics
195    ///
196    /// For a field no scale is stored as.
197    pub(crate) fn scale_dtype(self) -> ScaleDtype {
198        match self {
199            Field::Fp8(Fp8Format::E4M3) => ScaleDtype::UE4M3,
200            Field::Fp8(Fp8Format::UE8M0) => ScaleDtype::UE8M0,
201            Field::Float(FloatKind::F32) => ScaleDtype::F32,
202            Field::Float(FloatKind::F16) => ScaleDtype::F16,
203            Field::Float(FloatKind::BF16) => ScaleDtype::BF16,
204            other => panic!("Field: a scale is stored as a float, not as {other:?}"),
205        }
206    }
207
208    /// The bits a whole float occupies.
209    pub(crate) fn float_bits(kind: FloatKind) -> usize {
210        match kind {
211            FloatKind::F32 => 32,
212            FloatKind::F16 | FloatKind::BF16 => 16,
213            other => panic!(
214                "Field::Float: a stored float is 16 or 32 bits wide, got {other:?}; an 8-bit \
215                 code is Field::Fp8"
216            ),
217        }
218    }
219
220    /// How this field reads back.
221    pub(crate) fn decode(self) -> FieldDecode {
222        match self {
223            Field::Quant(
224                QuantValue::Q8F
225                | QuantValue::Q8S
226                | QuantValue::Q4F
227                | QuantValue::Q4S
228                | QuantValue::Q2F
229                | QuantValue::Q2S,
230            ) => FieldDecode::SignExtended,
231            Field::Quant(QuantValue::E2M1) => FieldDecode::Reinterpreted,
232            Field::Quant(QuantValue::E4M3) => FieldDecode::Byte(Fp8Format::E4M3),
233            Field::Quant(QuantValue::E5M2) => FieldDecode::Byte(Fp8Format::E5M2),
234            Field::Fp8(format) => FieldDecode::Byte(format),
235            Field::Float(kind) => FieldDecode::Bits(kind),
236            Field::Index { .. } => FieldDecode::Unsigned,
237            Field::ConvertedE2M1 => FieldDecode::Converted,
238            // A placed code is the field it was, decoded from its bits instead.
239            Field::Placed { value, lifted } => match Field::Quant(value).decode() {
240                FieldDecode::Reinterpreted => FieldDecode::Placed { lifted },
241                FieldDecode::Byte(format) => FieldDecode::PlacedByte { format, lifted },
242                other => {
243                    panic!("Field::Placed: {value:?} reads back as {other:?}, not a float code")
244                }
245            },
246        }
247    }
248}
249
250/// How a stored field reads back ([`Field::decode`]).
251#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
252pub(crate) enum FieldDecode {
253    /// An integer slot: the top bit is its sign, so the value sign-extends out of its bits.
254    SignExtended,
255    /// An index slot: its bits, unsigned.
256    Unsigned,
257    /// A 4-bit float code, read back by reinterpreting the byte two of them share.
258    Reinterpreted,
259    /// A 4-bit float code the device converts: a word cast as four `e2m1x2`.
260    Converted,
261    /// A 4-bit float code decoded from its bits as `f16` pairs, lifted to its value or placed
262    /// [`E2M1_F16_LIFT`] short of it.
263    Placed { lifted: bool },
264    /// An 8-bit float code decoded from its bits as `f16` pairs, lifted to its value or placed
265    /// [`fp8_f16_lift`] short of it.
266    PlacedByte { format: Fp8Format, lifted: bool },
267    /// An 8-bit float code, read back through the format's own decoder.
268    Byte(Fp8Format),
269    /// A whole float, read back by reinterpreting the bits of its slot.
270    Bits(FloatKind),
271}
272
273impl Packing {
274    /// The number format a value stored this way, as `element`, is written in: what an encoding
275    /// copy rounds it to ([`Tile::quantize_from`](crate::Tile::quantize_from)). A packed field's
276    /// ([`Field::quantized_value`]), or a plain `i8`'s, `e4m3`'s or `e5m2`'s.
277    ///
278    /// # Panics
279    ///
280    /// For a store no value is quantized to.
281    pub(crate) fn quantized_value(self, element: ElemType) -> QuantValue {
282        match (self, element) {
283            (Packing::Packed { field }, _) => field.quantized_value(),
284            (Packing::Plain, ElemType::Int(IntKind::I8)) => QuantValue::Q8S,
285            (Packing::Plain, ElemType::Float(FloatKind::E4M3)) => QuantValue::E4M3,
286            (Packing::Plain, ElemType::Float(FloatKind::E5M2)) => QuantValue::E5M2,
287            (Packing::Plain, other) => {
288                panic!("Packing: no value is quantized to a plain {other:?}")
289            }
290        }
291    }
292
293    /// The dtype a scale stored this way, as `element`, is rounded to: a packed field's
294    /// ([`Field::scale_dtype`]), or a plain float's.
295    ///
296    /// # Panics
297    ///
298    /// For a store no scale is held in.
299    pub(crate) fn scale_dtype(self, element: ElemType) -> ScaleDtype {
300        match (self, element) {
301            (Packing::Packed { field }, _) => field.scale_dtype(),
302            (Packing::Plain, ElemType::Float(kind)) => Field::of_float(kind).scale_dtype(),
303            (Packing::Plain, other) => panic!("Packing: a scale is a float, not {other:?}"),
304        }
305    }
306
307    /// This packing read placed ([`Field::placed`]).
308    pub(crate) fn placed(self) -> Packing {
309        match self {
310            Packing::Packed { field } => Packing::Packed {
311                field: field.placed(),
312            },
313            Packing::Plain => Packing::Plain,
314        }
315    }
316
317    /// What its values are short of ([`Field::lift`]).
318    pub(crate) fn lift(self) -> f64 {
319        match self {
320            Packing::Packed { field } => field.lift(),
321            Packing::Plain => 1.0,
322        }
323    }
324
325    /// This packing as the device `client` reads it ([`Field::read_on`]).
326    pub(crate) fn read_on(self, client: &Client) -> Packing {
327        match self {
328            Packing::Packed { field } => Packing::Packed {
329                field: field.read_on(client),
330            },
331            Packing::Plain => Packing::Plain,
332        }
333    }
334}
335
336impl Packing {
337    /// Values per stored element: one, unless a `u32` holds several fields.
338    fn factor(&self) -> usize {
339        match self {
340            Packing::Plain => 1,
341            Packing::Packed { field } => field.per_word(),
342        }
343    }
344
345    /// The physical line a `served`-wide logical line occupies.
346    pub(crate) fn physical(&self, served: usize) -> usize {
347        served / self.factor()
348    }
349
350    /// The width a binding `bound` wide serves.
351    pub(crate) fn served(&self, bound: usize) -> usize {
352        bound * self.factor()
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn a_packing_narrows_the_line_it_stores() {
362        assert_eq!(Packing::Plain.physical(16), 16);
363        assert_eq!(
364            Packing::Packed {
365                field: QuantValue::Q4S.into()
366            }
367            .physical(16),
368            2
369        );
370    }
371
372    #[test]
373    fn a_field_states_how_many_fit_in_a_word() {
374        assert_eq!(
375            Packing::Packed {
376                field: QuantValue::Q4S.into()
377            }
378            .factor(),
379            8
380        );
381        assert_eq!(
382            Packing::Packed {
383                field: QuantValue::Q8S.into()
384            }
385            .factor(),
386            4
387        );
388        assert_eq!(
389            Packing::Packed {
390                field: QuantValue::Q2S.into()
391            }
392            .factor(),
393            16
394        );
395        assert_eq!(
396            Packing::Packed {
397                field: Fp8Format::UE8M0.into()
398            }
399            .factor(),
400            4
401        );
402    }
403
404    #[test]
405    fn a_float_element_names_its_own_field() {
406        assert_eq!(
407            Field::of_float(FloatKind::UE8M0),
408            Field::Fp8(Fp8Format::UE8M0)
409        );
410        assert_eq!(
411            Field::of_float(FloatKind::E4M3),
412            Field::Fp8(Fp8Format::E4M3)
413        );
414        assert_eq!(
415            Field::of_float(FloatKind::F16),
416            Field::Float(FloatKind::F16)
417        );
418    }
419
420    #[test]
421    fn a_stored_float_is_a_field_of_its_own_width() {
422        assert_eq!(Field::Float(FloatKind::F32).per_word(), 1);
423        assert_eq!(Field::Float(FloatKind::F16).per_word(), 2);
424        assert_eq!(Field::Float(FloatKind::BF16).size_bits(), 16);
425        assert_eq!(
426            Field::from(FloatKind::F32).decode(),
427            FieldDecode::Bits(FloatKind::F32)
428        );
429    }
430
431    #[test]
432    fn a_byte_field_decodes_through_its_format() {
433        assert_eq!(
434            Field::from(QuantValue::E4M3).decode(),
435            FieldDecode::Byte(Fp8Format::E4M3)
436        );
437        assert_eq!(
438            Field::from(Fp8Format::UE8M0).decode(),
439            FieldDecode::Byte(Fp8Format::UE8M0)
440        );
441        assert_eq!(
442            Field::from(QuantValue::E2M1).decode(),
443            FieldDecode::Reinterpreted
444        );
445        assert_eq!(Field::ConvertedE2M1.decode(), FieldDecode::Converted);
446        assert_eq!(Field::ConvertedE2M1.per_word(), 8);
447    }
448
449    /// An `e2m1` code is one however a device reads it: converted, placed, or from its bits.
450    #[test]
451    fn every_read_of_an_e2m1_code_holds_e2m1() {
452        let placed = |value, lifted| Field::Placed { value, lifted };
453        for field in [
454            Field::from(QuantValue::E2M1),
455            Field::ConvertedE2M1,
456            placed(QuantValue::E2M1, true),
457            placed(QuantValue::E2M1, false),
458        ] {
459            assert!(field.holds_e2m1(), "{field:?}");
460        }
461        for field in [
462            Field::from(QuantValue::E4M3),
463            placed(QuantValue::E4M3, true),
464            Field::from(Fp8Format::E4M3),
465        ] {
466            assert!(!field.holds_e2m1(), "{field:?}");
467        }
468    }
469
470    /// A float code placed is the field it was, decoded from its bits, and read placed it is
471    /// short of its value by the gap between its exponent bias and an `f16`'s: `2^14` for
472    /// `e2m1`, `2^8` for `e4m3`, nothing for `e5m2`, which is an `f16`'s top byte. Lifted, it
473    /// is short of nothing.
474    #[test]
475    fn a_placed_code_leaves_its_lift_to_the_reader() {
476        let placed = |value| Field::Placed {
477            value,
478            lifted: true,
479        };
480        for (value, lift) in [
481            (QuantValue::E2M1, 16384.0),
482            (QuantValue::E4M3, 256.0),
483            (QuantValue::E5M2, 1.0),
484        ] {
485            assert_eq!(placed(value).size_bits(), Field::from(value).size_bits());
486            assert_eq!(placed(value).lift(), 1.0, "{value:?}");
487            assert_eq!(placed(value).placed().lift(), lift, "{value:?}");
488        }
489        assert_eq!(
490            placed(QuantValue::E4M3).placed().decode(),
491            FieldDecode::PlacedByte {
492                format: Fp8Format::E4M3,
493                lifted: false
494            }
495        );
496        assert_eq!(
497            placed(QuantValue::E2M1).decode(),
498            FieldDecode::Placed { lifted: true }
499        );
500    }
501}