use crate::backend::{BackendDevice, BackendStorage};
use crate::op::{BinaryOpT, CmpOp, ReduceOp, UnaryOpT};
use crate::{CpuStorage, DType, Error, Layout, Result, Shape};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use wgpu::util::DeviceExt;
fn kernel_wgsl(name: &str) -> Result<&'static str> {
let s: &'static str = match name {
"matmul" => include_str!("wgpu/shaders/matmul.wgsl"),
"matmul_nt" => include_str!("wgpu/shaders/matmul_nt.wgsl"),
"mul_mat_vec_q8_0" => include_str!("wgpu/shaders/mul_mat_vec_q8_0.wgsl"),
"mul_mat_vec_q4_0" => include_str!("wgpu/shaders/mul_mat_vec_q4_0.wgsl"),
"mul_mat_vec_q4k" => include_str!("wgpu/shaders/mul_mat_vec_q4k.wgsl"),
"moe_matvec_q4_0" => include_str!("wgpu/shaders/moe_matvec_q4_0.wgsl"),
"moe_matvec_q8_0" => include_str!("wgpu/shaders/moe_matvec_q8_0.wgsl"),
"moe_matvec_q4k" => include_str!("wgpu/shaders/moe_matvec_q4k.wgsl"),
"flash_attn" => include_str!("wgpu/shaders/flash_attn.wgsl"),
"strided_copy" => include_str!("wgpu/shaders/strided_copy.wgsl"),
"copy" => include_str!("wgpu/shaders/copy.wgsl"),
"copy2d" => include_str!("wgpu/shaders/copy2d.wgsl"),
"const_fill" => include_str!("wgpu/shaders/const_fill.wgsl"),
"exp" => include_str!("wgpu/shaders/exp.wgsl"),
"log" => include_str!("wgpu/shaders/log.wgsl"),
"sqrt" => include_str!("wgpu/shaders/sqrt.wgsl"),
"neg" => include_str!("wgpu/shaders/neg.wgsl"),
"abs" => include_str!("wgpu/shaders/abs.wgsl"),
"tanh" => include_str!("wgpu/shaders/tanh.wgsl"),
"sigmoid" => include_str!("wgpu/shaders/sigmoid.wgsl"),
"relu" => include_str!("wgpu/shaders/relu.wgsl"),
"gelu" => include_str!("wgpu/shaders/gelu.wgsl"),
"gelu_erf" => include_str!("wgpu/shaders/gelu_erf.wgsl"),
"erf" => include_str!("wgpu/shaders/erf.wgsl"),
"recip" => include_str!("wgpu/shaders/recip.wgsl"),
"sign" => include_str!("wgpu/shaders/sign.wgsl"),
"cos" => include_str!("wgpu/shaders/cos.wgsl"),
"sin" => include_str!("wgpu/shaders/sin.wgsl"),
"sqr" => include_str!("wgpu/shaders/sqr.wgsl"),
"ceil" => include_str!("wgpu/shaders/ceil.wgsl"),
"floor" => include_str!("wgpu/shaders/floor.wgsl"),
"round" => include_str!("wgpu/shaders/round.wgsl"),
"silu" => include_str!("wgpu/shaders/silu.wgsl"),
"elu" => include_str!("wgpu/shaders/elu.wgsl"),
"affine" => include_str!("wgpu/shaders/affine.wgsl"),
"powf" => include_str!("wgpu/shaders/powf.wgsl"),
"add" => include_str!("wgpu/shaders/add.wgsl"),
"sub" => include_str!("wgpu/shaders/sub.wgsl"),
"mul" => include_str!("wgpu/shaders/mul.wgsl"),
"div" => include_str!("wgpu/shaders/div.wgsl"),
"maximum" => include_str!("wgpu/shaders/maximum.wgsl"),
"minimum" => include_str!("wgpu/shaders/minimum.wgsl"),
"silu_mul" => include_str!("wgpu/shaders/silu_mul.wgsl"),
"cmp" => include_str!("wgpu/shaders/cmp.wgsl"),
"where_cond" => include_str!("wgpu/shaders/where_cond.wgsl"),
"reduce_sum" => include_str!("wgpu/shaders/reduce_sum.wgsl"),
"reduce_max" => include_str!("wgpu/shaders/reduce_max.wgsl"),
"reduce_min" => include_str!("wgpu/shaders/reduce_min.wgsl"),
"reduce_argmin" => include_str!("wgpu/shaders/reduce_argmin.wgsl"),
"reduce_argmax" => include_str!("wgpu/shaders/reduce_argmax.wgsl"),
"cast_f2u" => include_str!("wgpu/shaders/cast_f2u.wgsl"),
"cast_u2f" => include_str!("wgpu/shaders/cast_u2f.wgsl"),
"gather" => include_str!("wgpu/shaders/gather.wgsl"),
"index_select" => include_str!("wgpu/shaders/index_select.wgsl"),
"scatter_set" => include_str!("wgpu/shaders/scatter_set.wgsl"),
"scatter_add_set" => include_str!("wgpu/shaders/scatter_add_set.wgsl"),
"softmax_rows" => include_str!("wgpu/shaders/softmax_rows.wgsl"),
"rms_norm" => include_str!("wgpu/shaders/rms_norm.wgsl"),
"rope" => include_str!("wgpu/shaders/rope.wgsl"),
_ => crate::bail!("wgpu: no WGSL kernel for `{name}`"),
};
Ok(s)
}
#[derive(thiserror::Error, Debug)]
pub enum WgpuError {
#[error("{0}")]
Message(String),
}
impl From<String> for WgpuError {
fn from(e: String) -> Self {
WgpuError::Message(e)
}
}
impl From<WgpuError> for Error {
fn from(e: WgpuError) -> Self {
Error::Msg(e.to_string())
}
}
fn wgpuerr<E: std::fmt::Display>(e: E) -> Error {
Error::Msg(format!("wgpu: {e}"))
}
struct WgpuInner {
#[allow(dead_code)] instance: wgpu::Instance,
#[allow(dead_code)] adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
gpu_id: usize,
adapter_desc: String,
seed: Mutex<u64>,
pipelines: Mutex<HashMap<&'static str, Arc<wgpu::ComputePipeline>>>,
}
#[derive(Clone)]
pub struct WgpuDevice {
inner: Arc<WgpuInner>,
}
impl std::fmt::Debug for WgpuDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "WgpuDevice({}, {})", self.inner.gpu_id, self.inner.adapter_desc)
}
}
pub struct WgpuStorage {
buffer: Arc<wgpu::Buffer>,
count: usize,
dtype: DType,
device: WgpuDevice,
}
impl std::fmt::Debug for WgpuStorage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "WgpuStorage(count={}, dtype={:?})", self.count, self.dtype)
}
}
impl WgpuStorage {
fn count(&self) -> usize {
self.count
}
}
fn pack_u32(v: &[u32]) -> Vec<u8> {
let mut b = Vec::with_capacity(v.len() * 4);
for x in v {
b.extend_from_slice(&x.to_ne_bytes());
}
b
}
const WG1D: u32 = 64;
impl WgpuDevice {
fn dev(&self) -> &wgpu::Device {
&self.inner.device
}
fn queue(&self) -> &wgpu::Queue {
&self.inner.queue
}
pub fn adapter_description(&self) -> &str {
&self.inner.adapter_desc
}
fn pipeline(&self, name: &'static str) -> Result<Arc<wgpu::ComputePipeline>> {
if let Some(p) = self.inner.pipelines.lock().unwrap().get(name) {
return Ok(p.clone());
}
let src = kernel_wgsl(name)?;
let dev = self.dev();
let module = dev.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some(name),
source: wgpu::ShaderSource::Wgsl(src.into()),
});
let pipeline = Arc::new(dev.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some(name),
layout: None,
module: &module,
entry_point: "main",
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
}));
self.inner
.pipelines
.lock()
.unwrap()
.insert(name, pipeline.clone());
Ok(pipeline)
}
fn dispatch(
&self,
name: &'static str,
bufs: &[&wgpu::Buffer],
params: &[u8],
groups: (u32, u32, u32),
) -> Result<()> {
let pipeline = self.pipeline(name)?;
let dev = self.dev();
let params_padded: &[u8] = if params.is_empty() { &[0u8; 4] } else { params };
let ubuf = dev.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("params"),
contents: params_padded,
usage: wgpu::BufferUsages::UNIFORM,
});
let bgl = pipeline.get_bind_group_layout(0);
let mut entries: Vec<wgpu::BindGroupEntry> = Vec::with_capacity(bufs.len() + 1);
entries.push(wgpu::BindGroupEntry {
binding: 0,
resource: ubuf.as_entire_binding(),
});
for (i, b) in bufs.iter().enumerate() {
entries.push(wgpu::BindGroupEntry {
binding: (i + 1) as u32,
resource: b.as_entire_binding(),
});
}
let bind_group = dev.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some(name),
layout: &bgl,
entries: &entries,
});
let mut enc = dev.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some(name) });
{
let mut cpass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some(name),
timestamp_writes: None,
});
cpass.set_pipeline(&pipeline);
cpass.set_bind_group(0, &bind_group, &[]);
cpass.dispatch_workgroups(groups.0, groups.1, groups.2);
}
self.queue().submit(std::iter::once(enc.finish()));
Ok(())
}
fn raw_buffer(&self, count: usize) -> Arc<wgpu::Buffer> {
let size = ((count.max(1)) * 4) as u64;
let buffer = self.dev().create_buffer(&wgpu::BufferDescriptor {
label: Some("storage"),
size,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Arc::new(buffer)
}
fn raw_buffer_init(&self, init: &[u8]) -> Arc<wgpu::Buffer> {
let nwords = init.len().div_ceil(4).max(1);
let mut words = vec![0u8; nwords * 4];
words[..init.len()].copy_from_slice(init);
let buffer = self.dev().create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("storage_init"),
contents: &words,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST,
});
Arc::new(buffer)
}
fn alloc_f32(&self, count: usize) -> Result<WgpuStorage> {
Ok(WgpuStorage {
buffer: self.raw_buffer(count),
count,
dtype: DType::F32,
device: self.clone(),
})
}
fn alloc_u32(&self, count: usize) -> Result<WgpuStorage> {
Ok(WgpuStorage {
buffer: self.raw_buffer(count),
count,
dtype: DType::U32,
device: self.clone(),
})
}
pub(crate) fn upload_f32(&self, data: &[f32]) -> Result<WgpuStorage> {
let bytes: &[u8] = bytemuck_cast_f32(data);
Ok(WgpuStorage {
buffer: self.raw_buffer_init(bytes),
count: data.len(),
dtype: DType::F32,
device: self.clone(),
})
}
fn upload_u32(&self, data: &[u32]) -> Result<WgpuStorage> {
let bytes: &[u8] = bytemuck_cast_u32(data);
Ok(WgpuStorage {
buffer: self.raw_buffer_init(bytes),
count: data.len(),
dtype: DType::U32,
device: self.clone(),
})
}
pub fn upload_qweight(&self, bytes: &[u8]) -> Result<WgpuStorage> {
let nwords = bytes.len().div_ceil(4).max(1);
Ok(WgpuStorage {
buffer: self.raw_buffer_init(bytes),
count: nwords, dtype: DType::U32,
device: self.clone(),
})
}
pub fn matvec_q4_0_gpu(
&self,
wq: &WgpuStorage,
x: &WgpuStorage,
nout: usize,
k: usize,
) -> Result<WgpuStorage> {
if !k.is_multiple_of(32) {
crate::bail!("matvec_q4_0_gpu: k must be a multiple of 32, got {k}");
}
if x.count < k {
crate::bail!("matvec_q4_0_gpu: x count {} < k {k}", x.count);
}
let out = self.alloc_f32(nout)?;
self.dispatch(
"mul_mat_vec_q4_0",
&[&wq.buffer, &x.buffer, &out.buffer],
&pack_u32(&[nout as u32, k as u32]),
((nout as u32).div_ceil(WG1D), 1, 1),
)?;
Ok(out)
}
pub fn matvec_q8_0_gpu(
&self,
wq: &WgpuStorage,
x: &WgpuStorage,
nout: usize,
k: usize,
) -> Result<WgpuStorage> {
if !k.is_multiple_of(32) {
crate::bail!("matvec_q8_0_gpu: k must be a multiple of 32, got {k}");
}
if x.count < k {
crate::bail!("matvec_q8_0_gpu: x count {} < k {k}", x.count);
}
let out = self.alloc_f32(nout)?;
self.dispatch(
"mul_mat_vec_q8_0",
&[&wq.buffer, &x.buffer, &out.buffer],
&pack_u32(&[nout as u32, k as u32]),
((nout as u32).div_ceil(WG1D), 1, 1),
)?;
Ok(out)
}
pub fn matvec_q4_0(&self, wq: &WgpuStorage, x: &[f32], nout: usize, k: usize) -> Result<Vec<f32>> {
if x.len() != k {
crate::bail!("matvec_q4_0: x len {} != k {k}", x.len());
}
let xs = self.upload_f32(x)?;
self.matvec_q4_0_gpu(wq, &xs, nout, k)?.to_vec_f32()
}
pub fn matvec_q8_0(&self, wq: &WgpuStorage, x: &[f32], nout: usize, k: usize) -> Result<Vec<f32>> {
if x.len() != k {
crate::bail!("matvec_q8_0: x len {} != k {k}", x.len());
}
let xs = self.upload_f32(x)?;
self.matvec_q8_0_gpu(wq, &xs, nout, k)?.to_vec_f32()
}
pub fn matvec_q4k_gpu(
&self,
wq: &WgpuStorage,
x: &WgpuStorage,
nout: usize,
k: usize,
) -> Result<WgpuStorage> {
if !k.is_multiple_of(256) {
crate::bail!("matvec_q4k_gpu: k must be a multiple of 256, got {k}");
}
if x.count < k {
crate::bail!("matvec_q4k_gpu: x count {} < k {k}", x.count);
}
let out = self.alloc_f32(nout)?;
self.dispatch(
"mul_mat_vec_q4k",
&[&wq.buffer, &x.buffer, &out.buffer],
&pack_u32(&[nout as u32, k as u32]),
((nout as u32).div_ceil(WG1D), 1, 1),
)?;
Ok(out)
}
pub fn matvec_q4k(&self, wq: &WgpuStorage, x: &[f32], nout: usize, k: usize) -> Result<Vec<f32>> {
if x.len() != k {
crate::bail!("matvec_q4k: x len {} != k {k}", x.len());
}
let xs = self.upload_f32(x)?;
self.matvec_q4k_gpu(wq, &xs, nout, k)?.to_vec_f32()
}
pub fn upload_ids(&self, ids: &[u32]) -> Result<WgpuStorage> {
self.upload_u32(ids)
}
#[allow(clippy::too_many_arguments)]
pub fn moe_matvec_gpu(
&self,
kernel: &'static str,
wbank: &WgpuStorage,
x: &WgpuStorage,
ids: &WgpuStorage,
nrows: usize,
n: usize,
k: usize,
) -> Result<WgpuStorage> {
if x.count < nrows * k {
crate::bail!("moe_matvec_gpu: x count {} < nrows*k {}", x.count, nrows * k);
}
if ids.count < nrows {
crate::bail!("moe_matvec_gpu: ids count {} < nrows {nrows}", ids.count);
}
let total = nrows * n;
let out = self.alloc_f32(total)?;
self.dispatch(
kernel,
&[&wbank.buffer, &x.buffer, &ids.buffer, &out.buffer],
&pack_u32(&[n as u32, k as u32, nrows as u32]),
((total as u32).div_ceil(WG1D), 1, 1),
)?;
Ok(out)
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn_gpu(
&self,
q: &WgpuStorage,
k: &WgpuStorage,
v: &WgpuStorage,
bh: usize,
lq: usize,
lk: usize,
d: usize,
scale: f32,
causal: bool,
) -> Result<WgpuStorage> {
if d > 256 {
crate::bail!("flash_attn_gpu: head_dim {d} > 256 (kernel limit)");
}
if q.count < bh * lq * d {
crate::bail!("flash_attn_gpu: q count {} < bh*lq*d {}", q.count, bh * lq * d);
}
if k.count < bh * lk * d || v.count < bh * lk * d {
crate::bail!("flash_attn_gpu: k/v count too small for bh*lk*d {}", bh * lk * d);
}
let out = self.alloc_f32(bh * lq * d)?;
let mut params = pack_u32(&[bh as u32, lq as u32, lk as u32, d as u32]);
params.extend_from_slice(&scale.to_ne_bytes());
params.extend_from_slice(&(causal as u32).to_ne_bytes());
let total = bh * lq;
self.dispatch(
"flash_attn",
&[&q.buffer, &k.buffer, &v.buffer, &out.buffer],
¶ms,
((total as u32).div_ceil(WG1D), 1, 1),
)?;
Ok(out)
}
#[allow(clippy::too_many_arguments)]
pub fn flash_attn(
&self,
q: &[f32],
k: &[f32],
v: &[f32],
bh: usize,
lq: usize,
lk: usize,
d: usize,
scale: f32,
causal: bool,
) -> Result<Vec<f32>> {
let qs = self.upload_f32(q)?;
let ks = self.upload_f32(k)?;
let vs = self.upload_f32(v)?;
self.flash_attn_gpu(&qs, &ks, &vs, bh, lq, lk, d, scale, causal)?
.to_vec_f32()
}
fn read_bytes(&self, src: &wgpu::Buffer, count: usize) -> Result<Vec<u8>> {
if count == 0 {
return Ok(Vec::new());
}
let bytes = (count * 4) as u64;
let dev = self.dev();
let staging = dev.create_buffer(&wgpu::BufferDescriptor {
label: Some("readback"),
size: bytes,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut enc =
dev.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("readback") });
enc.copy_buffer_to_buffer(src, 0, &staging, 0, bytes);
self.queue().submit(std::iter::once(enc.finish()));
let (tx, rx) = std::sync::mpsc::channel();
let slice = staging.slice(..);
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = self.dev().poll(wgpu::Maintain::Wait);
match rx.recv() {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(wgpuerr(e)),
Err(e) => return Err(wgpuerr(e)),
}
let data = slice.get_mapped_range();
let out = data.to_vec();
drop(data);
staging.unmap();
Ok(out)
}
}
impl WgpuStorage {
fn to_vec_f32(&self) -> Result<Vec<f32>> {
let bytes = self.device.read_bytes(&self.buffer, self.count)?;
Ok(bytes
.chunks_exact(4)
.map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
.collect())
}
fn to_vec_u32(&self) -> Result<Vec<u32>> {
let bytes = self.device.read_bytes(&self.buffer, self.count)?;
Ok(bytes
.chunks_exact(4)
.map(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
.collect())
}
fn groups_1d(n: usize) -> (u32, u32, u32) {
((n as u32).div_ceil(WG1D), 1, 1)
}
fn contiguous(&self, layout: &Layout) -> Result<WgpuStorage> {
if layout.is_contiguous() && layout.start_offset() == 0 {
let n = layout.shape().elem_count();
let out = self.device.alloc_f32(n)?;
self.device.dispatch(
"copy",
&[&self.buffer, &out.buffer],
&pack_u32(&[n as u32]),
Self::groups_1d(n),
)?;
return Ok(out);
}
let dims = layout.dims();
let rank = dims.len();
if rank > 6 {
crate::bail!("wgpu: contiguous supports rank <= 6, got {rank}");
}
let n = layout.shape().elem_count();
let out = self.device.alloc_f32(n)?;
let strides = layout.stride();
let mut p = vec![n as u32, rank as u32, layout.start_offset() as u32, 0u32];
let mut shape6 = [0u32; 6];
let mut stride6 = [0u32; 6];
for d in 0..rank {
shape6[d] = dims[d] as u32;
stride6[d] = strides[d] as u32;
}
p.extend_from_slice(&shape6);
p.extend_from_slice(&stride6);
self.device.dispatch(
"strided_copy",
&[&self.buffer, &out.buffer],
&pack_u32(&p),
Self::groups_1d(n),
)?;
Ok(out)
}
fn contiguous_u32(&self, layout: &Layout) -> Result<WgpuStorage> {
debug_assert_eq!(self.dtype, DType::U32);
if layout.is_contiguous() && layout.start_offset() == 0 {
return self.device.upload_u32(&self.to_vec_u32()?);
}
let src = self.to_vec_u32()?;
let gathered: Vec<u32> = layout.strided_index().map(|i| src[i]).collect();
self.device.upload_u32(&gathered)
}
fn contig_buf(&self, layout: &Layout, keep: &mut Option<WgpuStorage>) -> Result<Arc<wgpu::Buffer>> {
if layout.is_contiguous() && layout.start_offset() == 0 {
Ok(self.buffer.clone())
} else {
let s = self.contiguous(layout)?;
let b = s.buffer.clone();
*keep = Some(s);
Ok(b)
}
}
pub fn softmax_last_dim(&self, layout: &Layout) -> Result<WgpuStorage> {
let mut xk = None;
let xb = self.contig_buf(layout, &mut xk)?;
let dims = layout.dims();
let m = *dims.last().unwrap_or(&1);
let nrows = layout.shape().elem_count() / m.max(1);
let out = self.device.alloc_f32(nrows * m)?;
self.device.dispatch(
"softmax_rows",
&[&xb, &out.buffer],
&pack_u32(&[nrows as u32, m as u32]),
Self::groups_1d(nrows),
)?;
Ok(out)
}
pub fn rms_norm(
&self,
layout: &Layout,
alpha: &WgpuStorage,
alpha_l: &Layout,
eps: f32,
) -> Result<WgpuStorage> {
let mut xk = None;
let mut ak = None;
let xb = self.contig_buf(layout, &mut xk)?;
let ab = alpha.contig_buf(alpha_l, &mut ak)?;
let dims = layout.dims();
let m = *dims.last().unwrap_or(&1);
let nrows = layout.shape().elem_count() / m.max(1);
let out = self.device.alloc_f32(nrows * m)?;
let mut params = pack_u32(&[nrows as u32, m as u32]);
params.extend_from_slice(&eps.to_ne_bytes());
self.device.dispatch(
"rms_norm",
&[&xb, &ab, &out.buffer],
¶ms,
Self::groups_1d(nrows),
)?;
Ok(out)
}
pub fn silu_mul(
&self,
layout: &Layout,
rhs: &WgpuStorage,
rhs_l: &Layout,
) -> Result<WgpuStorage> {
let mut ak = None;
let mut bk = None;
let ab = self.contig_buf(layout, &mut ak)?;
let bb = rhs.contig_buf(rhs_l, &mut bk)?;
let n = layout.shape().elem_count();
let out = self.device.alloc_f32(n)?;
self.device.dispatch(
"silu_mul",
&[&ab, &bb, &out.buffer],
&pack_u32(&[n as u32]),
Self::groups_1d(n),
)?;
Ok(out)
}
pub fn rope(
&self,
layout: &Layout,
cos: &WgpuStorage,
cos_l: &Layout,
sin: &WgpuStorage,
sin_l: &Layout,
) -> Result<WgpuStorage> {
let mut srck = None;
let mut ck = None;
let mut sk = None;
let srcb = self.contig_buf(layout, &mut srck)?;
let cb = cos.contig_buf(cos_l, &mut ck)?;
let sb = sin.contig_buf(sin_l, &mut sk)?;
let (b, h, t, d) = layout.shape().dims4()?;
let unbatched = (cos_l.dims().len() == 3 && sin_l.dims().len() == 3) as u32;
let out = self.device.alloc_f32(b * h * t * d)?;
let pairs = b * h * t * (d / 2);
self.device.dispatch(
"rope",
&[&srcb, &cb, &sb, &out.buffer],
&pack_u32(&[b as u32, h as u32, t as u32, d as u32, unbatched]),
Self::groups_1d(pairs),
)?;
Ok(out)
}
#[allow(clippy::too_many_arguments)]
fn scatter_impl(
&self,
kernel: &'static str,
l: &Layout,
ids: &WgpuStorage,
ids_l: &Layout,
src: &WgpuStorage,
src_l: &Layout,
dim: usize,
) -> Result<()> {
if ids.dtype != DType::U32 {
crate::bail!("wgpu: scatter requires u32 ids, got {:?}", ids.dtype);
}
let idc = ids.contiguous_u32(ids_l)?;
let srcc = src.contiguous(src_l)?;
let src_dims = src_l.dims();
let dst_dims = l.dims();
let right: usize = src_dims[dim + 1..].iter().product();
let dim_src = src_dims[dim];
let dim_dst = dst_dims[dim];
let n = src_l.shape().elem_count();
self.device.dispatch(
kernel,
&[&self.buffer, &srcc.buffer, &idc.buffer],
&pack_u32(&[n as u32, right as u32, dim_src as u32, dim_dst as u32]),
Self::groups_1d(n),
)?;
Ok(())
}
}
fn bytemuck_cast_f32(data: &[f32]) -> &[u8] {
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
}
fn bytemuck_cast_u32(data: &[u32]) -> &[u8] {
unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data)) }
}
impl BackendDevice for WgpuDevice {
type Storage = WgpuStorage;
fn new(ordinal: usize) -> Result<Self> {
#[cfg(target_os = "macos")]
const WGPU_BACKENDS: wgpu::Backends = wgpu::Backends::METAL;
#[cfg(not(target_os = "macos"))]
const WGPU_BACKENDS: wgpu::Backends = wgpu::Backends::VULKAN;
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: WGPU_BACKENDS,
..Default::default()
});
let mut gpus: Vec<wgpu::Adapter> = Vec::new();
for ad in instance.enumerate_adapters(WGPU_BACKENDS) {
let info = ad.get_info();
let is_cpu = info.device_type == wgpu::DeviceType::Cpu
|| info.name.to_lowercase().contains("llvmpipe");
if !is_cpu {
gpus.push(ad);
}
}
if gpus.is_empty() {
return Err(Error::Msg("wgpu: no non-CPU GPU adapter".into()));
}
let adapter = if ordinal < gpus.len() {
gpus.swap_remove(ordinal)
} else {
return Err(Error::Msg(format!("wgpu: no adapter at ordinal {ordinal}")));
};
let info = adapter.get_info();
let adapter_desc = format!("{} [{:?}]", info.name, info.backend);
let limits = adapter.limits();
let (device, queue) = pollster::block_on(adapter.request_device(
&wgpu::DeviceDescriptor {
label: Some("hanzo-ml-wgpu"),
required_features: wgpu::Features::empty(),
required_limits: limits,
memory_hints: wgpu::MemoryHints::Performance,
},
None,
))
.map_err(wgpuerr)?;
let inner = WgpuInner {
instance,
adapter,
device,
queue,
gpu_id: ordinal,
adapter_desc,
seed: Mutex::new(299792458),
pipelines: Mutex::new(HashMap::new()),
};
Ok(Self {
inner: Arc::new(inner),
})
}
fn location(&self) -> crate::DeviceLocation {
crate::DeviceLocation::Wgpu {
gpu_id: self.inner.gpu_id,
}
}
fn same_device(&self, rhs: &Self) -> bool {
Arc::ptr_eq(&self.inner, &rhs.inner)
}
fn zeros_impl(&self, shape: &Shape, dtype: DType) -> Result<Self::Storage> {
let count = shape.elem_count();
match dtype {
DType::F32 | DType::F16 | DType::BF16 => self.upload_f32(&vec![0f32; count]),
DType::U32 | DType::U8 => self.upload_u32(&vec![0u32; count]),
_ => crate::bail!("wgpu: only f32/u32/f16/bf16/u8 supported, got {dtype:?}"),
}
}
unsafe fn alloc_uninit(&self, shape: &Shape, dtype: DType) -> Result<Self::Storage> {
match dtype {
DType::F32 | DType::F16 | DType::BF16 => self.alloc_f32(shape.elem_count()),
DType::U32 | DType::U8 => self.alloc_u32(shape.elem_count()),
_ => crate::bail!("wgpu: only f32/u32/f16/bf16/u8 supported, got {dtype:?}"),
}
}
fn storage_from_slice<T: crate::WithDType>(&self, s: &[T]) -> Result<Self::Storage> {
self.storage_from_cpu_storage(&T::to_cpu_storage(s))
}
fn storage_from_cpu_storage(&self, s: &CpuStorage) -> Result<Self::Storage> {
match s {
CpuStorage::F32(v) => self.upload_f32(v),
CpuStorage::U32(v) => self.upload_u32(v),
CpuStorage::F16(v) => self.upload_f32(&v.iter().map(|x| x.to_f32()).collect::<Vec<_>>()),
CpuStorage::BF16(v) => self.upload_f32(&v.iter().map(|x| x.to_f32()).collect::<Vec<_>>()),
CpuStorage::U8(v) => self.upload_u32(&v.iter().map(|&x| x as u32).collect::<Vec<_>>()),
_ => crate::bail!("wgpu: only f32/u32/f16/bf16/u8 supported, got {:?}", s.dtype()),
}
}
fn storage_from_cpu_storage_owned(&self, s: CpuStorage) -> Result<Self::Storage> {
self.storage_from_cpu_storage(&s)
}
fn rand_uniform(&self, shape: &Shape, dtype: DType, min: f64, max: f64) -> Result<Self::Storage> {
if dtype != DType::F32 {
crate::bail!("wgpu: rand_uniform only f32, got {dtype:?}");
}
use rand::Rng;
let mut rng = rand::rng();
let n = shape.elem_count();
let uniform = rand::distr::Uniform::new(min as f32, max as f32).map_err(Error::wrap)?;
let mut data = Vec::with_capacity(n);
for _ in 0..n {
data.push(rng.sample::<f32, _>(uniform));
}
self.upload_f32(&data)
}
fn rand_normal(&self, shape: &Shape, dtype: DType, mean: f64, std: f64) -> Result<Self::Storage> {
if dtype != DType::F32 {
crate::bail!("wgpu: rand_normal only f32, got {dtype:?}");
}
use rand_distr::Distribution;
let mut rng = rand::rng();
let n = shape.elem_count();
let normal = rand_distr::Normal::new(mean as f32, std as f32).map_err(Error::wrap)?;
let mut data = Vec::with_capacity(n);
for _ in 0..n {
data.push(normal.sample(&mut rng));
}
self.upload_f32(&data)
}
fn set_seed(&self, seed: u64) -> Result<()> {
*self.inner.seed.lock().unwrap() = seed;
Ok(())
}
fn get_current_seed(&self) -> Result<u64> {
Ok(*self.inner.seed.lock().unwrap())
}
fn synchronize(&self) -> Result<()> {
let _ = self.dev().poll(wgpu::Maintain::Wait);
Ok(())
}
}
macro_rules! todo_wgpu {
($what:literal) => {
crate::bail!(concat!("wgpu: TODO ", $what))
};
}
impl BackendStorage for WgpuStorage {
type Device = WgpuDevice;
fn try_clone(&self, _: &Layout) -> Result<Self> {
match self.dtype {
DType::U32 | DType::U8 => self.device.upload_u32(&self.to_vec_u32()?),
_ => self.device.upload_f32(&self.to_vec_f32()?),
}
}
fn dtype(&self) -> DType {
self.dtype
}
fn device(&self) -> &Self::Device {
&self.device
}
fn to_cpu_storage(&self) -> Result<CpuStorage> {
match self.dtype {
DType::U32 => Ok(CpuStorage::U32(self.to_vec_u32()?)),
_ => Ok(CpuStorage::F32(self.to_vec_f32()?)),
}
}
fn affine(&self, layout: &Layout, mul: f64, add: f64) -> Result<Self> {
let mut ck = None;
let cb = self.contig_buf(layout, &mut ck)?;
let n = layout.shape().elem_count();
let out = self.device.alloc_f32(n)?;
let mut params = (n as u32).to_ne_bytes().to_vec();
params.extend_from_slice(&(mul as f32).to_ne_bytes());
params.extend_from_slice(&(add as f32).to_ne_bytes());
self.device
.dispatch("affine", &[&cb, &out.buffer], ¶ms, Self::groups_1d(n))?;
Ok(out)
}
fn powf(&self, layout: &Layout, e: f64) -> Result<Self> {
let mut ck = None;
let cb = self.contig_buf(layout, &mut ck)?;
let n = layout.shape().elem_count();
let out = self.device.alloc_f32(n)?;
let mut params = (n as u32).to_ne_bytes().to_vec();
params.extend_from_slice(&(e as f32).to_ne_bytes());
self.device
.dispatch("powf", &[&cb, &out.buffer], ¶ms, Self::groups_1d(n))?;
Ok(out)
}
fn elu(&self, layout: &Layout, alpha: f64) -> Result<Self> {
let mut ck = None;
let cb = self.contig_buf(layout, &mut ck)?;
let n = layout.shape().elem_count();
let out = self.device.alloc_f32(n)?;
let mut params = (n as u32).to_ne_bytes().to_vec();
params.extend_from_slice(&(alpha as f32).to_ne_bytes());
self.device
.dispatch("elu", &[&cb, &out.buffer], ¶ms, Self::groups_1d(n))?;
Ok(out)
}
fn reduce_op(&self, op: ReduceOp, layout: &Layout, sum_dims: &[usize]) -> Result<Self> {
let (kernel, is_arg) = match op {
ReduceOp::Sum => ("reduce_sum", false),
ReduceOp::Max => ("reduce_max", false),
ReduceOp::Min => ("reduce_min", false),
ReduceOp::ArgMin => ("reduce_argmin", true),
ReduceOp::ArgMax => ("reduce_argmax", true),
};
let dims = layout.dims();
let rank = dims.len();
if rank == 0 {
crate::bail!("wgpu: reduce_op on scalar not supported");
}
if sum_dims != [rank - 1] {
crate::bail!(
"wgpu: reduce_op only supports the last dim (got dims={sum_dims:?}, rank={rank})"
);
}
let c = self.contiguous(layout)?;
let cols = dims[rank - 1];
let rows: usize = dims[..rank - 1].iter().product();
let out = if is_arg {
self.device.alloc_u32(rows)?
} else {
self.device.alloc_f32(rows)?
};
self.device.dispatch(
kernel,
&[&c.buffer, &out.buffer],
&pack_u32(&[rows as u32, cols as u32]),
Self::groups_1d(rows),
)?;
Ok(out)
}
fn cmp(&self, op: CmpOp, rhs: &Self, lhs_l: &Layout, rhs_l: &Layout) -> Result<Self> {
let lc = self.contiguous(lhs_l)?;
let rc = rhs.contiguous(rhs_l)?;
let n = lhs_l.shape().elem_count();
let out = self.device.alloc_u32(n)?;
let code: u32 = match op {
CmpOp::Eq => 0,
CmpOp::Ne => 1,
CmpOp::Le => 2,
CmpOp::Ge => 3,
CmpOp::Lt => 4,
CmpOp::Gt => 5,
};
self.device.dispatch(
"cmp",
&[&lc.buffer, &rc.buffer, &out.buffer],
&pack_u32(&[n as u32, code]),
Self::groups_1d(n),
)?;
Ok(out)
}
fn to_dtype(&self, layout: &Layout, dtype: DType) -> Result<Self> {
let n = layout.shape().elem_count();
match (self.dtype, dtype) {
(DType::F32, DType::F32) | (DType::U32, DType::U32) => self.contiguous(layout),
(DType::F32, DType::U32) => {
let c = self.contiguous(layout)?;
let out = self.device.alloc_u32(n)?;
self.device.dispatch(
"cast_f2u",
&[&c.buffer, &out.buffer],
&pack_u32(&[n as u32]),
Self::groups_1d(n),
)?;
Ok(out)
}
(DType::U32, DType::F32) => {
let c = self.contiguous(layout)?;
let out = self.device.alloc_f32(n)?;
self.device.dispatch(
"cast_u2f",
&[&c.buffer, &out.buffer],
&pack_u32(&[n as u32]),
Self::groups_1d(n),
)?;
Ok(out)
}
_ => {
let cpu = self.to_cpu_storage()?;
let converted = crate::backend::BackendStorage::to_dtype(&cpu, layout, dtype)?;
self.device.storage_from_cpu_storage(&converted)
}
}
}
fn unary_impl<B: UnaryOpT>(&self, layout: &Layout) -> Result<Self> {
let kernel: &'static str = match B::NAME {
"silu" => "silu",
"gelu" => "gelu",
"relu" => "relu",
"exp" => "exp",
"neg" => "neg",
"sqr" => "sqr",
"sqrt" => "sqrt",
"recip" => "recip",
"tanh" => "tanh",
"sin" => "sin",
"cos" => "cos",
"log" => "log",
"abs" => "abs",
"floor" => "floor",
"ceil" => "ceil",
"round" => "round",
"sign" => "sign",
"erf" => "erf",
"gelu_erf" => "gelu_erf",
"sigmoid" => "sigmoid",
_ => {
let cpu = self.to_cpu_storage()?;
let r = crate::backend::BackendStorage::unary_impl::<B>(&cpu, layout)?;
return self.device.storage_from_cpu_storage(&r);
}
};
let mut ck = None;
let cb = self.contig_buf(layout, &mut ck)?;
let n = layout.shape().elem_count();
let out = self.device.alloc_f32(n)?;
self.device
.dispatch(kernel, &[&cb, &out.buffer], &pack_u32(&[n as u32]), Self::groups_1d(n))?;
Ok(out)
}
fn binary_impl<B: BinaryOpT>(&self, rhs: &Self, lhs_l: &Layout, rhs_l: &Layout) -> Result<Self> {
let kernel: &'static str = match B::NAME {
"add" => "add",
"sub" => "sub",
"mul" => "mul",
"div" => "div",
"maximum" => "maximum",
"minimum" => "minimum",
_ => {
let lc = self.to_cpu_storage()?;
let rc = rhs.to_cpu_storage()?;
let r = crate::backend::BackendStorage::binary_impl::<B>(&lc, &rc, lhs_l, rhs_l)?;
return self.device.storage_from_cpu_storage(&r);
}
};
let mut lk = None;
let mut rk = None;
let lb = self.contig_buf(lhs_l, &mut lk)?;
let rb = rhs.contig_buf(rhs_l, &mut rk)?;
let n = lhs_l.shape().elem_count();
let out = self.device.alloc_f32(n)?;
self.device
.dispatch(kernel, &[&lb, &rb, &out.buffer], &pack_u32(&[n as u32]), Self::groups_1d(n))?;
Ok(out)
}
fn where_cond(
&self,
l: &Layout,
t: &Self,
t_l: &Layout,
f: &Self,
f_l: &Layout,
) -> Result<Self> {
if self.dtype != DType::U32 {
crate::bail!("wgpu: where_cond requires u32 condition, got {:?}", self.dtype);
}
let mut condk = None;
let condb = if l.is_contiguous() && l.start_offset() == 0 {
self.buffer.clone()
} else {
let s = self.contiguous_u32(l)?;
let b = s.buffer.clone();
condk = Some(s);
b
};
let _ = &condk; let mut tk = None;
let mut fk = None;
let tb = t.contig_buf(t_l, &mut tk)?;
let fb = f.contig_buf(f_l, &mut fk)?;
let n = l.shape().elem_count();
let out = self.device.alloc_f32(n)?;
self.device.dispatch(
"where_cond",
&[&condb, &tb, &fb, &out.buffer],
&pack_u32(&[n as u32]),
Self::groups_1d(n),
)?;
Ok(out)
}
fn conv1d(
&self,
_: &Layout,
_: &Self,
_: &Layout,
_: &crate::conv::ParamsConv1D,
) -> Result<Self> {
todo_wgpu!("conv1d")
}
fn conv_transpose1d(
&self,
_: &Layout,
_: &Self,
_: &Layout,
_: &crate::conv::ParamsConvTranspose1D,
) -> Result<Self> {
todo_wgpu!("conv_transpose1d")
}
fn conv2d(
&self,
_: &Layout,
_: &Self,
_: &Layout,
_: &crate::conv::ParamsConv2D,
) -> Result<Self> {
todo_wgpu!("conv2d")
}
fn conv_transpose2d(
&self,
_: &Layout,
_: &Self,
_: &Layout,
_: &crate::conv::ParamsConvTranspose2D,
) -> Result<Self> {
todo_wgpu!("conv_transpose2d")
}
fn index_select(&self, ids: &Self, l: &Layout, ids_l: &Layout, dim: usize) -> Result<Self> {
if ids.dtype != DType::U32 {
crate::bail!("wgpu: index_select requires u32 ids, got {:?}", ids.dtype);
}
let mut idk = None;
let ids_buf = if ids_l.is_contiguous() && ids_l.start_offset() == 0 {
ids.buffer.clone()
} else {
let s = ids.contiguous_u32(ids_l)?;
let b = s.buffer.clone();
idk = Some(s);
b
};
let _ = &idk; let src_c = self.contiguous(l)?;
let dims = l.dims();
let left: usize = dims[..dim].iter().product();
let dim_size = dims[dim];
let right: usize = dims[dim + 1..].iter().product();
let n_ids = ids_l.shape().elem_count();
let total = left * n_ids * right;
let out = self.device.alloc_f32(total)?;
self.device.dispatch(
"index_select",
&[&ids_buf, &src_c.buffer, &out.buffer],
&pack_u32(&[left as u32, dim_size as u32, right as u32, n_ids as u32]),
Self::groups_1d(total),
)?;
Ok(out)
}
fn gather(&self, l: &Layout, ids: &Self, ids_l: &Layout, dim: usize) -> Result<Self> {
if ids.dtype != DType::U32 {
crate::bail!("wgpu: gather requires u32 ids, got {:?}", ids.dtype);
}
let src = self.contiguous(l)?;
let idc = ids.contiguous_u32(ids_l)?;
let out_dims = ids_l.dims();
let src_dims = l.dims();
let right: usize = out_dims[dim + 1..].iter().product();
let dim_out = out_dims[dim];
let dim_src = src_dims[dim];
let n = ids_l.shape().elem_count();
let out = self.device.alloc_f32(n)?;
self.device.dispatch(
"gather",
&[&src.buffer, &idc.buffer, &out.buffer],
&pack_u32(&[n as u32, right as u32, dim_out as u32, dim_src as u32]),
Self::groups_1d(n),
)?;
Ok(out)
}
fn scatter_set(
&mut self,
l: &Layout,
ids: &Self,
ids_l: &Layout,
src: &Self,
src_l: &Layout,
dim: usize,
) -> Result<()> {
self.scatter_impl("scatter_set", l, ids, ids_l, src, src_l, dim)
}
fn scatter_add_set(
&mut self,
l: &Layout,
ids: &Self,
ids_l: &Layout,
src: &Self,
src_l: &Layout,
dim: usize,
) -> Result<()> {
self.scatter_impl("scatter_add_set", l, ids, ids_l, src, src_l, dim)
}
fn index_add(
&self,
_: &Layout,
_: &Self,
_: &Layout,
_: &Self,
_: &Layout,
_: usize,
) -> Result<Self> {
todo_wgpu!("index_add")
}
fn matmul(
&self,
rhs: &Self,
(b, m, n, k): (usize, usize, usize, usize),
lhs_l: &Layout,
rhs_l: &Layout,
) -> Result<Self> {
let mut lkeep = None;
let lc_buf = if lhs_l.is_contiguous() && lhs_l.start_offset() == 0 {
&self.buffer
} else {
let s = self.contiguous(lhs_l)?;
lkeep = Some(s);
&lkeep.as_ref().unwrap().buffer
};
let d = rhs_l.dims();
let st = rhs_l.stride();
let nt = rhs_l.start_offset() == 0
&& ((d.len() == 2 && d[0] == k && d[1] == n && st[0] == 1 && st[1] == k)
|| (d.len() == 3
&& d[0] == b
&& d[1] == k
&& d[2] == n
&& st[0] == n * k
&& st[1] == 1
&& st[2] == k));
let mut rkeep = None;
let rc_buf = if nt || (rhs_l.is_contiguous() && rhs_l.start_offset() == 0) {
&rhs.buffer
} else {
let s = rhs.contiguous(rhs_l)?;
rkeep = Some(s);
&rkeep.as_ref().unwrap().buffer
};
let _ = (&lkeep, &rkeep); let out = self.device.alloc_f32(b * m * n)?;
let params = pack_u32(&[b as u32, m as u32, k as u32, n as u32]);
let kernel = if nt { "matmul_nt" } else { "matmul" };
let groups = ((n as u32).div_ceil(16), (m as u32).div_ceil(16), b as u32);
self.device
.dispatch(kernel, &[lc_buf, rc_buf, &out.buffer], ¶ms, groups)?;
Ok(out)
}
fn copy_strided_src(&self, dst: &mut Self, dst_offset: usize, src_l: &Layout) -> Result<()> {
let n = src_l.shape().elem_count();
if n == 0 {
return Ok(());
}
if dst.count() < dst_offset + n {
crate::bail!(
"wgpu: copy_strided_src dst too small ({} < {})",
dst.count(),
dst_offset + n
);
}
let dims = src_l.dims();
let rank = dims.len();
if rank > 6 {
crate::bail!("wgpu: copy_strided_src supports rank <= 6, got {rank}");
}
let strides = src_l.stride();
let mut p = vec![n as u32, rank as u32, src_l.start_offset() as u32, dst_offset as u32];
let mut shape6 = [0u32; 6];
let mut stride6 = [0u32; 6];
for d in 0..rank {
shape6[d] = dims[d] as u32;
stride6[d] = strides[d] as u32;
}
p.extend_from_slice(&shape6);
p.extend_from_slice(&stride6);
self.device.dispatch(
"strided_copy",
&[&self.buffer, &dst.buffer],
&pack_u32(&p),
Self::groups_1d(n),
)
}
fn copy2d(
&self,
dst: &mut Self,
d1: usize,
d2: usize,
src_stride1: usize,
dst_stride1: usize,
src_offset: usize,
dst_offset: usize,
) -> Result<()> {
let total = d1 * d2;
if total == 0 {
return Ok(());
}
let p = pack_u32(&[
d1 as u32,
d2 as u32,
src_stride1 as u32,
dst_stride1 as u32,
src_offset as u32,
dst_offset as u32,
]);
self.device
.dispatch("copy2d", &[&self.buffer, &dst.buffer], &p, Self::groups_1d(total))
}
fn const_set(&mut self, s: crate::scalar::Scalar, layout: &Layout) -> Result<()> {
let n = layout.shape().elem_count();
if layout.is_contiguous() && layout.start_offset() == 0 && n == self.count {
let bits: u32 = if self.dtype == DType::U32 || self.dtype == DType::U8 {
s.to_f64() as u32
} else {
(s.to_f64() as f32).to_bits()
};
return self.device.dispatch(
"const_fill",
&[&self.buffer],
&pack_u32(&[n as u32, bits]),
Self::groups_1d(n),
);
}
if self.dtype == DType::U32 || self.dtype == DType::U8 {
let v = s.to_f64() as u32;
let mut data = self.to_vec_u32()?;
for i in layout.strided_index() {
if i >= data.len() {
crate::bail!("wgpu: const_set out of range");
}
data[i] = v;
}
*self = self.device.upload_u32(&data)?;
} else {
let v = s.to_f64() as f32;
let mut data = self.to_vec_f32()?;
for i in layout.strided_index() {
if i >= data.len() {
crate::bail!("wgpu: const_set out of range");
}
data[i] = v;
}
*self = self.device.upload_f32(&data)?;
}
Ok(())
}
fn avg_pool2d(&self, _: &Layout, _: (usize, usize), _: (usize, usize)) -> Result<Self> {
todo_wgpu!("avg_pool2d")
}
fn max_pool2d(&self, _: &Layout, _: (usize, usize), _: (usize, usize)) -> Result<Self> {
todo_wgpu!("max_pool2d")
}
fn upsample_nearest1d(&self, _: &Layout, _: usize) -> Result<Self> {
todo_wgpu!("upsample_nearest1d")
}
fn upsample_nearest2d(&self, _: &Layout, _: usize, _: usize) -> Result<Self> {
todo_wgpu!("upsample_nearest2d")
}
fn upsample_bilinear2d(
&self,
_: &Layout,
_: usize,
_: usize,
_: bool,
_: Option<f64>,
_: Option<f64>,
) -> Result<Self> {
todo_wgpu!("upsample_bilinear2d")
}
}