use cubecl_ir::{
NamedRewrite, Scope,
dialect::{base::OperationPtrExt, general::CastOp},
interfaces::TypedExt,
prelude::*,
types::scalar::Float4E2M1x2Type,
};
use half::f16;
use pliron::r#type::TypeHandle;
use crate::post_processing::minifloat::Fp8Container;
use crate::{self as cubecl, prelude::*};
const SIGN: u32 = 0x8;
const MANTISSA: u32 = 0x1;
const NIBBLE: u32 = 0xF;
const MAGNITUDE: u32 = 0x7;
const MAGNITUDE_SHIFT: u32 = 22;
const EXPONENT_BIAS: u32 = 126 << 23;
const SIGN_SHIFT: u32 = 28;
#[cube]
pub fn e2m1_bits_to_float<F: Numeric, N: Size>(code: Vector<u32, N>) -> Vector<F, N> {
let magnitude = code & Vector::new(MAGNITUDE);
let normal = (magnitude << Vector::new(MAGNITUDE_SHIFT)) + Vector::new(EXPONENT_BIAS);
let mantissa = code & Vector::new(MANTISSA);
let subnormal = select_many(
mantissa.equal(&Vector::new(MANTISSA)),
Vector::new(EXPONENT_BIAS),
Vector::new(0u32),
);
let bits = select_many(
magnitude.greater_than(&Vector::new(MANTISSA)),
normal,
subnormal,
);
let sign = (code & Vector::new(SIGN)) << Vector::new(SIGN_SHIFT);
Vector::<F, N>::cast_from(Vector::<f32, N>::reinterpret(bits | sign))
}
#[cube]
pub fn e2m1_packed_bits_to_float<F: Numeric, N: Size>(word: u32) -> Vector<F, N> {
let mut codes = Vector::<u32, N>::empty();
#[unroll]
for lane in 0..N::value() {
codes.insert(lane, (word >> (4 * lane as u32)) & NIBBLE);
}
e2m1_bits_to_float::<F, N>(codes)
}
#[cube]
pub fn float_to_e2m1_bits<F: Numeric, N: Size>(value: Vector<F, N>) -> Vector<u32, N> {
let value = Vector::<f32, N>::cast_from(value);
let sign_bit = Vector::new(0x8000_0000u32);
let negative = (Vector::<u32, N>::reinterpret(value) & sign_bit).equal(&sign_bit);
let magnitude = select_many(negative, -value, value);
let mut code = cleared::<N>(magnitude.greater_than(&Vector::new(0.25f32)));
code += cleared::<N>(magnitude.greater_equal(&Vector::new(0.75f32)));
code += cleared::<N>(magnitude.greater_than(&Vector::new(1.25f32)));
code += cleared::<N>(magnitude.greater_equal(&Vector::new(1.75f32)));
code += cleared::<N>(magnitude.greater_than(&Vector::new(2.5f32)));
code += cleared::<N>(magnitude.greater_equal(&Vector::new(3.5f32)));
code += cleared::<N>(magnitude.greater_than(&Vector::new(5.0f32)));
code | (cleared::<N>(negative) * Vector::new(SIGN))
}
#[cube]
fn cleared<N: Size>(above: Vector<bool, N>) -> Vector<u32, N> {
select_many(above, Vector::new(1u32), Vector::new(0u32))
}
pub const E2M1_F16_LIFT: f32 = 16384.0;
#[cube]
fn f16_pair_bits(codes: u32) -> u32 {
((codes & 0x0007_0007) << 9) | ((codes & 0x0008_0008) << 12)
}
#[cube]
pub fn e2m1_words_to_f16<W: Size, V: Size>(words: Vector<u32, W>) -> Vector<f16, V> {
e2m1_words_to_f16_placed::<W, V>(words) * Vector::new(f16::new(E2M1_F16_LIFT))
}
#[cube]
pub fn e2m1_words_to_f16_placed<W: Size, V: Size>(words: Vector<u32, W>) -> Vector<f16, V> {
let mut values = Vector::<f16, V>::empty();
#[unroll]
for w in 0..W::value() {
let word = words.extract(w);
#[unroll]
for j in 0..4usize {
let shift = comptime![4 * j as u32];
let pair = Vector::<f16, Const<2>>::reinterpret(f16_pair_bits(word >> shift));
values.insert(8 * w + j, pair.extract(0usize));
values.insert(8 * w + j + 4, pair.extract(1usize));
}
}
values
}
#[cube]
pub fn e2m1_bytes_to_f16<B: Size, V: Size>(bytes: Vector<u32, B>) -> Vector<f16, V> {
let mut values = Vector::<f16, V>::empty();
#[unroll]
for b in 0..B::value() {
let byte = bytes.extract(b);
let codes = (byte & 0xF) | ((byte & 0xF0) << 12);
let pair = Vector::<f16, Const<2>>::reinterpret(f16_pair_bits(codes));
values.insert(2 * b, pair.extract(0usize));
values.insert(2 * b + 1, pair.extract(1usize));
}
values * Vector::new(f16::new(E2M1_F16_LIFT))
}
#[cube]
fn bytes_to_codes<B: Size, V: Size>(bytes: Vector<u32, B>) -> Vector<u32, V> {
let mut codes = Vector::<u32, V>::empty();
#[unroll]
for b in 0..B::value() {
let byte = bytes.extract(b);
codes.insert(2 * b, byte & NIBBLE);
codes.insert(2 * b + 1, (byte >> 4) & NIBBLE);
}
codes
}
#[cube]
fn codes_to_bytes<V: Size, B: Size>(codes: Vector<u32, V>) -> Vector<u32, B> {
let mut bytes = Vector::<u32, B>::empty();
#[unroll]
for b in 0..B::value() {
bytes.insert(
b,
(codes.extract(2 * b) & NIBBLE) | ((codes.extract(2 * b + 1) & NIBBLE) << 4),
);
}
bytes
}
#[cube]
fn bytes_to_words<B: Size, W: Size>(bytes: Vector<u32, B>) -> Vector<u32, W> {
let mut words = Vector::<u32, W>::empty();
#[unroll]
for w in 0..W::value() {
let mut word = 0u32;
#[unroll]
for offset in 0..4usize {
let shift = comptime![8 * offset as u32];
word |= (bytes.extract(4 * w + offset) & 0xFF) << shift;
}
words.insert(w, word);
}
words
}
#[cube]
fn words_to_bytes<W: Size, B: Size>(words: Vector<u32, W>) -> Vector<u32, B> {
let mut bytes = Vector::<u32, B>::empty();
#[unroll]
for b in 0..B::value() {
let shift = comptime![8 * (b % 4) as u32];
bytes.insert(b, (words.extract(b / 4) >> shift) & 0xFF);
}
bytes
}
define_size!(B);
define_size!(V);
define_size!(W);
pub type LowerFp4CastPass = MatchRewritePass<LowerFp4Cast>;
#[derive(new, Clone, Copy, Debug, Default, NamedRewrite)]
pub struct LowerFp4Cast {
half: bool,
container: Fp8Container,
}
fn is_e2m1x2(ctx: &Context, ty: TypeHandle) -> bool {
ty.scalar_ty(ctx).deref(ctx).is::<Float4E2M1x2Type>()
}
impl MatchRewrite for LowerFp4Cast {
fn r#match(&mut self, ctx: &Context, op: Ptr<Operation>) -> bool {
if !op.is_op::<CastOp>(ctx) {
return false;
}
is_e2m1x2(ctx, op.operand(ctx, 0).get_type(ctx))
|| is_e2m1x2(ctx, op.result(ctx).get_type(ctx))
}
fn rewrite(
&mut self,
ctx: &mut Context,
rewriter: &mut MatchRewriter,
op: Ptr<Operation>,
) -> Result<()> {
let scope = Scope::from_context_and_inserter(ctx, rewriter);
let input = op.operand(ctx, 0);
let result_ty = op.result(ctx).get_type(ctx);
let value = if is_e2m1x2(ctx, input.get_type(ctx)) {
self.decode(&scope, input, result_ty)
} else {
self.encode(&scope, input, result_ty)
};
rewriter.replace_operation_with_values(ctx, op, vec![value]);
Ok(())
}
}
impl LowerFp4Cast {
fn byte_lanes(&self, scope: &Scope, input: Value, bytes: usize) -> Value {
match self.container {
Fp8Container::Bytes => {
let lanes =
reinterpret_value(scope, input, Vector::<u8, B>::__expand_as_type(scope));
cast_value(scope, lanes, Vector::<u32, B>::__expand_as_type(scope))
}
Fp8Container::Words => {
let words = reinterpret_value(scope, input, self.words_type(scope, bytes));
words_to_bytes::expand::<W, B>(scope, words.into()).read_value(scope)
}
}
}
fn words_type(&self, scope: &Scope, bytes: usize) -> TypeHandle {
assert!(
bytes.is_multiple_of(4),
"fp4 is packed four bytes to a u32 on this backend: vectors of {bytes} e2m1x2 lanes \
are not supported, use a multiple of four"
);
scope.register_size::<W>(bytes / 4);
Vector::<u32, W>::__expand_as_type(scope)
}
fn decode(&self, scope: &Scope, input: Value, result_ty: TypeHandle) -> Value {
let ctx = scope.ctx();
let bytes = input.vector_size(ctx);
let values = result_ty.vector_size(ctx);
debug_assert_eq!(values, 2 * bytes, "an e2m1x2 lane casts to two values");
scope.register_size::<B>(bytes);
scope.register_size::<V>(values);
let decoded = match (self.half, bytes.is_multiple_of(4)) {
(true, true) => {
let words = reinterpret_value(scope, input, self.words_type(scope, bytes));
e2m1_words_to_f16::expand::<W, V>(scope, words.into()).read_value(scope)
}
(true, false) => {
let lanes = self.byte_lanes(scope, input, bytes);
e2m1_bytes_to_f16::expand::<B, V>(scope, lanes.into()).read_value(scope)
}
(false, _) => {
let lanes = self.byte_lanes(scope, input, bytes);
let codes = bytes_to_codes::expand::<B, V>(scope, lanes.into());
e2m1_bits_to_float::expand::<f32, V>(scope, codes).read_value(scope)
}
};
cast_value(scope, decoded, result_ty)
}
fn encode(&self, scope: &Scope, input: Value, result_ty: TypeHandle) -> Value {
let ctx = scope.ctx();
let values = input.vector_size(ctx);
let bytes = result_ty.vector_size(ctx);
debug_assert_eq!(values, 2 * bytes, "two values cast to an e2m1x2 lane");
scope.register_size::<B>(bytes);
scope.register_size::<V>(values);
let value = cast_value(scope, input, Vector::<f32, V>::__expand_as_type(scope));
let codes = float_to_e2m1_bits::expand::<f32, V>(scope, value.into());
let packed = codes_to_bytes::expand::<V, B>(scope, codes);
let container = match self.container {
Fp8Container::Bytes => cast_value(
scope,
packed.read_value(scope),
Vector::<u8, B>::__expand_as_type(scope),
),
Fp8Container::Words => {
self.words_type(scope, bytes);
bytes_to_words::expand::<B, W>(scope, packed).read_value(scope)
}
};
reinterpret_value(scope, container, result_ty)
}
}
#[cfg(test)]
mod tests {
use cubecl_common::e2m1;
#[test]
fn the_midpoints_round_to_even() {
for (midpoint, expected) in [
(0.25f32, 0.0f32),
(0.75, 1.0),
(1.25, 1.0),
(1.75, 2.0),
(2.5, 2.0),
(3.5, 4.0),
(5.0, 4.0),
] {
let landed = e2m1::from_f32(midpoint).to_f32();
assert_eq!(landed, expected, "{midpoint} rounded to {landed}");
}
}
#[test]
fn magnitudes_past_the_maximum_saturate() {
for value in [6.0f32, 6.1, 100.0, f32::MAX, f32::INFINITY] {
assert_eq!(e2m1::from_f32(value).to_f32(), 6.0);
assert_eq!(e2m1::from_f32(-value).to_f32(), -6.0);
}
}
#[test]
fn the_sign_bit_survives_a_round_trip() {
for code in 8..16u8 {
let value = e2m1::from_bits(code).to_f32();
assert!(value.is_sign_negative(), "code {code} decoded as {value}");
assert_eq!(e2m1::from_f32(value).to_bits(), code);
}
}
}