use kopitiam_core::{Error, Result};
use kopitiam_gpu::{Executor, ResidentBlockQ8Weight, BLOCK};
use kopitiam_loader::LoadedModel;
const Q8_0_BLOCK_BYTES: usize = 2 + BLOCK;
pub fn split_q8_0(bytes: &[u8], rows: usize, cols: usize) -> Result<(Vec<f32>, Vec<i8>)> {
if cols == 0 || !cols.is_multiple_of(BLOCK) {
return Err(Error::MalformedModel {
format: "q8_0-split",
reason: format!("cols ({cols}) must be a non-zero multiple of {BLOCK}"),
});
}
let blocks = rows * (cols / BLOCK);
let expected = blocks * Q8_0_BLOCK_BYTES;
if bytes.len() < expected {
return Err(Error::MalformedModel {
format: "q8_0-split",
reason: format!("expected {expected} bytes for {rows}x{cols} Q8_0, got {}", bytes.len()),
});
}
let mut scales = Vec::with_capacity(blocks);
let mut quants = Vec::with_capacity(rows * cols);
for b in 0..blocks {
let block = &bytes[b * Q8_0_BLOCK_BYTES..(b + 1) * Q8_0_BLOCK_BYTES];
scales.push(f16_to_f32(u16::from_le_bytes([block[0], block[1]])));
quants.extend(block[2..].iter().map(|&byte| byte as i8));
}
Ok((scales, quants))
}
fn f16_to_f32(bits: u16) -> f32 {
let sign = ((bits >> 15) & 1) as u32;
let exp = ((bits >> 10) & 0x1F) as u32;
let frac = (bits & 0x3FF) as u32;
let out = match exp {
0 if frac == 0 => sign << 31,
0 => {
let mut e = -1i32;
let mut f = frac;
while f & 0x400 == 0 {
f <<= 1;
e -= 1;
}
let exp32 = (127 - 15 + e) as u32;
(sign << 31) | (exp32 << 23) | ((f & 0x3FF) << 13)
}
0x1F => (sign << 31) | (0xFF << 23) | (frac << 13),
_ => (sign << 31) | ((exp + 127 - 15) << 23) | (frac << 13),
};
f32::from_bits(out)
}
pub struct GpuOutputHead {
weight: ResidentBlockQ8Weight,
}
impl GpuOutputHead {
#[must_use]
pub fn try_build(model: &LoadedModel, executor: &Executor) -> Option<Self> {
let ctx = executor.gpu_context()?;
let name = if model.tensor("output.weight").is_some() {
"output.weight"
} else {
"token_embd.weight"
};
let entry = model.tensor(name)?;
if entry.dtype != kopitiam_core::DType::Q8_0 {
return None;
}
let dims = entry.shape.dims();
if dims.len() != 2 {
return None;
}
let (rows, cols) = (dims[0], dims[1]);
let bytes = model.tensor_bytes(name).ok()?;
let (scales, quants) = split_q8_0(bytes, rows, cols).ok()?;
let weight = ResidentBlockQ8Weight::upload(ctx, &quants, &scales, rows, cols).ok()?;
Some(Self { weight })
}
pub fn forward(&self, executor: &Executor, x: &[f32], m: usize) -> Option<Vec<f32>> {
let ctx = executor.gpu_context()?;
self.weight.matmul_nt(ctx, x, m).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn f16_conversion_matches_known_values() {
assert_eq!(f16_to_f32(0x0000), 0.0);
assert_eq!(f16_to_f32(0x3C00), 1.0);
assert_eq!(f16_to_f32(0xBC00), -1.0);
assert_eq!(f16_to_f32(0x4000), 2.0);
assert!((f16_to_f32(0x3555) - 0.333_251).abs() < 1e-5);
}
#[test]
fn split_then_recombine_matches_the_verified_dequantizer() {
let mut bytes = Vec::new();
for (scale_bits, base) in [(0x3C00u16, 0i32), (0x3800u16, 1)] {
bytes.extend_from_slice(&scale_bits.to_le_bytes());
for t in 0..BLOCK {
bytes.push((((t as i32 * 17 + base) % 255) - 128) as i8 as u8);
}
}
let (scales, quants) = split_q8_0(&bytes, 1, BLOCK * 2).expect("split");
assert_eq!(scales.len(), 2);
assert_eq!(quants.len(), BLOCK * 2);
assert_eq!(scales[0], 1.0);
assert_eq!(scales[1], 0.5);
let dequantized = kopitiam_tensor::Tensor::from_quantized(
kopitiam_core::DType::Q8_0,
bytes.clone(),
[1, BLOCK * 2],
)
.expect("reference tensor")
.to_dtype(kopitiam_core::DType::F32)
.expect("reference dequantize")
.to_vec_f32()
.expect("reference values");
for i in 0..BLOCK * 2 {
let ours = scales[i / BLOCK] * f32::from(quants[i]);
assert!(
(ours - dequantized[i]).abs() < 1e-6,
"element {i}: split gives {ours}, reference dequantizer gives {}",
dequantized[i]
);
}
}
#[test]
fn a_column_count_that_is_not_a_whole_number_of_blocks_is_refused() {
let err = split_q8_0(&[0u8; 100], 1, 40).unwrap_err();
assert!(matches!(err, Error::MalformedModel { .. }));
}
#[test]
fn truncated_bytes_are_refused_rather_than_padded() {
let err = split_q8_0(&[0u8; 10], 1, BLOCK).unwrap_err();
assert!(matches!(err, Error::MalformedModel { .. }));
}
}