use crate::context::GpuContext;
use crate::executor::GpuOpError;
use wgpu::util::DeviceExt;
pub const BLOCK: usize = 32;
const WORKGROUP_X: u32 = 16;
const WORKGROUP_Y: u32 = 16;
#[must_use]
pub fn block_q8_matmul_nt_cpu(
x: &[f32],
quants: &[i8],
scales: &[f32],
m: usize,
k: usize,
n: usize,
) -> Vec<f32> {
let mut y = vec![0f32; m * n];
let blocks_per_row = k / BLOCK;
if x.len() < m * k || quants.len() < n * k || scales.len() < n * blocks_per_row {
return y;
}
for i in 0..m {
let x_row = &x[i * k..i * k + k];
for j in 0..n {
let q_row = &quants[j * k..j * k + k];
let s_row = &scales[j * blocks_per_row..j * blocks_per_row + blocks_per_row];
let mut acc = 0f32;
for (b, &scale) in s_row.iter().enumerate() {
let off = b * BLOCK;
let mut block_sum = 0f32;
for t in 0..BLOCK {
block_sum += x_row[off + t] * f32::from(q_row[off + t]);
}
acc += scale * block_sum;
}
y[i * n + j] = acc;
}
}
y
}
#[derive(Debug)]
pub struct ResidentBlockQ8Weight {
quants: wgpu::Buffer,
scales: wgpu::Buffer,
pipeline: wgpu::ComputePipeline,
n: usize,
k: usize,
}
impl ResidentBlockQ8Weight {
pub fn upload(
ctx: &GpuContext,
quants: &[i8],
scales: &[f32],
n: usize,
k: usize,
) -> Result<Self, GpuOpError> {
if k == 0 || !k.is_multiple_of(BLOCK) {
return Err(GpuOpError::InvalidInput(format!(
"k ({k}) must be a non-zero multiple of BLOCK ({BLOCK})"
)));
}
if quants.len() != n * k {
return Err(GpuOpError::InvalidInput(format!(
"quants has {} elements, expected n*k = {}",
quants.len(),
n * k
)));
}
let blocks_per_row = k / BLOCK;
if scales.len() != n * blocks_per_row {
return Err(GpuOpError::InvalidInput(format!(
"scales has {} elements, expected n*(k/{BLOCK}) = {}",
scales.len(),
n * blocks_per_row
)));
}
let device = ctx.device();
let limit = device.limits().max_storage_buffer_binding_size;
let quant_bytes = (n * k) as u64;
let scale_bytes = std::mem::size_of_val(scales) as u64;
if quant_bytes.max(scale_bytes) > limit {
return Err(GpuOpError::InvalidInput(format!(
"weight needs a {}-byte storage binding but this adapter's \
max_storage_buffer_binding_size is {limit}",
quant_bytes.max(scale_bytes)
)));
}
let mut packed = vec![0u32; (n * k).div_ceil(4)];
for (i, &q) in quants.iter().enumerate() {
packed[i / 4] |= (q as u8 as u32) << ((i % 4) * 8);
}
let quants_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_q8.quants"),
contents: bytemuck::cast_slice(&packed),
usage: wgpu::BufferUsages::STORAGE,
});
let scales_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_q8.scales"),
contents: bytemuck::cast_slice(scales),
usage: wgpu::BufferUsages::STORAGE,
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("block_q8_matmul_nt.wgsl"),
source: wgpu::ShaderSource::Wgsl(
include_str!("../shaders/block_q8_matmul_nt.wgsl").into(),
),
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("block_q8.pipeline"),
layout: None,
module: &shader,
entry_point: Some("main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
});
Ok(Self { quants: quants_buf, scales: scales_buf, pipeline, n, k })
}
#[must_use]
pub fn out_features(&self) -> usize {
self.n
}
#[must_use]
pub fn in_features(&self) -> usize {
self.k
}
pub fn matmul_nt(
&self,
ctx: &GpuContext,
x: &[f32],
m: usize,
) -> Result<Vec<f32>, GpuOpError> {
if x.len() != m * self.k {
return Err(GpuOpError::InvalidInput(format!(
"x has {} elements, expected m*k = {}*{} = {}",
x.len(),
m,
self.k,
m * self.k
)));
}
if m == 0 {
return Ok(Vec::new());
}
let device = ctx.device();
let queue = ctx.queue();
let out_bytes = (m * self.n * std::mem::size_of::<f32>()) as wgpu::BufferAddress;
let x_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_q8.x"),
contents: bytemuck::cast_slice(x),
usage: wgpu::BufferUsages::STORAGE,
});
let dims: [u32; 4] = [m as u32, self.k as u32, self.n as u32, 0];
let dims_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("block_q8.dims"),
contents: bytemuck::cast_slice(&dims),
usage: wgpu::BufferUsages::UNIFORM,
});
let y_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("block_q8.y"),
size: out_bytes,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("block_q8.staging"),
size: out_bytes,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("block_q8.bind_group"),
layout: &self.pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: x_buf.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: self.quants.as_entire_binding() },
wgpu::BindGroupEntry { binding: 2, resource: self.scales.as_entire_binding() },
wgpu::BindGroupEntry { binding: 3, resource: y_buf.as_entire_binding() },
wgpu::BindGroupEntry { binding: 4, resource: dims_buf.as_entire_binding() },
],
});
let mut encoder = device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("block_q8") });
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("block_q8.pass"),
timestamp_writes: None,
});
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.dispatch_workgroups(
(m as u32).div_ceil(WORKGROUP_X),
(self.n as u32).div_ceil(WORKGROUP_Y),
1,
);
}
encoder.copy_buffer_to_buffer(&y_buf, 0, &staging, 0, out_bytes);
queue.submit(Some(encoder.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
device
.poll(wgpu::PollType::wait_indefinitely())
.map_err(|e| GpuOpError::Backend(format!("device poll failed: {e:?}")))?;
rx.recv()
.map_err(|e| GpuOpError::Backend(format!("map callback dropped: {e}")))?
.map_err(|e| GpuOpError::Backend(format!("buffer map failed: {e:?}")))?;
let data = slice
.get_mapped_range()
.map_err(|e| GpuOpError::Backend(format!("get_mapped_range failed: {e:?}")))?;
let result: Vec<f32> = bytemuck::cast_slice(&data).to_vec();
drop(data);
staging.unmap();
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn deterministic_quants(n: usize) -> Vec<i8> {
(0..n).map(|i| ((i as i32 * 37 % 255) - 128) as i8).collect()
}
fn deterministic_scales(n: usize) -> Vec<f32> {
(0..n).map(|i| 0.001 + (i as f32 * 0.13).sin().abs() * 0.01).collect()
}
#[test]
fn cpu_twin_matches_a_hand_computation() {
let x = vec![3.0f32; 32];
let quants = vec![2i8; 32];
let scales = vec![0.5f32];
let y = block_q8_matmul_nt_cpu(&x, &quants, &scales, 1, 32, 1);
assert_eq!(y, vec![96.0]);
}
#[test]
fn cpu_twin_handles_negative_quants() {
let mut quants = vec![0i8; 32];
quants[0] = -1;
quants[1] = -128;
let x = vec![1.0f32; 32];
let y = block_q8_matmul_nt_cpu(&x, &quants, &[1.0], 1, 32, 1);
assert_eq!(y, vec![-129.0]);
}
#[test]
fn gpu_matches_the_cpu_twin_including_negative_quants() {
let Ok(ctx) = crate::GpuContext::new() else {
eprintln!("skipped: no GPU on this machine");
return;
};
let (m, k, n) = (1usize, 960usize, 512usize);
let quants = deterministic_quants(n * k);
let scales = deterministic_scales(n * (k / BLOCK));
let x: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.017).sin()).collect();
let resident = ResidentBlockQ8Weight::upload(&ctx, &quants, &scales, n, k).expect("upload");
assert_eq!(resident.out_features(), n);
assert_eq!(resident.in_features(), k);
let gpu = resident.matmul_nt(&ctx, &x, m).expect("gpu matmul");
let cpu = block_q8_matmul_nt_cpu(&x, &quants, &scales, m, k, n);
assert_eq!(gpu.len(), cpu.len());
let worst = gpu.iter().zip(&cpu).fold(0f32, |a, (g, c)| a.max((g - c).abs()));
let scale = cpu.iter().fold(0f32, |a, v| a.max(v.abs())).max(1e-6);
assert!(
worst / scale < 1e-4,
"GPU and CPU disagree: worst {worst} (relative {})",
worst / scale
);
}
#[test]
fn repeated_dispatches_reuse_the_resident_weight_consistently() {
let Ok(ctx) = crate::GpuContext::new() else {
eprintln!("skipped: no GPU on this machine");
return;
};
let (k, n) = (64usize, 32usize);
let quants = deterministic_quants(n * k);
let scales = deterministic_scales(n * (k / BLOCK));
let resident = ResidentBlockQ8Weight::upload(&ctx, &quants, &scales, n, k).expect("upload");
let x1: Vec<f32> = (0..k).map(|i| (i as f32 * 0.03).sin()).collect();
let x2: Vec<f32> = (0..k).map(|i| (i as f32 * 0.07).cos()).collect();
let a1 = resident.matmul_nt(&ctx, &x1, 1).expect("dispatch 1");
let b = resident.matmul_nt(&ctx, &x2, 1).expect("dispatch 2");
let a2 = resident.matmul_nt(&ctx, &x1, 1).expect("dispatch 3");
assert_eq!(a1, a2, "the same input must give the same answer after an intervening call");
assert_ne!(a1, b, "different inputs must give different answers");
}
#[test]
fn upload_rejects_a_k_that_is_not_a_whole_number_of_blocks() {
let Ok(ctx) = crate::GpuContext::new() else {
eprintln!("skipped: no GPU on this machine");
return;
};
let err = ResidentBlockQ8Weight::upload(&ctx, &[0i8; 40], &[1.0], 1, 40).unwrap_err();
assert!(matches!(err, GpuOpError::InvalidInput(ref m) if m.contains("multiple of BLOCK")));
}
}