use half::f16;
use pictor_core::{
fp8_e4m3_decode, fp8_e4m3_encode, fp8_e5m2_decode, fp8_e5m2_encode, BlockFP8E4M3, BlockFP8E5M2,
QK_FP8,
};
use pictor_kernels::{Fp8Kernel, KernelDispatcher, KernelError, KernelTier};
fn e4m3_block(scale: f32, qs: [u8; 32]) -> BlockFP8E4M3 {
BlockFP8E4M3 {
qs,
d: f16::from_f32(scale),
}
}
fn e5m2_block(scale: f32, qs: [u8; 32]) -> BlockFP8E5M2 {
BlockFP8E5M2 {
qs,
d: f16::from_f32(scale),
}
}
fn e4m3_block_uniform(weight: f32, scale: f32) -> BlockFP8E4M3 {
let scaled = if scale.abs() < 1e-12 {
0.0
} else {
weight / scale
};
let q = fp8_e4m3_encode(scaled);
e4m3_block(scale, [q; 32])
}
fn e5m2_block_uniform(weight: f32, scale: f32) -> BlockFP8E5M2 {
let scaled = if scale.abs() < 1e-12 {
0.0
} else {
weight / scale
};
let q = fp8_e5m2_encode(scaled);
e5m2_block(scale, [q; 32])
}
fn make_dispatcher() -> KernelDispatcher {
KernelDispatcher::with_tier(KernelTier::Reference)
}
#[test]
fn dispatcher_dequant_fp8_e4m3_matches_reference() {
let dispatcher = make_dispatcher();
let values: Vec<f32> = (0..QK_FP8).map(|i| (i as f32) * 0.5 - 8.0).collect();
let blocks = BlockFP8E4M3::quantize(&values).unwrap();
let mut output = vec![0.0f32; QK_FP8];
dispatcher
.dequant_fp8_e4m3(&blocks, &mut output)
.expect("dequant_fp8_e4m3 should succeed");
for (i, &v) in output.iter().enumerate() {
let expected = blocks[0].d.to_f32() * fp8_e4m3_decode(blocks[0].qs[i]);
assert!(
(v - expected).abs() < 1e-5,
"index {i}: expected {expected}, got {v}"
);
}
}
#[test]
fn dispatcher_dequant_fp8_e5m2_matches_reference() {
let dispatcher = make_dispatcher();
let values: Vec<f32> = (0..QK_FP8).map(|i| (i as f32) * 10.0 - 150.0).collect();
let blocks = BlockFP8E5M2::quantize(&values).unwrap();
let mut output = vec![0.0f32; QK_FP8];
dispatcher
.dequant_fp8_e5m2(&blocks, &mut output)
.expect("dequant_fp8_e5m2 should succeed");
for (i, &v) in output.iter().enumerate() {
let expected = blocks[0].d.to_f32() * fp8_e5m2_decode(blocks[0].qs[i]);
assert!(
(v - expected).abs() < 1e-5,
"index {i}: expected {expected}, got {v}"
);
}
}
#[test]
fn dispatcher_gemv_fp8_e4m3_basic() {
let dispatcher = make_dispatcher();
let q_one = fp8_e4m3_encode(1.0);
let blocks = vec![e4m3_block(1.0, [q_one; 32]), e4m3_block(1.0, [q_one; 32])];
let input = vec![1.0f32; 32];
let mut output = vec![0.0f32; 2];
dispatcher
.gemv_fp8_e4m3(&blocks, &input, &mut output, 2, 32)
.expect("gemv_fp8_e4m3 should succeed");
let expected = 32.0 * fp8_e4m3_decode(q_one);
for (r, &v) in output.iter().enumerate() {
assert!(
(v - expected).abs() < 0.5,
"row {r}: expected ~{expected}, got {v}"
);
}
}
#[test]
fn dispatcher_gemv_fp8_e5m2_basic() {
let dispatcher = make_dispatcher();
let q_one = fp8_e5m2_encode(1.0);
let blocks = vec![e5m2_block(1.0, [q_one; 32]), e5m2_block(1.0, [q_one; 32])];
let input = vec![1.0f32; 32];
let mut output = vec![0.0f32; 2];
dispatcher
.gemv_fp8_e5m2(&blocks, &input, &mut output, 2, 32)
.expect("gemv_fp8_e5m2 should succeed");
let expected = 32.0 * fp8_e5m2_decode(q_one);
for (r, &v) in output.iter().enumerate() {
assert!(
(v - expected).abs() < 1.0,
"row {r}: expected ~{expected}, got {v}"
);
}
}
#[test]
fn dispatcher_gemm_fp8_e4m3_batch2() {
let dispatcher = make_dispatcher();
let q_one = fp8_e4m3_encode(1.0);
let n_rows = 3;
let k = 32;
let batch = 2;
let blocks: Vec<BlockFP8E4M3> = (0..n_rows)
.map(|r| e4m3_block((r + 1) as f32, [q_one; 32]))
.collect();
let mut inputs = vec![0.0f32; batch * k];
for b in 0..batch {
for j in 0..k {
inputs[b * k + j] = (b + 1) as f32;
}
}
let mut gemm_out = vec![0.0f32; batch * n_rows];
dispatcher
.gemm_fp8_e4m3(&blocks, &inputs, &mut gemm_out, n_rows, k, batch)
.expect("gemm_fp8_e4m3 should succeed");
for b in 0..batch {
let in_row = &inputs[b * k..(b + 1) * k];
let mut gemv_out = vec![0.0f32; n_rows];
dispatcher
.gemv_fp8_e4m3(&blocks, in_row, &mut gemv_out, n_rows, k)
.expect("gemv should succeed");
for r in 0..n_rows {
assert!(
(gemm_out[b * n_rows + r] - gemv_out[r]).abs() < 1e-4,
"batch={b} row={r}: gemm={} gemv={}",
gemm_out[b * n_rows + r],
gemv_out[r]
);
}
}
}
#[test]
fn dispatcher_gemm_fp8_e5m2_batch2() {
let dispatcher = make_dispatcher();
let q_one = fp8_e5m2_encode(1.0);
let n_rows = 2;
let k = 32;
let batch = 2;
let blocks: Vec<BlockFP8E5M2> = (0..n_rows)
.map(|r| e5m2_block((r + 1) as f32, [q_one; 32]))
.collect();
let inputs = vec![1.0f32; batch * k];
let mut gemm_out = vec![0.0f32; batch * n_rows];
dispatcher
.gemm_fp8_e5m2(&blocks, &inputs, &mut gemm_out, n_rows, k, batch)
.expect("gemm_fp8_e5m2 should succeed");
for b in 0..batch {
let in_row = &inputs[b * k..(b + 1) * k];
let mut gemv_out = vec![0.0f32; n_rows];
dispatcher
.gemv_fp8_e5m2(&blocks, in_row, &mut gemv_out, n_rows, k)
.expect("gemv should succeed");
for r in 0..n_rows {
assert!(
(gemm_out[b * n_rows + r] - gemv_out[r]).abs() < 1e-4,
"batch={b} row={r}: gemm={} gemv={}",
gemm_out[b * n_rows + r],
gemv_out[r]
);
}
}
}
#[test]
fn dequant_e4m3_buffer_too_small() {
let dispatcher = make_dispatcher();
let block = e4m3_block(1.0, [0x38u8; 32]);
let mut output = vec![0.0f32; QK_FP8 - 1];
let result = dispatcher.dequant_fp8_e4m3(&[block], &mut output);
assert!(
matches!(result, Err(KernelError::BufferTooSmall { .. })),
"expected BufferTooSmall, got {result:?}"
);
}
#[test]
fn dequant_e5m2_buffer_too_small() {
let dispatcher = make_dispatcher();
let block = e5m2_block(1.0, [0x3Cu8; 32]);
let mut output = vec![0.0f32; 0];
let result = dispatcher.dequant_fp8_e5m2(&[block], &mut output);
assert!(
matches!(result, Err(KernelError::BufferTooSmall { .. })),
"expected BufferTooSmall, got {result:?}"
);
}
#[test]
fn gemv_e4m3_not_block_aligned() {
let dispatcher = make_dispatcher();
let blocks = vec![e4m3_block(1.0, [0x38u8; 32])];
let input = vec![1.0f32; 31];
let mut output = vec![0.0f32; 1];
let result = dispatcher.gemv_fp8_e4m3(&blocks, &input, &mut output, 1, 31);
assert!(
matches!(result, Err(KernelError::NotBlockAligned { .. })),
"expected NotBlockAligned, got {result:?}"
);
}
#[test]
fn gemv_e5m2_not_block_aligned() {
let dispatcher = make_dispatcher();
let blocks = vec![e5m2_block(1.0, [0x3Cu8; 32])];
let input = vec![1.0f32; 33];
let mut output = vec![0.0f32; 1];
let result = dispatcher.gemv_fp8_e5m2(&blocks, &input, &mut output, 1, 33);
assert!(
matches!(result, Err(KernelError::NotBlockAligned { .. })),
"expected NotBlockAligned, got {result:?}"
);
}
#[test]
fn gemv_e4m3_dimension_mismatch() {
let dispatcher = make_dispatcher();
let blocks = vec![e4m3_block(1.0, [0x38u8; 32])];
let input = vec![1.0f32; 32];
let mut output = vec![0.0f32; 2];
let result = dispatcher.gemv_fp8_e4m3(&blocks, &input, &mut output, 2, 32);
assert!(
matches!(result, Err(KernelError::DimensionMismatch { .. })),
"expected DimensionMismatch, got {result:?}"
);
}
#[test]
fn gemv_e5m2_dimension_mismatch() {
let dispatcher = make_dispatcher();
let blocks = vec![e5m2_block(1.0, [0x3Cu8; 32]), e5m2_block(1.0, [0x3Cu8; 32])];
let input = vec![1.0f32; 32];
let mut output = vec![0.0f32; 3];
let result = dispatcher.gemv_fp8_e5m2(&blocks, &input, &mut output, 3, 32);
assert!(
matches!(result, Err(KernelError::DimensionMismatch { .. })),
"expected DimensionMismatch, got {result:?}"
);
}
#[test]
fn gemm_e4m3_wrong_batch_size() {
let dispatcher = make_dispatcher();
let blocks = vec![e4m3_block(1.0, [0x38u8; 32]), e4m3_block(1.0, [0x38u8; 32])];
let inputs = vec![1.0f32; 64]; let mut outputs = vec![0.0f32; 3]; let result = dispatcher.gemm_fp8_e4m3(&blocks, &inputs, &mut outputs, 2, 32, 2);
assert!(
matches!(result, Err(KernelError::BufferTooSmall { .. })),
"expected BufferTooSmall, got {result:?}"
);
}
#[test]
fn gemm_e4m3_dimension_mismatch_blocks() {
let dispatcher = make_dispatcher();
let blocks = vec![e4m3_block(1.0, [0x38u8; 32]), e4m3_block(1.0, [0x38u8; 32])];
let inputs = vec![1.0f32; 32];
let mut outputs = vec![0.0f32; 3];
let result = dispatcher.gemm_fp8_e4m3(&blocks, &inputs, &mut outputs, 3, 32, 1);
assert!(
matches!(result, Err(KernelError::DimensionMismatch { .. })),
"expected DimensionMismatch, got {result:?}"
);
}
#[test]
fn gemm_e5m2_buffer_too_small() {
let dispatcher = make_dispatcher();
let blocks = vec![e5m2_block(1.0, [0x3Cu8; 32]), e5m2_block(1.0, [0x3Cu8; 32])];
let inputs = vec![1.0f32; 64]; let mut outputs = vec![0.0f32; 1]; let result = dispatcher.gemm_fp8_e5m2(&blocks, &inputs, &mut outputs, 2, 32, 2);
assert!(
matches!(result, Err(KernelError::BufferTooSmall { .. })),
"expected BufferTooSmall, got {result:?}"
);
}
#[test]
fn fp8_e4m3_gemv_vs_fp32_reference() {
let dispatcher = make_dispatcher();
let n_rows = 4;
let k = 64;
let raw_weights: Vec<f32> = (0..n_rows * k).map(|i| (i as f32).sin() * 3.0).collect();
let input: Vec<f32> = (0..k).map(|i| (i as f32).cos()).collect();
let blocks: Vec<BlockFP8E4M3> = (0..n_rows)
.flat_map(|r| {
let row = &raw_weights[r * k..(r + 1) * k];
BlockFP8E4M3::quantize(row).unwrap()
})
.collect();
let mut fp8_out = vec![0.0f32; n_rows];
dispatcher
.gemv_fp8_e4m3(&blocks, &input, &mut fp8_out, n_rows, k)
.expect("gemv_fp8_e4m3 should succeed");
let mut dequant_weights = vec![0.0f32; n_rows * k];
for r in 0..n_rows {
let row_blocks = &blocks[r * (k / QK_FP8)..(r + 1) * (k / QK_FP8)];
let out_slice = &mut dequant_weights[r * k..(r + 1) * k];
dispatcher
.dequant_fp8_e4m3(row_blocks, out_slice)
.expect("dequant should succeed");
}
let fp32_ref: Vec<f32> = (0..n_rows)
.map(|r| {
let row = &dequant_weights[r * k..(r + 1) * k];
row.iter().zip(input.iter()).map(|(w, x)| w * x).sum()
})
.collect();
for r in 0..n_rows {
assert!(
(fp8_out[r] - fp32_ref[r]).abs() < 1e-4,
"row {r}: fp8_gemv={} fp32_ref={}",
fp8_out[r],
fp32_ref[r]
);
}
}
#[test]
fn fp8_e5m2_gemv_vs_fp32_reference() {
let dispatcher = make_dispatcher();
let n_rows = 3;
let k = 32;
let q = fp8_e5m2_encode(1.0);
let scales = [0.5f32, 1.0, 2.0];
let blocks: Vec<BlockFP8E5M2> = scales.iter().map(|&s| e5m2_block(s, [q; 32])).collect();
let input: Vec<f32> = (0..k).map(|i| (i as f32) * 0.01).collect();
let mut fp8_out = vec![0.0f32; n_rows];
dispatcher
.gemv_fp8_e5m2(&blocks, &input, &mut fp8_out, n_rows, k)
.expect("gemv_fp8_e5m2 should succeed");
let mut dequant_weights = vec![0.0f32; n_rows * k];
dispatcher
.dequant_fp8_e5m2(&blocks, &mut dequant_weights)
.expect("dequant should succeed");
let fp32_ref: Vec<f32> = (0..n_rows)
.map(|r| {
let row = &dequant_weights[r * k..(r + 1) * k];
row.iter()
.zip(input.iter())
.map(|(w, x)| w * x)
.sum::<f32>()
})
.collect();
for r in 0..n_rows {
assert!(
fp8_out[r].is_finite(),
"row {r}: fp8_gemv should be finite, got {}",
fp8_out[r]
);
assert!(
(fp8_out[r] - fp32_ref[r]).abs() < 1e-4,
"row {r}: fp8_gemv={} fp32_ref={}",
fp8_out[r],
fp32_ref[r]
);
}
}
#[test]
fn gemv_e4m3_all_zeros_input() {
let dispatcher = make_dispatcher();
let blocks = vec![e4m3_block_uniform(3.0, 1.0)];
let input = vec![0.0f32; 32];
let mut output = vec![99.0f32; 1];
dispatcher
.gemv_fp8_e4m3(&blocks, &input, &mut output, 1, 32)
.expect("gemv should succeed");
assert!(
output[0].abs() < 1e-6,
"all-zero input: expected 0.0, got {}",
output[0]
);
}
#[test]
fn gemv_e4m3_all_zeros_weights() {
let dispatcher = make_dispatcher();
let blocks = vec![e4m3_block(1.0, [0x00u8; 32])];
let input = vec![1.0f32; 32];
let mut output = vec![99.0f32; 1];
dispatcher
.gemv_fp8_e4m3(&blocks, &input, &mut output, 1, 32)
.expect("gemv should succeed");
assert!(
output[0].abs() < 1e-6,
"all-zero weights: expected 0.0, got {}",
output[0]
);
}
#[test]
fn gemv_e4m3_unit_input() {
let dispatcher = make_dispatcher();
let w0 = fp8_e4m3_encode(7.5);
let w1 = fp8_e4m3_encode(-3.25);
let mut qs0 = [0x00u8; 32];
let mut qs1 = [0x00u8; 32];
qs0[0] = w0;
qs1[0] = w1;
let blocks = vec![e4m3_block(1.0, qs0), e4m3_block(1.0, qs1)];
let mut input = vec![0.0f32; 32];
input[0] = 1.0;
let mut output = vec![0.0f32; 2];
dispatcher
.gemv_fp8_e4m3(&blocks, &input, &mut output, 2, 32)
.expect("gemv should succeed");
let e0 = fp8_e4m3_decode(w0);
let e1 = fp8_e4m3_decode(w1);
assert!(
(output[0] - e0).abs() < 1e-5,
"row0: expected {e0}, got {}",
output[0]
);
assert!(
(output[1] - e1).abs() < 1e-5,
"row1: expected {e1}, got {}",
output[1]
);
}
#[test]
fn gemv_e5m2_all_zeros_input() {
let dispatcher = make_dispatcher();
let blocks = vec![e5m2_block_uniform(10.0, 1.0)];
let input = vec![0.0f32; 32];
let mut output = vec![99.0f32; 1];
dispatcher
.gemv_fp8_e5m2(&blocks, &input, &mut output, 1, 32)
.expect("gemv should succeed");
assert!(
output[0].abs() < 1e-6,
"all-zero input: expected 0.0, got {}",
output[0]
);
}
#[test]
fn gemv_e5m2_all_zeros_weights() {
let dispatcher = make_dispatcher();
let blocks = vec![e5m2_block(1.0, [0x00u8; 32])];
let input = vec![1.0f32; 32];
let mut output = vec![99.0f32; 1];
dispatcher
.gemv_fp8_e5m2(&blocks, &input, &mut output, 1, 32)
.expect("gemv should succeed");
assert!(
output[0].abs() < 1e-6,
"all-zero weights: expected 0.0, got {}",
output[0]
);
}
#[test]
fn dequant_e4m3_single_block() {
let dispatcher = make_dispatcher();
let q = fp8_e4m3_encode(2.5);
let block = e4m3_block(1.0, [q; 32]);
let mut output = vec![0.0f32; QK_FP8];
dispatcher
.dequant_fp8_e4m3(&[block], &mut output)
.expect("dequant single block should succeed");
let expected = fp8_e4m3_decode(q);
for (i, &v) in output.iter().enumerate() {
assert!(
(v - expected).abs() < 1e-5,
"index {i}: expected {expected}, got {v}"
);
}
}
#[test]
fn dequant_e5m2_single_block() {
let dispatcher = make_dispatcher();
let q = fp8_e5m2_encode(100.0);
let block = e5m2_block(1.0, [q; 32]);
let mut output = vec![0.0f32; QK_FP8];
dispatcher
.dequant_fp8_e5m2(&[block], &mut output)
.expect("dequant single block should succeed");
let expected = fp8_e5m2_decode(q);
for (i, &v) in output.iter().enumerate() {
assert!(
(v - expected).abs() < 1e-5,
"index {i}: expected {expected}, got {v}"
);
}
}
#[test]
fn gemv_e4m3_single_row() {
let dispatcher = make_dispatcher();
let q = fp8_e4m3_encode(0.5);
let block = e4m3_block(2.0, [q; 32]);
let input = vec![1.0f32; 32];
let mut output = vec![0.0f32; 1];
dispatcher
.gemv_fp8_e4m3(&[block], &input, &mut output, 1, 32)
.expect("single-row gemv should succeed");
let weight_val = 2.0 * fp8_e4m3_decode(q);
let expected = 32.0 * weight_val;
assert!(
(output[0] - expected).abs() < 0.5,
"expected ~{expected}, got {}",
output[0]
);
}
#[test]
fn gemm_e4m3_batch_one_equals_gemv() {
let dispatcher = make_dispatcher();
let n_rows = 4;
let k = 64;
let blocks: Vec<BlockFP8E4M3> = (0..n_rows)
.flat_map(|r| {
let vals: Vec<f32> = (0..k).map(|i| ((r * k + i) as f32).sin()).collect();
BlockFP8E4M3::quantize(&vals).unwrap()
})
.collect();
let input: Vec<f32> = (0..k).map(|i| (i as f32) * 0.1).collect();
let mut gemm_out = vec![0.0f32; n_rows];
let mut gemv_out = vec![0.0f32; n_rows];
dispatcher
.gemm_fp8_e4m3(&blocks, &input, &mut gemm_out, n_rows, k, 1)
.expect("gemm(batch=1) should succeed");
dispatcher
.gemv_fp8_e4m3(&blocks, &input, &mut gemv_out, n_rows, k)
.expect("gemv should succeed");
for r in 0..n_rows {
assert!(
(gemm_out[r] - gemv_out[r]).abs() < 1e-4,
"row {r}: gemm={} gemv={}",
gemm_out[r],
gemv_out[r]
);
}
}
#[test]
fn gemm_e5m2_batch_one_equals_gemv() {
let dispatcher = make_dispatcher();
let n_rows = 2;
let k = 32;
let blocks: Vec<BlockFP8E5M2> = (0..n_rows)
.flat_map(|r| {
let vals: Vec<f32> = (0..k)
.map(|i| ((r * k + i) as f32) * 100.0 - 1600.0)
.collect();
BlockFP8E5M2::quantize(&vals).unwrap()
})
.collect();
let input: Vec<f32> = (0..k).map(|i| (i as f32) * 0.05).collect();
let mut gemm_out = vec![0.0f32; n_rows];
let mut gemv_out = vec![0.0f32; n_rows];
dispatcher
.gemm_fp8_e5m2(&blocks, &input, &mut gemm_out, n_rows, k, 1)
.expect("gemm(batch=1) should succeed");
dispatcher
.gemv_fp8_e5m2(&blocks, &input, &mut gemv_out, n_rows, k)
.expect("gemv should succeed");
for r in 0..n_rows {
assert!(
(gemm_out[r] - gemv_out[r]).abs() < 1e-4,
"row {r}: gemm={} gemv={}",
gemm_out[r],
gemv_out[r]
);
}
}
#[test]
fn dequant_e4m3_empty_blocks() {
let dispatcher = make_dispatcher();
let mut output: Vec<f32> = vec![];
dispatcher
.dequant_fp8_e4m3(&[], &mut output)
.expect("empty dequant should succeed");
}
#[test]
fn dequant_e5m2_empty_blocks() {
let dispatcher = make_dispatcher();
let mut output: Vec<f32> = vec![];
dispatcher
.dequant_fp8_e5m2(&[], &mut output)
.expect("empty dequant should succeed");
}
#[test]
fn dispatcher_name_fp8() {
let dispatcher = make_dispatcher();
assert_eq!(dispatcher.name_fp8(), "fp8_reference");
}
#[test]
fn dequant_e4m3_oversized_output_not_clobbered() {
let dispatcher = make_dispatcher();
let block = e4m3_block(1.0, [0x38u8; 32]);
let sentinel = 42.0f32;
let mut output = vec![sentinel; QK_FP8 + 5];
dispatcher
.dequant_fp8_e4m3(&[block], &mut output)
.expect("dequant should succeed");
for (i, &v) in output.iter().enumerate().skip(QK_FP8) {
assert_eq!(
v, sentinel,
"element {i} should not be modified (was sentinel)"
);
}
}
#[test]
fn e4m3_quantize_then_dequant_max_error() {
let dispatcher = make_dispatcher();
let values: Vec<f32> = (0..96).map(|i| (i as f32) * 0.3 - 14.0).collect();
let blocks = BlockFP8E4M3::quantize(&values).unwrap();
let mut output = vec![0.0f32; 96];
dispatcher
.dequant_fp8_e4m3(&blocks, &mut output)
.expect("dequant should succeed");
let max_err: f32 = values
.iter()
.zip(output.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f32::max);
assert!(
max_err < 1.5,
"E4M3 quantize-dequant max error {max_err} exceeds threshold"
);
}
#[test]
fn e5m2_quantize_then_dequant_max_error() {
let dispatcher = make_dispatcher();
let values: Vec<f32> = (0..64).map(|i| (i as f32) * 2.0 - 64.0).collect();
let blocks = BlockFP8E5M2::quantize(&values).unwrap();
let mut output = vec![0.0f32; 64];
dispatcher
.dequant_fp8_e5m2(&blocks, &mut output)
.expect("dequant should succeed");
let max_err: f32 = values
.iter()
.zip(output.iter())
.filter(|(a, b)| a.is_finite() && b.is_finite())
.map(|(a, b)| (a - b).abs())
.fold(0.0, f32::max);
assert!(
max_err < 20.0,
"E5M2 quantize-dequant max error {max_err} exceeds threshold"
);
}
#[test]
fn gemv_e4m3_multiple_blocks_per_row() {
let dispatcher = make_dispatcher();
let n_rows = 4;
let k = 64; let q = fp8_e4m3_encode(1.0);
let blocks: Vec<BlockFP8E4M3> = (0..n_rows * 2).map(|_| e4m3_block(1.0, [q; 32])).collect();
let input = vec![1.0f32; k];
let mut output = vec![0.0f32; n_rows];
dispatcher
.gemv_fp8_e4m3(&blocks, &input, &mut output, n_rows, k)
.expect("multi-block gemv should succeed");
let expected = 64.0 * fp8_e4m3_decode(q);
for (r, &v) in output.iter().enumerate() {
assert!(
(v - expected).abs() < 1.0,
"row {r}: expected ~{expected}, got {v}"
);
}
}
#[test]
fn gemv_e4m3_negative_weights_positive_input() {
let dispatcher = make_dispatcher();
let blocks = vec![e4m3_block(1.0, [0xB8u8; 32])];
let input = vec![1.0f32; 32];
let mut output = vec![0.0f32; 1];
dispatcher
.gemv_fp8_e4m3(&blocks, &input, &mut output, 1, 32)
.expect("gemv should succeed");
assert!(
output[0] < 0.0,
"negative weights + positive input should produce negative output, got {}",
output[0]
);
}