Skip to main content

cubecl_core/post_processing/
minifloat.rs

1//! Software fp8 conversion, on `u32` bit patterns only so that a backend with no 8- or 16-bit types
2//! can still call it on the bits in a word.
3
4use cubecl_ir::{
5    NamedRewrite, Scope,
6    dialect::{
7        base::OperationPtrExt,
8        cmp::{FEqualOp, FNotEqualOp},
9        general::CastOp,
10    },
11    interfaces::TypedExt,
12    prelude::*,
13    types::Fp8Format,
14};
15use enumset::EnumSet;
16use pliron::r#type::TypeHandle;
17
18use crate::{self as cubecl, prelude::*};
19
20define_size!(N);
21
22const F32_MANTISSA_BITS: u32 = f32::MANTISSA_DIGITS - 1;
23const F32_MANTISSA_MASK: u32 = (1 << F32_MANTISSA_BITS) - 1;
24const F32_MAGNITUDE_MASK: u32 = u32::MAX >> 1;
25const F32_EXPONENT_BIAS: u32 = (f32::MAX_EXP - 1) as u32;
26const F32_INFINITY_BITS: u32 = f32::INFINITY.to_bits();
27const F32_NAN_BITS: u32 = f32::NAN.to_bits();
28const FP8_SIGN_BIT: u32 = 1 << (u8::BITS - 1);
29const FP8_MAGNITUDE_MASK: u32 = FP8_SIGN_BIT - 1;
30const SIGN_SHIFT: u32 = u32::BITS - u8::BITS;
31
32/// Bits above the low byte are ignored.
33#[cube]
34pub fn fp8_bits_to_f32<N: Size>(
35    bits: Vector<u32, N>,
36    #[comptime] format: Fp8Format,
37) -> Vector<f32, N> {
38    let mantissa_bits = comptime![format.mantissa_bits()];
39    let exponent_mask = comptime![(1u32 << format.exponent_bits()) - 1];
40    let mantissa_mask = comptime![(1u32 << mantissa_bits) - 1];
41    let rebias = comptime![F32_EXPONENT_BIAS - format.bias()];
42    let mantissa_shift = comptime![F32_MANTISSA_BITS - mantissa_bits];
43    let subnormal_step = comptime![format.subnormal_step()];
44
45    let sign = (bits & Vector::new(FP8_SIGN_BIT)) << Vector::new(SIGN_SHIFT);
46    let exponent = (bits >> Vector::new(mantissa_bits)) & Vector::new(exponent_mask);
47    let mantissa = bits & Vector::new(mantissa_mask);
48
49    let normal = sign
50        | ((exponent + Vector::new(rebias)) << Vector::new(F32_MANTISSA_BITS))
51        | (mantissa << Vector::new(mantissa_shift));
52    let subnormal = sign
53        | Vector::<u32, N>::reinterpret(
54            Vector::<f32, N>::cast_from(mantissa) * Vector::new(subnormal_step),
55        );
56    let value = select_many(exponent.equal(&Vector::new(0u32)), subnormal, normal);
57
58    let nan = sign | Vector::new(F32_NAN_BITS);
59    let result = if comptime![format.has_infinity()] {
60        let inf = sign | Vector::new(F32_INFINITY_BITS);
61        let special = select_many(mantissa.equal(&Vector::new(0u32)), inf, nan);
62        select_many(exponent.equal(&Vector::new(exponent_mask)), special, value)
63    } else {
64        let magnitude = bits & Vector::new(FP8_MAGNITUDE_MASK);
65        select_many(
66            magnitude.equal(&Vector::new(FP8_MAGNITUDE_MASK)),
67            nan,
68            value,
69        )
70    };
71
72    Vector::<f32, N>::reinterpret(result)
73}
74
75/// Round to nearest even; overflow and infinities saturate to the largest finite value, as the host
76/// codecs do.
77#[cube]
78pub fn f32_to_fp8_bits<N: Size>(
79    value: Vector<f32, N>,
80    #[comptime] format: Fp8Format,
81) -> Vector<u32, N> {
82    let mantissa_bits = comptime![format.mantissa_bits()];
83    let mantissa_shift = comptime![F32_MANTISSA_BITS - mantissa_bits];
84    let rebias = comptime![format.bias().wrapping_sub(F32_EXPONENT_BIAS)];
85    let half_ulp = comptime![1u32 << (mantissa_shift - 1)];
86    let subnormal_scale = comptime![1.0 / format.subnormal_step()];
87    let min_normal = comptime![format.min_normal()];
88    let max_value = comptime![format.max_value()];
89    let max_code = comptime![format.max_code()];
90    let nan_code = comptime![format.nan_code()];
91
92    let bits = Vector::<u32, N>::reinterpret(value);
93    let sign = (bits >> Vector::new(SIGN_SHIFT)) & Vector::new(FP8_SIGN_BIT);
94    let magnitude_bits = bits & Vector::new(F32_MAGNITUDE_MASK);
95    let magnitude = Vector::<f32, N>::reinterpret(magnitude_bits);
96
97    // Rounding by hand: the usual magic-number trick does not survive fast-math reassociation.
98    // `steps` overflows for normal magnitudes, which only feeds the lane the select below
99    // discards; no backend traps on a float-to-int overflow.
100    let steps = magnitude * Vector::new(subnormal_scale);
101    let truncated = Vector::<u32, N>::cast_from(steps);
102    let fraction = steps - Vector::<f32, N>::cast_from(truncated);
103    let above_half = fraction.greater_than(&Vector::new(0.5f32));
104    let tie_to_odd = fraction
105        .equal(&Vector::new(0.5f32))
106        .vec_and((truncated & Vector::new(1u32)).equal(&Vector::new(1u32)));
107    let round_up = Vector::<u32, N>::cast_from(above_half.or(tie_to_odd));
108    let subnormal = truncated + round_up;
109
110    let exponent = (magnitude_bits >> Vector::new(F32_MANTISSA_BITS)) + Vector::new(rebias);
111    let mantissa = magnitude_bits & Vector::new(F32_MANTISSA_MASK);
112    let lsb = (mantissa >> Vector::new(mantissa_shift)) & Vector::new(1u32);
113    let rounded =
114        ((exponent << Vector::new(F32_MANTISSA_BITS)) | mantissa) + Vector::new(half_ulp - 1) + lsb;
115    let normal = rounded >> Vector::new(mantissa_shift);
116
117    let code = select_many(
118        magnitude.less_than(&Vector::new(min_normal)),
119        subnormal,
120        normal,
121    );
122    let code = select_many(
123        magnitude.greater_than(&Vector::new(max_value)),
124        Vector::new(max_code),
125        code,
126    );
127    let code = select_many(
128        magnitude_bits.greater_than(&Vector::new(F32_INFINITY_BITS)),
129        Vector::new(nan_code),
130        code,
131    );
132
133    code | sign
134}
135
136/// How a backend without a native fp8 type holds the bytes of an fp8 vector.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
138pub enum Fp8Container {
139    /// One 8-bit integer per lane.
140    #[default]
141    Bytes,
142    /// Four lanes per `u32`, lane 0 in the low byte, for backends with no 8-bit type at all.
143    /// fp8 vectors must then be a multiple of four lanes wide.
144    Words,
145}
146
147pub type LowerMinifloatCastPass = MatchRewritePass<LowerMinifloatCast>;
148
149/// Lowers every cast from or to an fp8 format the backend does not convert natively onto the
150/// software polyfill, through `f32`.
151#[derive(new, Clone, Copy, Debug, Default, NamedRewrite)]
152pub struct LowerMinifloatCast {
153    native: EnumSet<Fp8Format>,
154    container: Fp8Container,
155}
156
157impl LowerMinifloatCast {
158    fn emulated(&self, ctx: &Context, value: impl Typed) -> Option<Fp8Format> {
159        Fp8Format::of_type(ctx, value.scalar_ty(ctx))
160            .filter(|format| !self.native.contains(*format))
161    }
162}
163
164impl MatchRewrite for LowerMinifloatCast {
165    fn r#match(&mut self, ctx: &Context, op: Ptr<Operation>) -> bool {
166        if !op.is_op::<CastOp>(ctx) {
167            return false;
168        }
169        self.emulated(ctx, op.operand(ctx, 0)).is_some()
170            || self.emulated(ctx, op.result(ctx)).is_some()
171    }
172
173    fn rewrite(
174        &mut self,
175        ctx: &mut Context,
176        rewriter: &mut MatchRewriter,
177        op: Ptr<Operation>,
178    ) -> Result<()> {
179        let scope = Scope::from_context_and_inserter(ctx, rewriter);
180        let input = op.operand(ctx, 0);
181        let result_ty = op.result(ctx).get_type(ctx);
182        let lanes = input.vector_size(ctx);
183        debug_assert_eq!(
184            lanes,
185            result_ty.vector_size(ctx),
186            "A cast keeps its vectorization, so one `N` describes both sides"
187        );
188        scope.register_size::<N>(lanes);
189
190        // Bool sources and targets go through the `f32` cast the backends already lower.
191        let mut value = input;
192        if let Some(format) = self.emulated(ctx, input) {
193            value = self.decode(&scope, value, format);
194        }
195        let value = match self.emulated(ctx, result_ty) {
196            Some(format) => self.encode(&scope, value, format, result_ty),
197            None => cast_value(&scope, value, result_ty),
198        };
199        rewriter.replace_operation_with_values(ctx, op, vec![value]);
200        Ok(())
201    }
202}
203
204impl LowerMinifloatCast {
205    fn decode(&self, scope: &Scope, value: Value, format: Fp8Format) -> Value {
206        let bits = match self.container {
207            Fp8Container::Bytes => {
208                let bytes =
209                    reinterpret_value(scope, value, Vector::<u8, N>::__expand_as_type(scope));
210                cast_value(scope, bytes, Vector::<u32, N>::__expand_as_type(scope))
211            }
212            Fp8Container::Words => {
213                let words = reinterpret_value(scope, value, words_type(scope));
214                unpack_words::expand::<N, W>(scope, words.into()).read_value(scope)
215            }
216        };
217        fp8_bits_to_f32::expand::<N>(scope, bits.into(), format).read_value(scope)
218    }
219
220    fn encode(
221        &self,
222        scope: &Scope,
223        value: Value,
224        format: Fp8Format,
225        result_ty: TypeHandle,
226    ) -> Value {
227        let value = cast_value(scope, value, Vector::<f32, N>::__expand_as_type(scope));
228        let bits = f32_to_fp8_bits::expand::<N>(scope, value.into(), format).read_value(scope);
229        let container = match self.container {
230            Fp8Container::Bytes => {
231                cast_value(scope, bits, Vector::<u8, N>::__expand_as_type(scope))
232            }
233            Fp8Container::Words => {
234                register_words_size(scope);
235                pack_words::expand::<N, W>(scope, bits.into()).read_value(scope)
236            }
237        };
238        reinterpret_value(scope, container, result_ty)
239    }
240}
241
242pub type LowerMinifloatComparePass = MatchRewritePass<LowerMinifloatCompare>;
243
244/// Lowers fp8 equality onto the lanes' bit patterns.
245///
246/// No backend compares fp8 as a float. `VK_EXT_shader_float8` allows conversion, cooperative
247/// matrix multiply, and the operations that only move bits around, so a float comparison is out
248/// even where fp8 is native; without it fp8 is an integer, which a float comparison cannot read
249/// either. Comparing the bits is what a CUDA kernel already gets, where fp8 is the raw
250/// `__nv_fp8_storage_t` byte and `__nv_fp8_e4m3` declares no comparison operators at all.
251///
252/// Bit equality parts from float equality in exactly two places: `0.0` and `-0.0` are equal as
253/// floats and different as bits, and a NaN equals itself here where a float NaN does not. Scale
254/// factors, where fp8 sees most of its use, are non-negative and never NaN, so neither case
255/// reaches them.
256#[derive(new, Clone, Copy, Debug, Default, NamedRewrite)]
257pub struct LowerMinifloatCompare {
258    container: Fp8Container,
259}
260
261impl MatchRewrite for LowerMinifloatCompare {
262    fn r#match(&mut self, ctx: &Context, op: Ptr<Operation>) -> bool {
263        if !op.is_op::<FEqualOp>(ctx) && !op.is_op::<FNotEqualOp>(ctx) {
264            return false;
265        }
266        Fp8Format::of_type(ctx, op.operand(ctx, 0).scalar_ty(ctx)).is_some()
267    }
268
269    fn rewrite(
270        &mut self,
271        ctx: &mut Context,
272        rewriter: &mut MatchRewriter,
273        op: Ptr<Operation>,
274    ) -> Result<()> {
275        let equal = op.is_op::<FEqualOp>(ctx);
276        let scope = Scope::from_context_and_inserter(ctx, rewriter);
277        let lhs = op.operand(scope.ctx(), 0);
278        let rhs = op.operand(scope.ctx(), 1);
279        scope.register_size::<N>(lhs.vector_size(scope.ctx()));
280
281        let lhs = self.lanes(&scope, lhs);
282        let rhs = self.lanes(&scope, rhs);
283        let value = match self.container {
284            Fp8Container::Bytes => compare_lanes::<u8>(&scope, equal, lhs, rhs),
285            Fp8Container::Words => compare_lanes::<u32>(&scope, equal, lhs, rhs),
286        };
287        rewriter.replace_operation_with_values(ctx, op, vec![value]);
288        Ok(())
289    }
290}
291
292impl LowerMinifloatCompare {
293    /// One lane per lane, in whichever integer the container leaves them addressable in. Packed
294    /// lanes have to come apart first: comparing the words would answer once for four lanes.
295    fn lanes(&self, scope: &Scope, value: Value) -> Value {
296        match self.container {
297            Fp8Container::Bytes => {
298                reinterpret_value(scope, value, Vector::<u8, N>::__expand_as_type(scope))
299            }
300            Fp8Container::Words => {
301                let words = reinterpret_value(scope, value, words_type(scope));
302                unpack_words::expand::<N, W>(scope, words.into()).read_value(scope)
303            }
304        }
305    }
306}
307
308fn compare_lanes<T: Int>(scope: &Scope, equal: bool, lhs: Value, rhs: Value) -> Value {
309    match equal {
310        true => bits_equal::expand::<T>(scope, lhs.into(), rhs.into()).read_value(scope),
311        false => bits_not_equal::expand::<T>(scope, lhs.into(), rhs.into()).read_value(scope),
312    }
313}
314
315#[cube]
316fn bits_equal<T: Int>(lhs: Vector<T, N>, rhs: Vector<T, N>) -> Vector<bool, N> {
317    lhs.equal(&rhs)
318}
319
320#[cube]
321fn bits_not_equal<T: Int>(lhs: Vector<T, N>, rhs: Vector<T, N>) -> Vector<bool, N> {
322    lhs.not_equal(&rhs)
323}
324
325define_size!(W);
326
327const LANES_PER_WORD: usize = (u32::BITS / u8::BITS) as usize;
328
329/// Registers `W`, the word count of an `N`-lane fp8 vector, so that `Vector<u32, W>` names the
330/// words those lanes are packed into.
331fn register_words_size(scope: &Scope) {
332    let lanes = N::__expand_value(scope);
333    assert!(
334        lanes.is_multiple_of(LANES_PER_WORD),
335        "fp8 is packed four lanes to a u32 on this backend: vectors of {lanes} lanes are not \
336         supported, use a vector size that is a multiple of {LANES_PER_WORD}"
337    );
338    scope.register_size::<W>(lanes / LANES_PER_WORD);
339}
340
341/// [`register_words_size`], then the word type itself.
342fn words_type(scope: &Scope) -> TypeHandle {
343    register_words_size(scope);
344    Vector::<u32, W>::__expand_as_type(scope)
345}
346
347#[cube]
348fn unpack_words<N: Size, W: Size>(words: Vector<u32, W>) -> Vector<u32, N> {
349    let mut lanes = Vector::<u32, N>::empty();
350    #[unroll]
351    for lane in 0..N::value() {
352        let word = words.extract(lane / LANES_PER_WORD);
353        let shift = comptime![(lane % LANES_PER_WORD) as u32 * u8::BITS];
354        lanes.insert(lane, (word >> shift) & 0xFF);
355    }
356    lanes
357}
358
359#[cube]
360fn pack_words<N: Size, W: Size>(lanes: Vector<u32, N>) -> Vector<u32, W> {
361    let mut words = Vector::<u32, W>::empty();
362    #[unroll]
363    for index in 0..W::value() {
364        let mut word = 0u32;
365        #[unroll]
366        for offset in 0..LANES_PER_WORD {
367            let shift = comptime![offset as u32 * u8::BITS];
368            word |= (lanes.extract(index * LANES_PER_WORD + offset) & 0xFF) << shift;
369        }
370        words.insert(index, word);
371    }
372    words
373}