use super::types::SparseAttentionError;
pub const FP8_E4M3_MAX: f32 = 448.0;
pub const FP8_E4M3_MIN_NORMAL: f32 = 1.0 / 64.0;
pub const FP8_E4M3_MIN_SUBNORMAL: f32 = 1.0 / (64.0 * 8.0);
#[inline]
#[must_use]
pub fn f32_to_e4m3(x: f32) -> u8 {
let sign_bit: u8 = if x.is_sign_negative() { 0x80 } else { 0x00 };
let ax = x.abs();
if !ax.is_finite() || ax >= FP8_E4M3_MAX * (1.0 + 1.0 / 16.0) {
return sign_bit | 0x7E;
}
if ax == 0.0 {
return sign_bit; }
if ax < FP8_E4M3_MIN_NORMAL {
let scaled = ax / FP8_E4M3_MIN_SUBNORMAL; let m = round_ties_even(scaled) as u32;
let m_clamped = m.min(7);
return sign_bit | (m_clamped as u8);
}
let bits = ax.to_bits();
let f32_exp = ((bits >> 23) & 0xFF) as i32; let f32_mant = bits & 0x7F_FFFF;
let unbiased_exp = f32_exp - 127;
let fp8_biased_exp = unbiased_exp + 7;
if fp8_biased_exp <= 0 {
return sign_bit;
}
if fp8_biased_exp > 15 {
return sign_bit | 0x7E;
}
let mant_top3 = f32_mant >> 20; let mant_low = f32_mant & ((1 << 20) - 1);
let halfway = 1u32 << 19;
let mut mant_fp8 = mant_top3;
match mant_low.cmp(&halfway) {
std::cmp::Ordering::Greater => mant_fp8 += 1,
std::cmp::Ordering::Equal => {
if mant_fp8 & 1 != 0 {
mant_fp8 += 1;
}
}
std::cmp::Ordering::Less => {}
}
let mut biased_exp = fp8_biased_exp as u32;
if mant_fp8 >= 8 {
mant_fp8 = 0;
biased_exp += 1;
if biased_exp > 15 {
return sign_bit | 0x7E; }
}
if biased_exp == 15 && mant_fp8 == 7 {
return sign_bit | 0x7E;
}
sign_bit | ((biased_exp as u8) << 3) | (mant_fp8 as u8)
}
#[inline]
#[must_use]
pub fn e4m3_to_f32(b: u8) -> f32 {
let sign = if b & 0x80 != 0 { -1.0 } else { 1.0 };
let magnitude = b & 0x7F;
if magnitude == 0 {
return 0.0 * sign;
}
if magnitude == 0x7F {
return sign * FP8_E4M3_MAX;
}
let exp = ((magnitude >> 3) & 0x0F) as i32;
let mant = (magnitude & 0x07) as f32;
if exp == 0 {
sign * (mant / 8.0) * FP8_E4M3_MIN_NORMAL
} else {
let base = 1.0 + mant / 8.0;
let scale = 2.0f32.powi(exp - 7);
sign * base * scale
}
}
#[derive(Debug, Clone)]
pub struct FpKvCache {
pub data: Vec<u8>,
pub scales: Vec<f32>,
pub num_pages: usize,
pub hkv: usize,
pub page_size: usize,
pub head_dim: usize,
}
impl FpKvCache {
pub fn encode(
buf: &[f32],
num_pages: usize,
hkv: usize,
page_size: usize,
head_dim: usize,
) -> Result<Self, SparseAttentionError> {
let elems_per_slot = page_size * head_dim;
let total = num_pages * hkv * elems_per_slot;
if buf.len() != total {
return Err(SparseAttentionError::ShapeMismatch {
what: "FpKvCache::encode buf",
expected: total,
got: buf.len(),
});
}
let num_slots = num_pages * hkv;
let mut data = vec![0u8; total];
let mut scales = vec![0.0f32; num_slots];
for slot in 0..num_slots {
let slot_off = slot * elems_per_slot;
let src = &buf[slot_off..slot_off + elems_per_slot];
let mut max_abs = 0.0f32;
for v in src {
let a = v.abs();
if a > max_abs {
max_abs = a;
}
}
let scale = if max_abs > 0.0 {
max_abs / FP8_E4M3_MAX
} else {
1.0
};
scales[slot] = scale;
let inv = if max_abs > 0.0 { 1.0 / scale } else { 0.0 };
for (i, v) in src.iter().enumerate() {
data[slot_off + i] = f32_to_e4m3(v * inv);
}
}
Ok(Self {
data,
scales,
num_pages,
hkv,
page_size,
head_dim,
})
}
#[must_use]
pub fn decode(&self) -> Vec<f32> {
let elems_per_slot = self.page_size * self.head_dim;
let mut out = Vec::with_capacity(self.data.len());
for slot in 0..self.scales.len() {
let scale = self.scales[slot];
let slot_off = slot * elems_per_slot;
for i in 0..elems_per_slot {
out.push(e4m3_to_f32(self.data[slot_off + i]) * scale);
}
}
out
}
#[must_use]
pub fn compression_ratio(&self) -> f32 {
let f32_bytes = (self.data.len() * 4) as f32;
let stored = (self.data.len() + self.scales.len() * 4) as f32;
f32_bytes / stored
}
}
#[inline]
fn round_ties_even(x: f32) -> f32 {
let floor = x.floor();
let frac = x - floor;
match frac.partial_cmp(&0.5) {
Some(std::cmp::Ordering::Less) => floor,
Some(std::cmp::Ordering::Greater) => floor + 1.0,
Some(std::cmp::Ordering::Equal) => {
if (floor as i64) & 1 == 0 {
floor
} else {
floor + 1.0
}
}
None => floor,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn e4m3_roundtrip_representable_values() {
let exact = [
0.0f32, 1.0, -1.0, 0.5, -0.5, 240.0, -240.0, 384.0, -384.0, 448.0, -448.0,
];
for &v in &exact {
let b = f32_to_e4m3(v);
let back = e4m3_to_f32(b);
assert!(
(back - v).abs() < 1e-6 || back == v,
"roundtrip failed for {v}: byte={b:#04x}, back={back}"
);
}
}
#[test]
fn e4m3_saturates_on_overflow() {
assert!((e4m3_to_f32(f32_to_e4m3(1e6)) - FP8_E4M3_MAX).abs() < 1e-3);
assert!((e4m3_to_f32(f32_to_e4m3(-1e6)) + FP8_E4M3_MAX).abs() < 1e-3);
assert!((e4m3_to_f32(f32_to_e4m3(f32::NAN)) - FP8_E4M3_MAX).abs() < 1e-3);
assert!((e4m3_to_f32(f32_to_e4m3(f32::INFINITY)) - FP8_E4M3_MAX).abs() < 1e-3);
assert!((e4m3_to_f32(f32_to_e4m3(f32::NEG_INFINITY)) + FP8_E4M3_MAX).abs() < 1e-3);
}
#[test]
fn e4m3_subnormal_region_encodes_small_values() {
for m in 1..=7 {
let v = (m as f32 / 8.0) * FP8_E4M3_MIN_NORMAL;
let byte = f32_to_e4m3(v);
let back = e4m3_to_f32(byte);
assert!(
(back - v).abs() < 1e-6,
"subnormal m={m}: v={v}, back={back}"
);
}
}
#[test]
fn fp_kv_cache_roundtrip_preserves_dynamic_range() {
let num_pages = 3;
let hkv = 2;
let page_size = 4;
let head_dim = 8;
let total = num_pages * hkv * page_size * head_dim;
let src: Vec<f32> = (0..total)
.map(|i| ((i as f32) * 0.13 - 0.5) * 3.0)
.collect();
let cache = FpKvCache::encode(&src, num_pages, hkv, page_size, head_dim).unwrap();
let back = cache.decode();
assert_eq!(back.len(), total);
let mut max_rel = 0.0f32;
let mut sum_sq = 0.0f64;
let mut count = 0usize;
for (i, (a, b)) in src.iter().zip(&back).enumerate() {
let denom = a.abs().max(1e-3);
let rel = (a - b).abs() / denom;
max_rel = max_rel.max(rel);
sum_sq += f64::from(rel) * f64::from(rel);
count += 1;
assert!(rel < 0.15, "elem {i}: src={a}, back={b}, rel={rel}");
}
let rms = (sum_sq / count as f64).sqrt() as f32;
assert!(rms < 0.05, "RMS rel err {rms} exceeds 5%");
eprintln!(
"FpKvCache roundtrip: max rel err = {max_rel}, compression ratio = {}",
cache.compression_ratio()
);
}
#[test]
fn fp_kv_cache_compression_ratio_close_to_four() {
let src = vec![1.0f32; 128];
let cache = FpKvCache::encode(&src, 2, 2, 4, 8).unwrap();
let ratio = cache.compression_ratio();
assert!(ratio > 3.0 && ratio < 4.0, "unexpected ratio {ratio}");
}
#[test]
fn fp_kv_cache_encode_shape_mismatch() {
let err = FpKvCache::encode(&[1.0; 5], 1, 1, 2, 4).unwrap_err();
matches!(err, SparseAttentionError::ShapeMismatch { .. });
}
#[test]
fn fp8_kv_cache_paired_with_kvouter_forward_end_to_end() {
use super::super::{
build_kvouter_index,
forward::kvouter_forward,
types::{BlockTables, CuSeqlensQ, SparseSelection},
};
let tq = 2;
let hq = 2;
let hkv = 1;
let head_dim = 8;
let page_size = 4;
let block_size = 4;
let max_pages = 3;
let num_pages = 3;
let q: Vec<f32> = (0..tq * hq * head_dim)
.map(|i| ((i as f32) * 0.07).sin())
.collect();
let k_fp32: Vec<f32> = (0..num_pages * hkv * page_size * head_dim)
.map(|i| ((i as f32) * 0.11).cos())
.collect();
let v_fp32: Vec<f32> = (0..num_pages * hkv * page_size * head_dim)
.map(|i| ((i as f32) * 0.13).sin())
.collect();
let k_cache = FpKvCache::encode(&k_fp32, num_pages, hkv, page_size, head_dim).unwrap();
let v_cache = FpKvCache::encode(&v_fp32, num_pages, hkv, page_size, head_dim).unwrap();
let k_deq = k_cache.decode();
let v_deq = v_cache.decode();
let tbl = BlockTables::new(vec![0, 1, 2], 1, max_pages).unwrap();
let cu = CuSeqlensQ::new(vec![0, tq as i64]).unwrap();
let used = vec![(num_pages * page_size) as i32];
let mut sel_flat = Vec::new();
for _i in 0..tq {
for _h in 0..hkv {
for r in 0..max_pages {
sel_flat.push(r as i32);
}
}
}
let sel = SparseSelection::new(sel_flat, tq, hkv, max_pages).unwrap();
let idx = build_kvouter_index(&sel, &tbl, &cu, Some(&used), block_size, page_size).unwrap();
let scale = 1.0f32 / (head_dim as f32).sqrt();
let fp32_out = kvouter_forward(
&q,
&k_fp32,
&v_fp32,
&idx,
&tbl,
&cu,
Some(&used),
hq,
hkv,
head_dim,
block_size,
page_size,
scale,
false,
)
.unwrap();
let fp8_out = kvouter_forward(
&q,
&k_deq,
&v_deq,
&idx,
&tbl,
&cu,
Some(&used),
hq,
hkv,
head_dim,
block_size,
page_size,
scale,
false,
)
.unwrap();
let mut sum_sq = 0.0f64;
let mut count = 0usize;
let mut max_rel = 0.0f32;
for (a, b) in fp32_out.o_partial.iter().zip(&fp8_out.o_partial) {
let denom = a.abs().max(1e-3);
let rel = (a - b).abs() / denom;
sum_sq += f64::from(rel) * f64::from(rel);
count += 1;
max_rel = max_rel.max(rel);
}
let rms = (sum_sq / count as f64).sqrt() as f32;
eprintln!("fp32→fp8 KV cache forward RMS rel err = {rms}, max = {max_rel}");
assert!(rms < 0.10, "RMS rel err {rms} exceeds 10%");
assert!(max_rel < 0.50, "max rel err {max_rel} exceeds 50%");
}
}