use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, mpsc};
use wgpu::BindGroup;
pub use wgpu::Buffer;
use crate::error::{Error, Result, bail};
use crate::gguf::{Tensor, Type};
type Pipeline = wgpu::ComputePipeline;
pub type Groups = Vec<(Pipeline, Vec<Buffer>, BindGroup)>;
const MARKS: u64 = 256;
pub struct Gpu {
device: wgpu::Device,
queue: wgpu::Queue,
weights: wgpu::BufferUsages,
kernels: Kernels,
fast: Fast,
spare: Buffer,
marks: Buffer,
marks_read: Buffer,
calls: AtomicU32,
pending: AtomicU64,
busy: Mutex<()>,
failure: Arc<Mutex<Option<String>>>,
halves: Mutex<Option<Buffer>>,
}
struct Kernels {
gemm: Vec<Pipeline>,
gemm_rows: usize,
layer_norm: Pipeline,
im2col: Pipeline,
conv_glu: Pipeline,
flash: Pipeline,
relative: Pipeline,
scores: Pipeline,
softmax: Pipeline,
values: Pipeline,
depthwise: Pipeline,
first_depthwise: Pipeline,
flatten: Pipeline,
rope: Pipeline,
group_norm: Pipeline,
swiglu: Pipeline,
argmax: Pipeline,
mark: Pipeline,
gemm_module: wgpu::ShaderModule,
decoder: OnceLock<Decoder>,
}
impl Kernels {
fn new(device: &wgpu::Device, gemm_rows: usize) -> Self {
let gemm = module(device, include_str!("gemm.wgsl"));
let ops = module(device, include_str!("ops.wgsl"));
let attention = module(device, include_str!("attention.wgsl"));
let spatial = module(device, include_str!("spatial.wgsl"));
Self {
gemm: (0..3)
.map(|ty| {
let constants = [("WTYPE", ty as f64), ("ROWS", gemm_rows as f64)];
pipeline(device, &gemm, "gemm", &constants)
})
.collect(),
gemm_rows,
layer_norm: pipeline(device, &ops, "layer_norm", &[]),
im2col: pipeline(device, &ops, "im2col", &[]),
conv_glu: pipeline(device, &ops, "conv_glu", &[]),
flash: pipeline(device, &attention, "flash", &[]),
relative: pipeline(device, &attention, "relative", &[]),
scores: pipeline(device, &attention, "scores", &[]),
softmax: pipeline(device, &ops, "softmax", &[]),
values: pipeline(device, &attention, "values", &[]),
depthwise: pipeline(device, &spatial, "depthwise", &[]),
first_depthwise: pipeline(device, &spatial, "first_depthwise", &[]),
flatten: pipeline(device, &spatial, "flatten", &[]),
rope: pipeline(device, &ops, "rotary", &[]),
group_norm: pipeline(device, &ops, "group_norm", &[]),
swiglu: pipeline(device, &ops, "swiglu", &[]),
argmax: pipeline(device, &ops, "argmax", &[]),
mark: pipeline(device, &ops, "mark", &[]),
gemm_module: gemm,
decoder: OnceLock::new(),
}
}
}
type FastPipelines = (Vec<Pipeline>, [Option<Pipeline>; 4]);
fn compile_fast(device: &wgpu::Device, f: usize) -> Option<FastPipelines> {
let scopes = [wgpu::ErrorFilter::Internal, wgpu::ErrorFilter::Validation]
.map(|f| device.push_error_scope(f));
let source = include_str!("gemm_fast.wgsl")
.replace("F: u32 = 8u", &format!("F: u32 = {f}u"))
.replace("8x8", &format!("{f}x{f}"));
let gemm = module(device, &source);
let gemm: Vec<_> = (0..6)
.map(|i| {
let constants = [("WTYPE", (i / 2) as f64), ("ROWS", ((4 * f) << (i % 2)) as f64)];
pipeline(device, &gemm, "gemm", &constants)
})
.collect();
let kernels = (f == 8).then(|| {
let ops = module(device, include_str!("ops_fast.wgsl"));
let relative = module(device, include_str!("attention_fast.wgsl"));
let flash = module(device, include_str!("flash.wgsl"));
[
pipeline(device, &ops, "layer_norm", &[]),
pipeline(device, &ops, "conv_glu", &[]),
pipeline(device, &flash, "flash", &[]),
pipeline(device, &relative, "attention", &[]),
]
});
let kernels = kernels.map(|k| k.map(Some)).unwrap_or_else(|| {
let flash = module(device, include_str!("flash_f16.wgsl"));
[None, None, Some(pipeline(device, &flash, "flash", &[])), None]
});
let errors = scopes.into_iter().rev().filter_map(|s| pollster::block_on(s.pop())).count();
(errors == 0).then_some((gemm, kernels))
}
struct Decoder {
layer_norm: Option<Pipeline>,
gemv: Vec<Pipeline>,
embed: Vec<Pipeline>,
attend: Pipeline,
combine: Pipeline,
pack: Pipeline,
}
#[derive(Default)]
struct Fast {
size: usize,
gemm: Option<Vec<Pipeline>>,
layer_norm: Option<Pipeline>,
conv_glu: Option<Pipeline>,
attention: Option<Pipeline>,
attention_size: usize,
relative: Option<Pipeline>,
tensor: Option<TensorGemm>,
}
struct TensorGemm {
cast: Pipeline,
gemm: Vec<Pipeline>,
}
pub struct Linear {
n: usize,
cols: usize,
k: usize,
dual: usize,
ty: usize,
weight: Buffer,
scales: Option<Buffer>,
bias: Buffer,
}
impl Linear {
pub fn width(&self) -> usize {
self.cols + self.dual
}
}
pub struct Channels {
w: Buffer,
b: Buffer,
}
#[derive(Clone, Copy, PartialEq)]
pub enum ConvNorm {
LayerNorm,
Affine,
}
#[derive(Clone, Copy)]
pub enum Epilogue {
Bias,
Silu,
Relu,
Residual(f32),
Gelu,
GeluResidual,
Tanh,
}
impl Epilogue {
fn params(self) -> [u32; 2] {
match self {
Epilogue::Bias => [0, 0],
Epilogue::Silu => [1, 0],
Epilogue::Relu => [2, 0],
Epilogue::Residual(alpha) => [3, alpha.to_bits()],
Epilogue::Gelu => [4, 0],
Epilogue::GeluResidual => [5, 0],
Epilogue::Tanh => [6, 0],
}
}
}
impl Gpu {
pub fn new(fast: bool) -> Result<Self> {
let adapter = adapter()?;
let size = if fast { fast_size(&adapter) } else { None };
let fast = size.is_some();
let tensor = size == Some(8) && tensor_gpu(&adapter);
let mappable = adapter.get_info().device_type == wgpu::DeviceType::IntegratedGpu
&& adapter.features().contains(wgpu::Features::MAPPABLE_PRIMARY_BUFFERS);
let mut features = wgpu::Features::IMMEDIATES;
if fast {
features |= COOP;
}
if mappable {
features |= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS;
}
if tensor {
features |= wgpu::Features::PASSTHROUGH_SHADERS;
}
let supported = adapter.limits();
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_features: features,
required_limits: wgpu::Limits {
max_immediate_size: 32,
max_buffer_size: supported.max_buffer_size,
max_storage_buffer_binding_size: supported.max_storage_buffer_binding_size,
max_compute_workgroup_storage_size: supported.max_compute_workgroup_storage_size,
..Default::default()
},
experimental_features: if fast {
unsafe { wgpu::ExperimentalFeatures::enabled() }
} else {
wgpu::ExperimentalFeatures::disabled()
},
memory_hints: wgpu::MemoryHints::MemoryUsage,
..Default::default()
}))?;
let failure = Arc::new(Mutex::new(None));
let f = failure.clone();
device.on_uncaptured_error(Arc::new(move |e| fail(&f, e.to_string())));
let f = failure.clone();
device.set_device_lost_callback(move |_, message| fail(&f, format!("device lost: {message}")));
let (kernels, (compiled, tensor)) = std::thread::scope(|s| {
let compiled = s.spawn(|| {
let fast = size.and_then(|f| compile_fast(&device, f));
(fast, if tensor { compile_tensor(&device) } else { None })
});
let rows = if size == Some(16) { 32 } else { 64 };
(Kernels::new(&device, rows), compiled.join().unwrap())
});
let weights = wgpu::BufferUsages::STORAGE
| if mappable { wgpu::BufferUsages::MAP_WRITE } else { wgpu::BufferUsages::COPY_DST };
let buffer = |size, usage| {
device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size,
usage,
mapped_at_creation: false,
})
};
let spare = buffer(4, wgpu::BufferUsages::STORAGE);
let marks = buffer(MARKS * 4, wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC);
let marks_read = buffer(MARKS * 4, wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST);
let mut gpu = Self {
device,
queue,
weights,
kernels,
fast: Fast::default(),
spare,
marks,
marks_read,
calls: AtomicU32::new(0),
pending: AtomicU64::new(0),
busy: Mutex::new(()),
failure,
halves: Mutex::new(None),
};
if let (Some(size), Some(compiled)) = (size, compiled) {
gpu.fast = gpu.probe(size, compiled);
}
gpu.fast.tensor = tensor;
if gpu.fast.tensor.is_some() && !gpu.probe_tensor() {
gpu.fast.tensor = None;
}
Ok(gpu)
}
fn probe_tensor(&self) -> bool {
let (m, k, n) = (130, 32, 64);
let a: Vec<f32> = (0..m * k).map(|i| (i % k + i / k % 32) as f32).collect();
let one = |j: usize, x: usize| if x == j % 32 { 0x3c00 } else { 0 };
let w: Vec<f32> = (0..n * k / 2)
.map(|i| (i / 16, i % 16 * 2))
.map(|(j, x)| f32::from_bits(one(j, x) | one(j, x + 1) << 16))
.collect();
let (Ok(a), Ok(weight), Ok(bias)) = (self.upload(&a), self.upload(&w), self.upload(&[0.0; 64]))
else {
return false;
};
let l = Linear { n, cols: n, k, dual: 0, ty: 1, weight, scales: None, bias };
let (c, read) = (self.buffer(m * n), self.readback(m * n));
let got =
self.run(&mut Vec::new(), &c, &read, m * n, |p| p.gemm(&a, &l, &c, m, Epilogue::Bias));
let want = (0..m * n).map(|i| (i % n % 32 + i / n % 32) as f32);
got.is_ok_and(|got| got.into_iter().eq(want))
}
fn halves(&self, len: usize) -> Buffer {
let mut halves = self.halves.lock().unwrap_or_else(PoisonError::into_inner);
if halves.as_ref().is_none_or(|h| h.size() < 2 * len as u64) {
*halves = Some(self.buffer(len.div_ceil(2)));
}
halves.clone().unwrap()
}
fn probe(&self, size: usize, compiled: FastPipelines) -> Fast {
let (gemm, [layer_norm, conv_glu, attention, relative]) = compiled;
let mut probes: Vec<_> = gemm.into_iter().map(|k| (Some(k), 5, 2, 6, 4.0)).collect();
probes.extend([
(layer_norm, 6, 3, 4, 4.0),
(conv_glu, 6, 5, 6, 4.0),
(attention, 3, 2, 7, 4.0),
(relative, 4, 3, 8, 1.0),
]);
let read = self.readback(2 * probes.len());
let mut groups = Vec::new();
let mut p = Pass::new(self, &mut groups, 0);
let outs: Vec<Buffer> = (probes.iter())
.map(|(k, bindings, out, params, _)| {
let mut buffers: Vec<Buffer> = (0..*bindings).map(|_| self.buffer(16)).collect();
if let Some(k) = k {
p.run(k, &buffers.iter().collect::<Vec<_>>(), &[0; 8][..*params], [1, 1, 1]);
}
buffers.swap_remove(*out)
})
.collect();
let Pass { mut encoder, pass, .. } = p;
drop(pass);
for (i, out) in outs.iter().enumerate() {
encoder.copy_buffer_to_buffer(out, 0, &read, 8 * i as u64, 8);
}
self.queue.submit([encoder.finish()]);
let (tx, rx) = mpsc::channel();
read.slice(..).map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap());
let mapped = self.device.poll(wgpu::PollType::wait_indefinitely()).is_ok()
&& rx.recv().unwrap().is_ok()
&& self.failure.lock().unwrap_or_else(PoisonError::into_inner).take().is_none();
let range = mapped.then(|| read.slice(..).get_mapped_range().ok()).flatten();
let got: Vec<f32> = range.map(|r| bytemuck::cast_slice(&r).to_vec()).unwrap_or_default();
let mut kept = (probes.into_iter().enumerate()).map(|(i, (k, .., subgroups))| {
k.filter(|_| got.get(2 * i..2 * i + 2) == Some(&[32.0, subgroups][..]))
});
let mut next = || kept.next().flatten();
let gemm: Vec<_> = (0..6).map(|_| next()).collect();
let gemm: Option<Vec<_>> = gemm.into_iter().collect();
let (layer_norm, conv_glu, attention, relative) = (next(), next(), next(), next());
Fast {
size: if gemm.is_some() { size } else { 0 },
gemm,
layer_norm,
conv_glu,
attention_size: if attention.is_some() { size } else { 0 },
attention,
relative,
tensor: None,
}
}
pub fn buffer(&self, len: usize) -> Buffer {
self.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: (len.max(1) * 4) as u64,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
pub fn readback(&self, len: usize) -> Buffer {
self.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: (len.max(1) * 4) as u64,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn fill(&self, size: usize, len: usize) -> Result<Fill> {
let staged = !self.weights.contains(wgpu::BufferUsages::MAP_WRITE);
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: size.max(4).next_multiple_of(4) as u64,
usage: self.weights,
mapped_at_creation: !staged,
});
if !staged {
let view = View::Mapped(buffer.get_mapped_range_mut(..)?);
return Ok(Fill { buffer, view });
}
let aligned = len.max(1).next_multiple_of(4);
let size = wgpu::BufferSize::new(aligned as u64).unwrap();
let mut view = (self.queue.write_buffer_with(&buffer, 0, size))
.ok_or_else(|| Error::Gpu(format!("staging {aligned} bytes for a weight buffer failed")))?;
view.slice(len..).copy_from_slice(&[0; 3][..aligned - len]);
Ok(Fill { buffer, view: View::Staged(view) })
}
fn filled(&self, f: Fill) -> Buffer {
let Fill { buffer, view } = f;
match view {
View::Mapped(view) => {
drop(view);
buffer.unmap();
}
View::Staged(view) => {
let len = view.len() as u64;
drop(view);
if self.pending.fetch_add(len, Ordering::Relaxed) + len >= 16 << 20 {
self.pending.store(0, Ordering::Relaxed);
self.queue.submit([]);
}
}
}
buffer
}
pub fn upload(&self, data: &[f32]) -> Result<Buffer> {
let bytes: &[u8] = bytemuck::cast_slice(data);
let mut f = self.fill(bytes.len(), bytes.len())?;
f.slice(0..bytes.len()).copy_from_slice(bytes);
Ok(self.filled(f))
}
pub fn write(&self, buffer: &Buffer, data: &[f32]) {
self.queue.write_buffer(buffer, 0, bytemuck::cast_slice(data));
}
pub fn linear(&self, weights: &[Tensor], bias: &[f32]) -> Result<Linear> {
self.upload_linear(weights, bias, false)
}
pub fn conv(&self, weight: Tensor, bias: &[f32]) -> Result<Linear> {
self.upload_linear(&[weight], bias, true)
}
fn upload_linear(&self, weights: &[Tensor], bias: &[f32], pad: bool) -> Result<Linear> {
let w0 = &weights[0];
let k = w0.len() / w0.dims.last().unwrap();
let rows = weights.iter().map(|w| w.dims.last().unwrap()).sum::<usize>();
if weights.iter().any(|w| w.ty != w0.ty || w.len() / w.dims.last().unwrap() != k) {
bail!("stacked weights differ in type or shape");
}
if !pad && !k.is_multiple_of(32) {
bail!("linear layer {k}→{rows}: needs k % 32 == 0");
}
let n = rows.next_multiple_of(64);
let cols = if rows.is_multiple_of(32) { rows } else { n };
let kp = k.next_multiple_of(32);
const PIECE: usize = 34 << 13;
let (ty, weight, scales) = match w0.ty {
_ if kp != k => {
let mut padded = vec![0f32; rows * kp];
let w = weights.iter().map(|w| w.to_f32()).collect::<Result<Vec<_>>>()?;
for (row, w) in padded.chunks_mut(kp).zip(w.iter().flat_map(|w| w.chunks(k))) {
row[..k].copy_from_slice(w);
}
let mut f = self.fill(n * kp * 4, rows * kp * 4)?;
f.slice(0..rows * kp * 4).copy_from_slice(bytemuck::cast_slice(&padded));
(0, self.filled(f), None)
}
Type::F32 | Type::F16 => {
let (ty, width) = if w0.ty == Type::F32 { (0, 4) } else { (1, 2) };
let mut f = self.fill(n * k * width, rows * k * width)?;
let mut at = 0;
for w in weights {
w.read(PIECE, |i, piece| f.slice(at + i..at + i + piece.len()).copy_from_slice(piece))?;
at += w.len() * width;
}
(ty, self.filled(f), None)
}
Type::Q8_0 => {
let (mut dv, mut sv) = (self.fill(n * k, rows * k)?, self.fill(n * k / 16, rows * k / 16)?);
let mut block = 0;
for w in weights {
w.read(PIECE, |i, piece| {
let blocks = piece.as_chunks::<34>().0;
let (a, b) = (block + i / 34, block + i / 34 + blocks.len());
let d = dv.slice(a * 32..b * 32).into_chunks::<32>().0;
d.write_iter(blocks.iter().map(|q| <[u8; 32]>::try_from(&q[2..]).unwrap()));
let s = sv.slice(a * 2..b * 2).into_chunks::<2>().0;
s.write_iter(blocks.iter().map(|q| [q[0], q[1]]));
})?;
block += w.len() / 32;
}
(2, self.filled(dv), Some(self.filled(sv)))
}
};
let dual = bias.len().saturating_sub(rows);
if !bias.is_empty() && bias.len() < rows || dual > rows {
bail!("linear layer {k}→{rows}: bias has {} values", bias.len());
}
let (own, second) = bias.split_at(bias.len() - dual);
let mut padded = vec![0.0; n.max(cols + dual)];
padded[..own.len()].copy_from_slice(own);
padded[cols..cols + dual].copy_from_slice(second);
Ok(Linear { n, cols, k: kp, dual, ty, weight, scales, bias: self.upload(&padded)? })
}
pub fn channels(&self, w: &[f32], b: &[f32]) -> Result<Channels> {
Ok(Channels { w: self.upload(w)?, b: self.upload(b)? })
}
fn decoder(&self) -> &Decoder {
self.kernels.decoder.get_or_init(|| {
let device = &self.device;
let fast = self.fast.size != 0;
let source =
|s: &str| if fast { swap_fns(s, include_str!("subgroup.wgsl")) } else { s.into() };
let gemm = match fast {
true => &module(device, &source(include_str!("gemm.wgsl"))),
false => &self.kernels.gemm_module,
};
let decode = module(device, &source(include_str!("decode.wgsl")));
let ops = fast.then(|| module(device, &source(include_str!("ops.wgsl"))));
Decoder {
layer_norm: ops.map(|ops| pipeline(device, &ops, "layer_norm", &[])),
gemv: gemms(device, gemm, "gemv"),
embed: gemms(device, gemm, "embed"),
attend: pipeline(device, &decode, "attend", &[]),
combine: pipeline(device, &decode, "combine", &[]),
pack: pipeline(device, &decode, "pack", &[]),
}
})
}
fn tile_rows(&self, n: usize, m: usize) -> Option<usize> {
self.fast.gemm.as_ref()?;
let short = 4 * self.fast.size;
let (cols, m8, m4) = (n.div_ceil(2 * short), m.div_ceil(2 * short), m.div_ceil(short));
if short == 64 && cols * m4 < 16 {
return None;
}
Some(if m8 * cols >= 64 && m8 * 20 <= m4 * 11 { 2 * short } else { short })
}
fn flash(&self, head_dim: usize, relative: bool) -> Option<&Pipeline> {
let (k, min, max, step) = match (relative, self.fast.attention_size) {
(true, _) => (&self.fast.relative, 8, 128, 8),
(false, 8) => (&self.fast.attention, 32, 64, 4),
(false, _) => (&self.fast.attention, 4, 64, 4),
};
k.as_ref().filter(|_| (min..=max).contains(&head_dim) && head_dim.is_multiple_of(step))
}
fn portable_flash(head_dim: usize, relative: bool) -> bool {
head_dim <= if relative { 128 } else { 64 } && head_dim.is_multiple_of(4)
}
pub fn scores_len(&self, t: usize, heads: usize, head_dim: usize, relative: bool) -> usize {
match self.flash(head_dim, relative).is_some() || Self::portable_flash(head_dim, relative) {
true => 1,
false => heads * t * t,
}
}
fn begin(&self) -> (wgpu::CommandEncoder, wgpu::ComputePass<'static>) {
let mut encoder = self.device.create_command_encoder(&Default::default());
let pass = encoder.begin_compute_pass(&Default::default()).forget_lifetime();
(encoder, pass)
}
pub fn run(
&self,
groups: &mut Groups,
out: &Buffer,
read: &Buffer,
len: usize,
record: impl FnOnce(&mut Pass),
) -> Result<Vec<f32>> {
let busy = self.busy();
let r = self.record_call(groups, out, read, len, record);
self.complete(busy, r, read, || ())
}
pub fn record(
&self,
groups: &mut Groups,
out: &Buffer,
read: &Buffer,
len: usize,
record: impl FnOnce(&mut Pass),
) -> Recorded {
self.record_call(groups, out, read, len, |p| {
record(p);
assert!(p.queued.is_none(), "Gpu::record of a call with Pass::layers");
})
}
pub fn finish(&self, r: Recorded, read: &Buffer, meanwhile: impl FnOnce()) -> Result<Vec<f32>> {
self.complete(self.busy(), r, read, meanwhile)
}
fn busy(&self) -> MutexGuard<'_, ()> {
self.busy.lock().unwrap_or_else(PoisonError::into_inner)
}
fn record_call(
&self,
groups: &mut Groups,
out: &Buffer,
read: &Buffer,
len: usize,
record: impl FnOnce(&mut Pass),
) -> Recorded {
let call = self.calls.fetch_add(1, Ordering::Relaxed).wrapping_add(1);
let mut p = Pass::new(self, groups, call);
record(&mut p);
p.mark(out);
let Pass { mut encoder, pass, marked, .. } = p;
drop(pass);
let (size, marks) = ((len * 4) as u64, marked as u64 * 4);
encoder.copy_buffer_to_buffer(out, 0, read, 0, size);
encoder.copy_buffer_to_buffer(&self.marks, 0, &self.marks_read, 0, marks);
Recorded { commands: encoder.finish(), call, size, marks }
}
fn complete(
&self,
_busy: MutexGuard<'_, ()>,
r: Recorded,
read: &Buffer,
meanwhile: impl FnOnce(),
) -> Result<Vec<f32>> {
let Recorded { commands, call, size, marks } = r;
self.queue.submit([commands]);
meanwhile();
let slices = [read.slice(..size), self.marks_read.slice(..marks)];
let (tx, rx) = mpsc::channel();
for slice in &slices {
let tx = tx.clone();
slice.map_async(wgpu::MapMode::Read, move |r| tx.send(r).unwrap());
}
self.device.poll(wgpu::PollType::wait_indefinitely())?;
for _ in &slices {
rx.recv().unwrap()?;
}
let data = bytemuck::cast_slice(&slices[0].get_mapped_range()?).to_vec();
let done =
bytemuck::cast_slice::<_, u32>(&slices[1].get_mapped_range()?).iter().all(|&m| m == call);
read.unmap();
self.marks_read.unmap();
if let Some(failure) = self.failure.lock().unwrap_or_else(PoisonError::into_inner).take() {
return Err(Error::Gpu(failure));
}
if !done {
return Err(Error::Gpu("the driver did not finish the work (cancelled as too long?)".into()));
}
Ok(data)
}
}
pub struct Recorded {
commands: wgpu::CommandBuffer,
call: u32,
size: u64,
marks: u64,
}
struct Fill {
buffer: Buffer,
view: View,
}
enum View {
Mapped(wgpu::BufferViewMut),
Staged(wgpu::QueueWriteBufferView),
}
impl Fill {
fn slice(&mut self, range: std::ops::Range<usize>) -> wgpu::WriteOnly<'_, [u8]> {
match &mut self.view {
View::Mapped(v) => v.slice(range),
View::Staged(v) => v.slice(range),
}
}
}
fn swap_fns(source: &str, fns: &str) -> String {
let mut out = source.to_string();
for f in fns.split("\nfn ").skip(1) {
let name = &f[..f.find('(').unwrap()];
let Some(start) = out.find(&format!("\nfn {name}(")) else { continue };
let end = start + out[start..].find("\n}").unwrap() + 2;
out.replace_range(start..end, &format!("\nfn {}", &f[..f.find("\n}").unwrap() + 2]));
}
out
}
fn fail(failure: &Mutex<Option<String>>, message: String) {
failure.lock().unwrap_or_else(PoisonError::into_inner).get_or_insert(message);
}
const ATTEND_KEYS: usize = 256;
pub fn attend_len(n: usize, heads: usize, head_dim: usize) -> usize {
n.div_ceil(ATTEND_KEYS) * heads * (head_dim + 2)
}
pub fn pos_rows(t: usize) -> usize {
2 * t + 15
}
pub fn half(n: usize) -> usize {
(n - 1) / 2 + 1
}
const COOP: wgpu::Features = wgpu::Features::SUBGROUP
.union(wgpu::Features::SUBGROUP_BARRIER)
.union(wgpu::Features::SHADER_F16)
.union(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX);
fn adapter() -> Result<wgpu::Adapter> {
let request = |backends| {
let desc = wgpu::InstanceDescriptor {
backends,
..wgpu::InstanceDescriptor::new_without_display_handle()
};
let instance = wgpu::Instance::new(desc.with_env());
pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
..Default::default()
}))
};
let primary = request(wgpu::Backends::VULKAN | wgpu::Backends::METAL);
Ok(primary.or_else(|e| request(wgpu::Backends::DX12).map_err(|_| e))?)
}
fn fast_size(adapter: &wgpu::Adapter) -> Option<usize> {
use wgpu::CooperativeScalarType::{F16, F32};
let info = adapter.get_info();
let matrices = adapter.cooperative_matrix_properties();
let has = |f, ab| {
let shape = (f, f, f, ab, F32);
matrices.iter().any(|p| (p.m_size, p.n_size, p.k_size, p.ab_type, p.cr_type) == shape)
};
if !adapter.features().contains(COOP)
|| !(info.subgroup_min_size..=info.subgroup_max_size).contains(&32)
{
return None;
}
const NVIDIA: u32 = 0x10de;
if has(8, F32) && has(8, F16) {
Some(8)
} else {
(has(16, F16) && info.vendor == NVIDIA).then_some(16)
}
}
fn tensor_gpu(adapter: &wgpu::Adapter) -> bool {
adapter.features().contains(wgpu::Features::PASSTHROUGH_SHADERS)
&& tensor_chip(&adapter.get_info().name)
}
fn tensor_chip(name: &str) -> bool {
let chip = name.split_whitespace().find_map(|w| {
let (family, n) = w.split_at_checked(1)?;
Some((family, n.parse::<u32>().ok()?)).filter(|(f, _)| *f == "M" || *f == "A")
});
matches!(chip, Some(("M", 5..)) | Some(("A", 19..)))
}
fn compile_tensor(device: &wgpu::Device) -> Option<TensorGemm> {
let scopes = [wgpu::ErrorFilter::Internal, wgpu::ErrorFilter::Validation]
.map(|f| device.push_error_scope(f));
let names = ["cast", "gemm_f32", "gemm_f16", "gemm_q8"];
let entry_points = names.map(|n| wgpu::PassthroughShaderEntryPoint {
name: n.into(),
workgroup_size: (if n == "cast" { 256 } else { 128 }, 1, 1),
});
let desc = wgpu::ShaderModuleDescriptorPassthrough {
entry_points: entry_points.to_vec().into(),
msl: Some(include_str!("gemm_tensor.metal").into()),
..Default::default()
};
let module = unsafe { device.create_shader_module_passthrough(desc) };
let pipeline = |entry: &str, bindings: u32, out: u32, immediates: u32| {
let entries: Vec<_> = (0..bindings)
.map(|binding| wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: binding != out },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
})
.collect();
let desc = wgpu::BindGroupLayoutDescriptor { label: None, entries: &entries };
let group = device.create_bind_group_layout(&desc);
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[Some(&group)],
immediate_size: immediates,
});
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(entry),
layout: Some(&layout),
module: &module,
entry_point: Some(entry),
compilation_options: Default::default(),
cache: None,
})
};
let cast = pipeline("cast", 2, 1, 4);
let gemm = names[1..].iter().map(|n| pipeline(n, 5, 2, 24)).collect();
let errors = scopes.into_iter().rev().filter_map(|s| pollster::block_on(s.pop())).count();
(errors == 0).then_some(TensorGemm { cast, gemm })
}
fn module(device: &wgpu::Device, source: &str) -> wgpu::ShaderModule {
let desc =
wgpu::ShaderModuleDescriptor { label: None, source: wgpu::ShaderSource::Wgsl(source.into()) };
unsafe { device.create_shader_module_trusted(desc, wgpu::ShaderRuntimeChecks::unchecked()) }
}
fn pipeline(
device: &wgpu::Device,
module: &wgpu::ShaderModule,
entry: &str,
constants: &[(&str, f64)],
) -> Pipeline {
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(entry),
layout: None,
module,
entry_point: Some(entry),
compilation_options: wgpu::PipelineCompilationOptions {
constants,
zero_initialize_workgroup_memory: false,
},
cache: None,
})
}
fn gemms(device: &wgpu::Device, module: &wgpu::ShaderModule, entry: &str) -> Vec<Pipeline> {
(0..3).map(|ty| pipeline(device, module, entry, &[("WTYPE", ty as f64)])).collect()
}
pub struct Pass<'a> {
gpu: &'a Gpu,
encoder: wgpu::CommandEncoder,
pass: wgpu::ComputePass<'static>,
groups: &'a mut Groups,
next: usize,
call: u32,
marked: u32,
queued: Option<wgpu::SubmissionIndex>,
}
impl<'a> Pass<'a> {
fn new(gpu: &'a Gpu, groups: &'a mut Groups, call: u32) -> Self {
let (encoder, pass) = gpu.begin();
Self { gpu, encoder, pass, groups, next: 0, call, marked: 0, queued: None }
}
pub fn layers<'b, T>(&mut self, items: &[T], mut layer: impl FnMut(&mut Self, &T) -> &'b Buffer) {
for item in items {
let out = layer(self, item);
self.flush(out);
}
}
fn flush(&mut self, after: &Buffer) {
if self.gpu.fast.size == 8 {
return;
}
self.mark(after);
let (encoder, pass) = self.gpu.begin();
drop(std::mem::replace(&mut self.pass, pass));
let done = std::mem::replace(&mut self.encoder, encoder);
let index = self.gpu.queue.submit([done.finish()]);
if let Some(previous) = self.queued.replace(index) {
let wait = wgpu::PollType::Wait { submission_index: Some(previous), timeout: None };
let _ = self.gpu.device.poll(wait);
}
}
fn mark(&mut self, after: &Buffer) {
let gpu = self.gpu;
let group = self.bind_group(&gpu.kernels.mark, &[after, &gpu.marks]);
self.dispatch(&gpu.kernels.mark, &group, &[self.marked, self.call], [1, 1, 1]);
self.marked += 1;
}
fn run(&mut self, kernel: &Pipeline, buffers: &[&Buffer], params: &[u32], size: [usize; 3]) {
let cached =
|(k, b, _): &(Pipeline, Vec<Buffer>, _)| k == kernel && b.iter().eq(buffers.iter().copied());
if !self.groups.get(self.next).is_some_and(cached) {
let owned = buffers.iter().map(|&b| b.clone()).collect();
let group = (kernel.clone(), owned, self.bind_group(kernel, buffers));
match self.groups.get_mut(self.next) {
Some(g) => *g = group,
None => self.groups.push(group),
}
}
let group = self.groups[self.next].2.clone();
self.dispatch(kernel, &group, params, size);
self.next += 1;
}
fn bind_group(&self, kernel: &Pipeline, buffers: &[&Buffer]) -> BindGroup {
let entries: Vec<_> = buffers
.iter()
.enumerate()
.map(|(i, b)| wgpu::BindGroupEntry { binding: i as u32, resource: b.as_entire_binding() })
.collect();
let layout = kernel.get_bind_group_layout(0);
let desc = wgpu::BindGroupDescriptor { label: None, layout: &layout, entries: &entries };
self.gpu.device.create_bind_group(&desc)
}
fn dispatch(
&mut self,
kernel: &Pipeline,
group: &BindGroup,
params: &[u32],
[x, y, z]: [usize; 3],
) {
self.pass.set_pipeline(kernel);
self.pass.set_bind_group(0, group, &[]);
self.pass.set_immediates(0, bytemuck::cast_slice(params));
self.pass.dispatch_workgroups(x as u32, y as u32, z as u32);
}
pub fn gemm(&mut self, a: &Buffer, l: &Linear, c: &Buffer, m: usize, epilogue: Epilogue) {
let gpu = self.gpu;
let [mode, alpha] = epilogue.params();
let params = [m as u32, l.cols as u32, l.k as u32, mode, alpha, l.dual as u32];
let scales = l.scales.as_ref().unwrap_or(&l.bias);
if let Some(t) = &gpu.fast.tensor {
let (h, n4) = (gpu.halves(m * l.k), m * l.k / 4);
self.run(&t.cast, &[a, &h], &[n4 as u32], [n4.div_ceil(256).min(65535), 1, 1]);
let size = [l.n / 64, m.div_ceil(128), 1];
return self.run(&t.gemm[l.ty], &[&h, &l.bias, c, &l.weight, scales], ¶ms, size);
}
let buffers = [a, &l.bias, c, &l.weight, scales];
let f = gpu.fast.size;
match (&gpu.fast.gemm, gpu.tile_rows(l.n, m)) {
(Some(k), Some(rows)) => {
let tall = rows > 4 * f;
self.run(
&k[2 * l.ty + tall as usize],
&buffers,
¶ms,
[l.n.div_ceil(8 * f), m.div_ceil(rows), 1],
)
}
_ => {
let size = [l.n.div_ceil(128), m.div_ceil(gpu.kernels.gemm_rows), 1];
self.run(&gpu.kernels.gemm[l.ty], &buffers, ¶ms, size)
}
}
}
pub fn layer_norm(
&mut self,
x: &Buffer,
n: &Channels,
y: &Buffer,
rows: usize,
cols: usize,
rope: Option<(&Buffer, &Buffer, usize)>,
) {
let gpu = self.gpu;
let (table, yr, head_dim) = rope.unwrap_or((x, &gpu.spare, 0));
let params = [rows as u32, cols as u32, head_dim as u32];
let buffers = [x, &n.w, &n.b, y, table, yr];
match &gpu.fast.layer_norm {
Some(k) => self.run(k, &buffers, ¶ms, [rows.div_ceil(4), 1, 1]),
None => self.run(&gpu.kernels.layer_norm, &buffers, ¶ms, [rows, 1, 1]),
}
}
pub fn layer_norm_row(&mut self, x: &Buffer, n: &Channels, y: &Buffer, cols: usize) {
match &self.gpu.decoder().layer_norm {
Some(k) => {
self.run(k, &[x, &n.w, &n.b, y, x, &self.gpu.spare], &[1, cols as u32, 0], [1, 1, 1])
}
None => self.layer_norm(x, n, y, 1, cols, None),
}
}
pub fn gemv(&mut self, a: &Buffer, l: &Linear, c: &Buffer, epilogue: Epilogue, offset: usize) {
let [mode, alpha] = epilogue.params();
let params = [1, l.cols as u32, l.k as u32, mode, alpha, 0, offset as u32];
let buffers = [a, &l.bias, c, &l.weight, l.scales.as_ref().unwrap_or(&l.bias)];
self.run(&self.gpu.decoder().gemv[l.ty], &buffers, ¶ms, [l.cols / 32, 1, 1]);
}
pub fn embed(
&mut self,
l: &Linear,
pos: &Buffer,
c: &Buffer,
(ids, i): (&Buffer, usize),
offset: usize,
) {
let params = [i as u32, l.n as u32, l.k as u32, 0, 0, 0, offset as u32];
let buffers = [pos, &l.bias, c, &l.weight, l.scales.as_ref().unwrap_or(&l.bias), ids];
self.run(&self.gpu.decoder().embed[l.ty], &buffers, ¶ms, [l.k.div_ceil(256), 1, 1]);
}
#[allow(clippy::too_many_arguments)]
pub fn attend(
&mut self,
q: &Buffer,
kv: &Buffer,
part: &Buffer,
o: &Buffer,
n: usize,
cols: usize,
heads: usize,
hd: usize,
) {
let scale = (1.0 / (hd as f32).sqrt()).to_bits();
let params = [n as u32, hd as u32, scale, cols as u32];
let k = self.gpu.decoder();
self.run(&k.attend, &[q, kv, part], ¶ms, [n.div_ceil(ATTEND_KEYS), heads, 1]);
self.run(&k.combine, &[part, o], ¶ms, [heads, 1, 1]);
}
#[allow(clippy::too_many_arguments)]
pub fn pack(
&mut self,
x: &Buffer,
[src, stride]: [usize; 2],
y: &Buffer,
rows: usize,
d: usize,
[cols, col]: [usize; 2],
) {
let params = [rows, d, 0, cols, col, src, stride].map(|v| v as u32);
let size =
if rows == 1 { [d.div_ceil(256), 1, 1] } else { [rows.div_ceil(64), d.div_ceil(4), 1] };
self.run(&self.gpu.decoder().pack, &[x, y], ¶ms, size);
}
pub fn im2col(&mut self, x: &Buffer, col: &Buffer, t: usize, ch: usize, conv: [usize; 3]) {
let [kernel, stride, pad] = conv;
let rows = (t + 2 * pad - kernel) / stride + 1;
let params = [t, ch, 0, 0, kernel, 0, stride, pad].map(|v| v as u32);
let width = (ch * kernel).next_multiple_of(32);
self.run(&self.gpu.kernels.im2col, &[x, col], ¶ms, [width.div_ceil(256), rows, 1]);
}
#[allow(clippy::too_many_arguments)]
pub fn conv_glu(
&mut self,
h: &Buffer,
dw: &Channels,
kernel: usize,
n: &Channels,
norm: ConvNorm,
y: &Buffer,
t: usize,
ch: usize,
) {
let gpu = self.gpu;
let buffers = [h, &dw.w, &dw.b, &n.w, &n.b, y];
let layer_norm = (norm == ConvNorm::LayerNorm) as u32;
let params = [t as u32, ch as u32, 0, 0, kernel as u32, layer_norm];
match &gpu.fast.conv_glu {
Some(k) => self.run(k, &buffers, ¶ms, [t.div_ceil(4), 1, 1]),
None => self.run(&gpu.kernels.conv_glu, &buffers, ¶ms, [t, 1, 1]),
}
}
#[allow(clippy::too_many_arguments)]
pub fn attention(
&mut self,
qk: &Buffer,
v: Option<&Buffer>,
pos: Option<&Buffer>,
s: &Buffer,
o: &Buffer,
t: usize,
heads: usize,
head_dim: usize,
) {
let ch = (heads * head_dim) as u32;
let (v, [a, b, c, d], queries) = match (v, pos) {
(Some(v), _) => (v, [2 * ch, ch, 0, 0], 64),
(None, None) => (qk, [3 * ch, 3 * ch, 2 * ch, 0], 64),
(None, Some(_)) => (qk, [4 * ch, 4 * ch, 2 * ch, 1], 16),
};
let scale = (1.0 / (head_dim as f32).sqrt()).to_bits();
let params = [t as u32, ch, head_dim as u32, scale, a, b, c, d];
let pos = pos.unwrap_or(qk);
if let Some(k) = self.gpu.flash(head_dim, d == 1) {
let buffers: &[&Buffer] = if d == 1 { &[qk, v, pos, o] } else { &[qk, v, o] };
return self.run(k, buffers, ¶ms[..7 + d as usize], [t.div_ceil(queries), heads, 1]);
}
let (k, tiles) = (&self.gpu.kernels, t.div_ceil(16));
if Gpu::portable_flash(head_dim, d == 1) {
let (k, buffers) = if d == 1 { (&k.relative, [qk, pos, o]) } else { (&k.flash, [qk, v, o]) };
return self.run(k, &buffers, ¶ms[..7], [t.div_ceil(32), heads, 1]);
}
self.run(&k.scores, &[qk, pos, s], ¶ms, [tiles, tiles, heads]);
self.run(&k.softmax, &[s], ¶ms[..1], [t, heads, 1]);
self.run(&k.values, &[s, v, o], ¶ms, [tiles, heads, 1]);
}
#[allow(clippy::too_many_arguments)]
pub fn depthwise(
&mut self,
x: &Buffer,
first: Option<&Channels>,
dw: &Channels,
y: &Buffer,
image: [usize; 3],
t0: usize,
rows: usize,
) {
let k = &self.gpu.kernels;
let [t, f, ch] = image.map(|v| v as u32);
let params = [t, f, ch, t0 as u32];
match first {
None => self.run(&k.depthwise, &[x, &dw.w, &dw.b, y], ¶ms, [half(image[1]), rows, 1]),
Some(c) => {
let buffers = [x, &dw.w, &dw.b, y, &c.w, &c.b];
self.run(&k.first_depthwise, &buffers, ¶ms, [half(half(image[1])), rows, 1]);
}
}
}
pub fn rope(&mut self, table: &Buffer, qkv: &Buffer, rows: usize, pos: usize, shape: [usize; 3]) {
let [heads, hd, rot] = shape;
let params = [rows, 3 * heads * hd, hd, 0, rot, 0, 2 * heads, pos].map(|v| v as u32);
let size = [(heads * rot).div_ceil(256), rows, 1];
self.run(&self.gpu.kernels.rope, &[table, qkv], ¶ms, size);
}
pub fn group_norm(&mut self, x: &Buffer, n: &Channels, t: usize, ch: usize, eps: f32) {
let params = [t as u32, ch as u32, 0, eps.to_bits()];
self.run(&self.gpu.kernels.group_norm, &[x, &n.w, &n.b], ¶ms, [1, 1, 1]);
}
pub fn swiglu(&mut self, h: &Buffer, ch: usize) {
self.run(&self.gpu.kernels.swiglu, &[h], &[0, ch as u32], [ch.div_ceil(256), 1, 1]);
}
pub fn argmax(&mut self, x: &Buffer, out: &Buffer, n: usize) {
self.run(&self.gpu.kernels.argmax, &[x, out], &[n as u32], [1, 1, 1]);
}
pub fn flatten(&mut self, x: &Buffer, y: &Buffer, t: usize, f: usize, ch: usize) {
let k = &self.gpu.kernels.flatten;
self.run(k, &[x, y], &[t as u32, f as u32, ch as u32], [t, 1, 1]);
}
}
#[cfg(test)]
mod tests {
use std::sync::OnceLock;
use half::f16;
use super::*;
use crate::gguf::Gguf;
fn gpus() -> &'static [Gpu; 2] {
static GPUS: OnceLock<[Gpu; 2]> = OnceLock::new();
GPUS.get_or_init(|| {
let fast = Gpu::new(true).unwrap();
let f = &fast.fast;
let ops = [&f.layer_norm, &f.conv_glu, &f.attention, &f.relative];
let adapter = adapter().unwrap();
if let Some(size) = fast_size(&adapter) {
let want = [size == 8, size == 8, true, size == 8];
assert!(
f.gemm.is_some() && ops.iter().zip(want).all(|(k, w)| k.is_some() == w),
"a fast kernel failed its probe"
);
let tensor = size == 8 && tensor_gpu(&adapter);
assert_eq!(f.tensor.is_some(), tensor, "the tensor GEMM failed its probe");
}
[Gpu::new(false).unwrap(), fast]
})
}
fn random(n: usize, seed: u32) -> Vec<f32> {
let mut x = seed.wrapping_mul(2654435761) | 1;
(0..n)
.map(|_| {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
x as f32 / u32::MAX as f32 * 2.0 - 1.0
})
.collect()
}
fn up(gpu: &Gpu, data: &[f32]) -> Buffer {
gpu.upload(data).unwrap()
}
fn run(gpu: &Gpu, init: &[f32], record: impl FnOnce(&mut Pass, &Buffer)) -> Vec<f32> {
let out = gpu.buffer(init.len());
gpu.write(&out, init);
let read = gpu.readback(init.len());
gpu.run(&mut Vec::new(), &out, &read, init.len(), |p| record(p, &out)).unwrap()
}
fn assert_close(got: &[f32], want: &[f32]) {
assert_near(got, want, 2e-5);
}
fn assert_near(got: &[f32], want: &[f32], tolerance: f32) {
assert_eq!(got.len(), want.len());
let scale = want.iter().fold(1f32, |m, x| m.max(x.abs()));
for (i, (g, w)) in got.iter().zip(want).enumerate() {
assert!((g - w).abs() <= tolerance * scale, "[{i}]: {g} vs {w}");
}
}
fn layer_norm(x: &[f32], g: &[f32], b: &[f32]) -> Vec<f32> {
let n = g.len() as f32;
(x.chunks(g.len()))
.flat_map(|row| {
let mean = row.iter().sum::<f32>() / n;
let var = row.iter().map(|x| (x - mean) * (x - mean)).sum::<f32>() / n;
let rstd = 1.0 / (var + 1e-5).sqrt();
row.iter().zip(g).zip(b).map(move |((x, g), b)| (x - mean) * rstd * g + b)
})
.collect()
}
fn silu(x: f32) -> f32 {
x / (1.0 + (-x).exp())
}
fn gelu(x: f32) -> f32 {
let z = (std::f32::consts::FRAC_1_SQRT_2 * x).abs();
let t = 1.0 / (1.0 + 0.327_591_1 * z);
let p = ((((1.061_405_4 * t - 1.453_152) * t) + 1.421_413_8) * t - 0.284_496_74) * t;
0.5 * x * (1.0 + x.signum() * (1.0 - (p + 0.254_829_6) * t * (-z * z).exp()))
}
fn epilogue(e: Epilogue, v: f32, c: f32) -> f32 {
match e {
Epilogue::Bias => v,
Epilogue::Silu => silu(v),
Epilogue::Relu => v.max(0.0),
Epilogue::Residual(alpha) => c + alpha * v,
Epilogue::Gelu => gelu(v),
Epilogue::GeluResidual => c + gelu(v),
Epilogue::Tanh => v.tanh(),
}
}
const EPILOGUES: [Epilogue; 7] = [
Epilogue::Bias,
Epilogue::Silu,
Epilogue::Relu,
Epilogue::Residual(0.5),
Epilogue::Gelu,
Epilogue::GeluResidual,
Epilogue::Tanh,
];
fn weights(n: usize, k: usize) -> [(Type, Vec<u8>); 3] {
let w = random(n * k, 1);
let f32s: Vec<u8> = bytemuck::cast_slice(&w).to_vec();
let f16s: Vec<u8> = w.iter().flat_map(|x| f16::from_f32(*x).to_le_bytes()).collect();
let q8: Vec<u8> = (w.chunks(32))
.flat_map(|block| {
let d = block.iter().fold(0f32, |m, x| m.max(x.abs())) / 127.0;
let q = block.iter().map(move |x| (x / d).round() as i8 as u8);
f16::from_f32(d).to_le_bytes().into_iter().chain(q)
})
.collect();
[(Type::F32, f32s), (Type::F16, f16s), (Type::Q8_0, q8)]
}
#[test]
fn tensor_chips() {
let names = ["Apple M5", "Apple M5 Pro", "Apple M7 Max", "Apple A19 Pro", "Apple A20"];
assert!(names.iter().all(|n| tensor_chip(n)));
let names = ["Apple M2", "Apple M1 Ultra", "Apple A18 Pro", "NVIDIA GeForce RTX 4060", ""];
assert!(!names.iter().any(|n| tensor_chip(n)));
assert!(!tensor_chip("Apple M2 A19"));
}
#[test]
fn failed_gemm_probe() {
let gpu = &gpus()[1];
let Some(size) = fast_size(&adapter().unwrap()) else { return };
let (mut gemm, kernels) = compile_fast(&gpu.device, size).unwrap();
gemm[0] = gpu.kernels.gemm[0].clone();
let fast = gpu.probe(size, (gemm, kernels.clone()));
assert!(fast.gemm.is_none() && fast.size == 0);
assert_eq!(fast.attention_size, if fast.attention.is_some() { size } else { 0 });
assert!([fast.layer_norm, fast.conv_glu, fast.attention, fast.relative] == kernels);
}
#[test]
fn gemm() {
for (n, cols) in [(164, 192), (288, 288)] {
let k = 96;
let w = random(n * k, 1);
let bias = random(n, 2);
let dual: Vec<f32> = bias.iter().copied().chain(random(64, 5)).collect();
let f32s: &[u8] = bytemuck::cast_slice(&w);
let f16s: Vec<u8> = w.iter().flat_map(|x| f16::from_f32(*x).to_le_bytes()).collect();
let q8: Vec<u8> = (w.chunks(32))
.flat_map(|block| {
let d = block.iter().fold(0f32, |m, x| m.max(x.abs())) / 127.0;
let q = block.iter().map(move |x| (x / d).round() as i8 as u8);
f16::from_f32(d).to_le_bytes().into_iter().chain(q)
})
.collect();
let wd = [k, n / 2];
for (ty, bytes) in [(Type::F32, f32s), (Type::F16, &f16s), (Type::Q8_0, &q8)] {
let (w1, w2) = bytes.split_at(bytes.len() / 2);
let file = Gguf::with_tensors(&[("w1", &wd, ty, w1), ("w2", &wd, ty, w2)]);
let t = |name| file.tensor(name, &wd).unwrap();
let half = |x: &f32| f16::from_f32(*x).to_f32();
let wf: Vec<f32> = [t("w1").to_f32().unwrap(), t("w2").to_f32().unwrap()].concat();
let wf: Vec<f32> = wf.iter().map(half).collect();
for b in [&bias[..], &[], &dual] {
let width = if b.len() > n { cols + 64 } else { cols };
for gpu in gpus() {
let l = gpu.linear(&[t("w1"), t("w2")], b).unwrap();
assert_eq!(l.width(), width);
for m in [1, 37, 70, 2100] {
let a = random(m * k, 3);
let fast = gpu.tile_rows(n.next_multiple_of(64), m).is_some();
let ar: Vec<f32> = if fast { a.iter().map(half).collect() } else { a.clone() };
let c = random(m * width + 1, 4);
let ab = up(gpu, &a);
for e in EPILOGUES {
let dot =
|r: usize, j: usize| (0..k).map(|x| ar[r * k + x] * wf[j * k + x]).sum::<f32>();
let want: Vec<f32> = (0..c.len())
.map(|i| {
let (r, j) = (i / width, i % width);
if r == m {
c[i]
} else if j >= cols {
b[n + j - cols] + dot(r, j - cols)
} else {
let v = if j < n { b.get(j).unwrap_or(&0.0) + dot(r, j) } else { 0.0 };
epilogue(e, v, c[i])
}
})
.collect();
assert_close(&run(gpu, &c, |p, out| p.gemm(&ab, &l, out, m, e)), &want);
}
}
}
}
}
}
}
#[test]
fn layer_norm_and_rope() {
let (rows, cols, hd) = (37, 192, 48);
let x = random(rows * cols, 5);
let (g, b) = (random(cols, 6), random(cols, 7));
let rope = random(rows * hd, 8);
let y = layer_norm(&x, &g, &b);
let yr: Vec<f32> = (0..rows * cols)
.map(|i| {
let (t, d, half) = (i / cols, i % hd, hd / 2);
let (cos, sin) = (rope[t * hd + d % half], rope[t * hd + half + d % half]);
if d < half { y[i] * cos - y[i + half] * sin } else { y[i] * cos + y[i - half] * sin }
})
.collect();
for gpu in gpus() {
let n = gpu.channels(&g, &b).unwrap();
let (xb, rb) = (up(gpu, &x), up(gpu, &rope));
let zeros = vec![0.0; rows * cols];
assert_close(&run(gpu, &zeros, |p, y| p.layer_norm(&xb, &n, y, rows, cols, None)), &y);
let got = run(gpu, &zeros[..cols], |p, y| p.layer_norm_row(&xb, &n, y, cols));
assert_close(&got, &y[..cols]);
let yb = gpu.buffer(rows * cols);
let got =
run(gpu, &zeros, |p, yr| p.layer_norm(&xb, &n, &yb, rows, cols, Some((&rb, yr, hd))));
assert_close(&got, &yr);
}
}
#[test]
fn im2col() {
let shapes =
[(37, 20, [5, 2, 2]), (37, 20, [3, 1, 1]), (37, 20, [3, 2, 1]), (900, 1, [127, 64, 0])];
for (t_in, ch, [kernel, stride, pad]) in shapes {
let x = random(t_in * ch, 9);
let t_out = (t_in + 2 * pad - kernel) / stride + 1;
let width = (ch * kernel).next_multiple_of(32);
let want: Vec<f32> = (0..t_out * width)
.map(|i| {
let (t, c, k) = (i / width, i % width / kernel, i % width % kernel);
let s = (stride * t + k).wrapping_sub(pad);
if s < t_in && c < ch { x[s * ch + c] } else { 0.0 }
})
.collect();
for gpu in gpus() {
let xb = up(gpu, &x);
let shape = [kernel, stride, pad];
let got = run(gpu, &vec![0.0; want.len()], |p, col| p.im2col(&xb, col, t_in, ch, shape));
assert_close(&got, &want);
}
}
}
#[test]
fn gemm_padded_k() {
let (n, k, m) = (64, 40, 37);
let half = |x: &f32| f16::from_f32(*x).to_f32();
let mut a = random(m * 64, 3);
a.chunks_mut(64).for_each(|row| row[k..].fill(0.0));
for (ty, bytes) in weights(n, k).into_iter().take(2) {
let file = Gguf::with_tensors(&[("w", &[k, n], ty, &bytes)]);
let t = file.tensor("w", &[k, n]).unwrap();
let wf: Vec<f32> = t.to_f32().unwrap().iter().map(half).collect();
for gpu in gpus() {
assert!(gpu.linear(std::slice::from_ref(&t), &[]).is_err());
let l = gpu.conv(file.tensor("w", &[k, n]).unwrap(), &[]).unwrap();
let fast = gpu.tile_rows(n, m).is_some();
let ar: Vec<f32> = if fast { a.iter().map(half).collect() } else { a.clone() };
let want: Vec<f32> = (0..m * n)
.map(|i| (0..k).map(|x| ar[i / n * 64 + x] * wf[i % n * k + x]).sum())
.collect();
let ab = up(gpu, &a);
let got = run(gpu, &vec![0.0; m * n], |p, c| p.gemm(&ab, &l, c, m, Epilogue::Bias));
assert_close(&got, &want);
}
}
}
#[test]
fn gemv_and_embed() {
for (n, cols) in [(100, 128), (288, 288)] {
let k = 96;
let bias = random(n, 2);
let a = random(k, 3);
let pos = random(2 * k, 6);
for (ty, bytes) in weights(n, k) {
let file = Gguf::with_tensors(&[("w", &[k, n], ty, &bytes)]);
let t = file.tensor("w", &[k, n]).unwrap();
let w = t.to_f32().unwrap();
for gpu in gpus() {
let l = gpu.linear(&[file.tensor("w", &[k, n]).unwrap()], &bias).unwrap();
let ab = up(gpu, &a);
let c = random(3 + cols + 1, 4);
for e in EPILOGUES {
let want: Vec<f32> = (0..c.len())
.map(|i| match i.checked_sub(3) {
Some(j) if j < n => {
epilogue(e, bias[j] + (0..k).map(|x| a[x] * w[j * k + x]).sum::<f32>(), c[i])
}
Some(j) if j < cols => epilogue(e, 0.0, c[i]),
_ => c[i],
})
.collect();
assert_close(&run(gpu, &c, |p, out| p.gemv(&ab, &l, out, e, 3)), &want);
}
let pb = up(gpu, &pos);
let ids = up(gpu, &[f32::from_bits(5), f32::from_bits(7)]);
let want: Vec<f32> = (0..k).map(|i| w[7 * k + i] + pos[k + i] + bias[i]).collect();
assert_close(
&run(gpu, &vec![0.0; k], |p, out| p.embed(&l, &pb, out, (&ids, 1), k)),
&want,
);
}
}
}
}
#[test]
fn attend_and_pack() {
let half = |x: f32| f16::from_f32(x).to_f32();
let keys = 4100;
for (heads, hd) in [(2, 40), (3, 36)] {
let d = heads * hd;
let kv = random(keys * 2 * d, 7);
let q = random(d, 8);
for gpu in gpus() {
let (qb, xb) = (up(gpu, &q), up(gpu, &kv));
let cache = gpu.buffer(keys * d);
let part = gpu.buffer(attend_len(keys, heads, hd));
for n in [1, 37, 256, 257, 1500, 2049, keys] {
let mut want = vec![0.0; d];
for h in 0..heads {
let s: Vec<f32> = (0..n)
.map(|j| {
let k = |c: usize| half(kv[j * 2 * d + h * hd + c]);
(0..hd).map(|c| half(q[h * hd + c]) * k(c)).sum::<f32>()
})
.map(|v| v / (hd as f32).sqrt())
.collect();
let mx = s.iter().fold(f32::MIN, |m, x| m.max(*x));
let e: Vec<f32> = s.iter().map(|x| (x - mx).exp()).collect();
let sum: f32 = e.iter().sum();
for c in 0..hd {
want[h * hd + c] =
(0..n).map(|j| e[j] * half(kv[j * 2 * d + d + h * hd + c])).sum::<f32>() / sum;
}
}
let got = run(gpu, &vec![0.0; d], |p, o| {
p.pack(&xb, [0, 2 * d], &cache, keys, d, [keys, 0]);
p.attend(&qb, &cache, &part, o, n, keys, heads, hd);
});
assert_close(&got, &want);
}
}
}
let x = random(3 + 13 * 2, 7);
let h = |x: f32| f16::from_f32(x).to_bits() as u32;
for gpu in gpus() {
let xb = up(gpu, &x);
for (rows, col) in [(1, 4), (2, 5)] {
let y = random(7 * 6, 9);
let got = run(gpu, &y, |p, out| p.pack(&xb, [3, 13], out, rows, 6, [7, col]));
let mut want: Vec<u32> = y.iter().map(|v| v.to_bits()).collect();
for j in 0..rows {
let pair = |i: usize| h(x[3 + 13 * j + 2 * i]) | h(x[4 + 13 * j + 2 * i]) << 16;
for i in 0..3 {
want[i * 7 + col + j] = pair(i);
want[3 * 7 + (col + j) * 3 + i] = pair(3 + i);
}
}
let got: Vec<u32> = got.iter().map(|v| v.to_bits()).collect();
assert_eq!(got, want, "pack {rows} rows");
}
}
}
#[test]
fn conv_glu() {
let (t, ch) = (37, 192);
let h = random(t * 2 * ch, 10);
let (g, b) = (random(ch, 13), random(ch, 14));
let glu = |s: usize, c: usize| h[s * 2 * ch + c] / (1.0 + (-h[s * 2 * ch + ch + c]).exp());
for (kernel, norm) in [(5, ConvNorm::LayerNorm), (9, ConvNorm::Affine)] {
let (w, wb) = (random(ch * kernel, 11), random(ch, 12));
let pad = kernel / 2;
let dw: Vec<f32> = (0..t * ch)
.map(|i| {
let (r, c) = (i / ch, i % ch);
let taps = (0..kernel).filter(|k| (r + k).wrapping_sub(pad) < t);
wb[c] + taps.map(|k| w[c * kernel + k] * glu(r + k - pad, c)).sum::<f32>()
})
.collect();
let want: Vec<f32> = match norm {
ConvNorm::LayerNorm => layer_norm(&dw, &g, &b),
ConvNorm::Affine => dw.iter().enumerate().map(|(i, x)| x * g[i % ch] + b[i % ch]).collect(),
};
let want: Vec<f32> = want.into_iter().map(silu).collect();
for gpu in gpus() {
let (n, dw, hb) =
(gpu.channels(&g, &b).unwrap(), gpu.channels(&w, &wb).unwrap(), up(gpu, &h));
let got =
run(gpu, &vec![0.0; t * ch], |p, y| p.conv_glu(&hb, &dw, kernel, &n, norm, y, t, ch));
assert_close(&got, &want);
}
}
}
#[test]
fn attention() {
for (t, hd) in [(37usize, 48), (100, 48), (100, 56), (5, 36), (70, 52), (100, 64), (40, 80)] {
let heads = 2;
let d = heads * hd;
let half = |x: f32| f16::from_f32(x).to_f32();
let qk: Vec<f32> = random(t.next_multiple_of(64) * 2 * d, 15).into_iter().map(half).collect();
let v: Vec<f32> = random(t.next_multiple_of(64) * d + 4, 16).into_iter().map(half).collect();
let scale = 1.0 / (hd as f32).sqrt();
let mut want = vec![0.0; t * d];
for h in 0..heads {
for i in 0..t {
let q = &qk[i * 2 * d + h * hd..][..hd];
let s: Vec<f32> = (0..t)
.map(|j| {
scale * q.iter().zip(&qk[j * 2 * d + d + h * hd..]).map(|(a, b)| a * b).sum::<f32>()
})
.collect();
let mx = s.iter().fold(f32::MIN, |m, x| m.max(*x));
let e: Vec<f32> = s.iter().map(|x| (x - mx).exp()).collect();
let sum: f32 = e.iter().sum();
for c in 0..hd {
want[i * d + h * hd + c] = (0..t).map(|j| e[j] / sum * v[j * d + h * hd + c]).sum();
}
}
}
for gpu in gpus() {
let (qb, vb) = (up(gpu, &qk), up(gpu, &v));
let s = gpu.buffer(gpu.scores_len(t, heads, hd, false));
let got = run(gpu, &vec![0.0; t * d], |p, o| {
p.attention(&qb, Some(&vb), None, &s, o, t, heads, hd)
});
let wide = gpu.fast.attention_size == 16 && gpu.flash(hd, false).is_some();
assert_near(&got, &want, if wide { 1e-3 } else { 2e-5 });
}
}
}
#[test]
fn relative_attention() {
for (t, heads, hd) in [(5usize, 2, 40), (37, 2, 40), (100, 3, 128)] {
let d = heads * hd;
let (qkv, pos) =
(random(t.next_multiple_of(64) * 4 * d, 17), random((pos_rows(t) + 64) * d, 18));
let scale = 1.0 / (hd as f32).sqrt();
let dot = |a: &[f32], b: &[f32]| a[..hd].iter().zip(b).map(|(x, y)| x * y).sum::<f32>();
let mut want = vec![0.0; t * d];
for h in 0..heads {
for i in 0..t {
let row = |j: usize, part: usize| &qkv[j * 4 * d + part * d + h * hd..];
let s: Vec<f32> = (0..t)
.map(|j| {
(dot(row(i, 0), row(j, 1)) + dot(row(i, 3), &pos[(j + t + 15 - i) * d + h * hd..]))
* scale
})
.collect();
let mx = s.iter().fold(f32::MIN, |m, x| m.max(*x));
let e: Vec<f32> = s.iter().map(|x| (x - mx).exp()).collect();
let sum: f32 = e.iter().sum();
for c in 0..hd {
want[i * d + h * hd + c] = (0..t).map(|j| e[j] / sum * row(j, 2)[c]).sum();
}
}
}
for gpu in gpus() {
let (qb, pb) = (up(gpu, &qkv), up(gpu, &pos));
let s = gpu.buffer(gpu.scores_len(t, heads, hd, true));
let got = run(gpu, &vec![0.0; t * d], |p, o| {
p.attention(&qb, None, Some(&pb), &s, o, t, heads, hd)
});
assert_close(&got, &want);
}
}
}
fn depthwise(x: &[f32], w: &[f32], b: &[f32], t: usize, f: usize, ch: usize) -> Vec<f32> {
let fo = half(f);
(0..half(t) * fo * ch)
.map(|e| {
let (r, j, c) = (e / (fo * ch), e / ch % fo, e % ch);
let taps = (0..9).filter_map(|k| {
let (ti, fi) = ((2 * r + k / 3).checked_sub(1)?, (2 * j + k % 3).checked_sub(1)?);
(ti < t && fi < f).then(|| w[k * ch + c] * x[(ti * f + fi) * ch + c])
});
b[c] + taps.sum::<f32>()
})
.collect()
}
#[test]
fn subsampling() {
let (t, f, ch) = (29, 11, 5);
let mel = random(t * f, 19);
let ws: Vec<_> = (0..6).map(|i| random(ch * 9, 20 + i)).collect();
let [w0, b0, w2, b2, w5, b5] =
std::array::from_fn(|i| &ws[i][..if i % 2 == 0 { ch * 9 } else { ch }]);
let wide: Vec<f32> = mel.iter().flat_map(|&v| std::iter::repeat_n(v, ch)).collect();
let first: Vec<f32> =
depthwise(&wide, w0, b0, t, f, ch).into_iter().map(|v| v.max(0.0)).collect();
let (t2, f2) = (half(half(t)), half(half(f)));
let second = depthwise(&first, w2, b2, half(t), half(f), ch);
let want = depthwise(&second, w5, b5, t2, f2, ch);
let x = random(t * f * ch, 26);
let flat: Vec<f32> =
(0..t * f * ch).map(|e| x[(e / (f * ch) * f + e % f) * ch + e % (f * ch) / f]).collect();
for gpu in gpus() {
let [c0, c2, c5] = [(w0, b0), (w2, b2), (w5, b5)].map(|(w, b)| gpu.channels(w, b).unwrap());
let (mb, xb) = (up(gpu, &mel), up(gpu, &x));
let chunk = gpu.buffer(5 * f2 * ch);
let got = run(gpu, &vec![0.0; want.len()], |p, y| {
for t0 in [0, 2] {
p.depthwise(&mb, Some(&c0), &c2, &chunk, [t, f, ch], t0, 5);
p.depthwise(&chunk, None, &c5, y, [t2, f2, ch], t0, 2);
}
});
assert_close(&got, &want);
assert_close(&run(gpu, &vec![0.0; flat.len()], |p, y| p.flatten(&xb, y, t, f, ch)), &flat);
}
}
#[test]
fn rope() {
let (rows, heads, hd, rot, pos) = (5, 3, 10, 6, 2);
let stride = 3 * heads * hd;
let x = random(rows * stride, 30);
let table = random((pos + rows) * rot, 31);
let want: Vec<f32> = (0..x.len())
.map(|e| {
let (r, i) = (e / stride, e % stride);
let d = i % hd;
if i >= 2 * heads * hd || d >= rot {
return x[e];
}
let t = &table[(pos + r) * rot..];
let (c, s) = (t[d / 2], t[rot / 2 + d / 2]);
if d % 2 == 0 { x[e] * c - x[e + 1] * s } else { x[e - 1] * s + x[e] * c }
})
.collect();
for gpu in gpus() {
let tb = up(gpu, &table);
let got = run(gpu, &x, |p, xb| p.rope(&tb, xb, rows, pos, [heads, hd, rot]));
assert_close(&got, &want);
}
}
#[test]
fn group_norm_swiglu_argmax() {
let (t, ch) = (37, 20);
let x = random(t * ch, 32);
let (w, b) = (random(ch, 33), random(ch, 34));
let n = x.len() as f32;
let mean = x.iter().sum::<f32>() / n;
let var = x.iter().map(|v| (v - mean) * (v - mean)).sum::<f32>() / n;
let rstd = 1.0 / (var + 1e-5).sqrt();
let norm: Vec<f32> =
x.iter().enumerate().map(|(i, v)| (v - mean) * rstd * w[i % ch] + b[i % ch]).collect();
let h = &x[..600];
let glu: Vec<f32> =
(0..600).map(|i| if i < 300 { h[i] * silu(h[300 + i]) } else { h[i] }).collect();
let mut logits = random(1000, 35);
logits[700] = 2.0;
logits[300] = 2.0;
for gpu in gpus() {
let c = gpu.channels(&w, &b).unwrap();
assert_close(&run(gpu, &x, |p, xb| p.group_norm(xb, &c, t, ch, 1e-5)), &norm);
assert_close(&run(gpu, h, |p, hb| p.swiglu(hb, 300)), &glu);
let lb = up(gpu, &logits);
let got = run(gpu, &[0.0], |p, out| p.argmax(&lb, out, 1000));
assert_eq!(got[0].to_bits(), 300);
let got = run(gpu, &[0.0], |p, out| p.argmax(&lb, out, 500));
assert_eq!(got[0].to_bits(), 300);
}
}
}