#![cfg(feature = "vulkan")]
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLock};
use fuel_dispatch::{
kernel::{KernelBindingTable, MatmulM, OpParams},
vulkan_dispatch::register_vulkan_kernels,
};
use fuel_ir::{DType, Layout, Shape, dispatch::OpKind, probe::BackendId};
use fuel_memory::{BackendStorage, Storage, alloc_cpu_zeroed};
use fuel_vulkan_backend::VulkanBackend;
struct VkTest {
_serial: MutexGuard<'static, ()>,
backend: Arc<VulkanBackend>,
}
impl std::ops::Deref for VkTest {
type Target = Arc<VulkanBackend>;
fn deref(&self) -> &Self::Target {
&self.backend
}
}
fn backend_or_skip() -> Option<VkTest> {
static GPU_SERIAL: OnceLock<Mutex<()>> = OnceLock::new();
static SHARED: OnceLock<Option<Arc<VulkanBackend>>> = OnceLock::new();
let serial = GPU_SERIAL
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let backend = SHARED
.get_or_init(|| VulkanBackend::new().ok().map(Arc::new))
.clone()?;
Some(VkTest {
_serial: serial,
backend,
})
}
fn upload_f32(backend: &Arc<VulkanBackend>, host: &[f32]) -> Storage {
let bytes: &[u8] = bytemuck::cast_slice(host);
let vk_bytes = backend
.upload_bytes_handle(bytes)
.expect("vulkan upload_bytes_handle");
Storage::new(BackendStorage::Vulkan(vk_bytes), DType::F32)
}
fn download_f32(backend: &Arc<VulkanBackend>, s: &Storage) -> Vec<f32> {
let bytes = match &s.inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
bytemuck::cast_slice::<u8, f32>(&bytes).to_vec()
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_add_f32_direct_wrapper() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a_data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let b_data: Vec<f32> = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
let n = a_data.len();
let a_storage = upload_f32(&backend, &a_data);
let b_storage = upload_f32(&backend, &b_data);
let out_bytes = backend
.alloc_bytes_handle(n * 4)
.expect("alloc_bytes_handle");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let alts = table.lookup_alternatives(
OpKind::AddElementwise,
&[DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
);
assert!(!alts.is_empty(), "no Vulkan AddElementwise registration");
let kernel = alts[0].kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout.clone(), layout];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("kernel dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expected: Vec<f32> = a_data
.iter()
.zip(b_data.iter())
.map(|(x, y)| x + y)
.collect();
assert_eq!(got, expected, "Vulkan Add f32 result mismatch");
}
#[test]
fn vulkan_dispatch_binary_f32_registered() {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let key = [DType::F32, DType::F32, DType::F32];
for op in [
OpKind::AddElementwise,
OpKind::SubElementwise,
OpKind::MulElementwise,
OpKind::DivElementwise,
OpKind::MaximumElementwise,
OpKind::MinimumElementwise,
] {
let alts = table.lookup_alternatives(op, &key, BackendId::Vulkan);
assert_eq!(
alts.len(),
1,
"expected 1 Vulkan alternative for {op:?} f32 after register_vulkan_kernels, got {}",
alts.len(),
);
}
}
fn run_binary_f32(
backend: &Arc<VulkanBackend>,
op: OpKind,
a_data: &[f32],
b_data: &[f32],
) -> Vec<f32> {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let n = a_data.len();
let a_storage = upload_f32(backend, a_data);
let b_storage = upload_f32(backend, b_data);
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let alts =
table.lookup_alternatives(op, &[DType::F32, DType::F32, DType::F32], BackendId::Vulkan);
let kernel = alts[0].kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout.clone(), layout];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("kernel dispatch");
download_f32(backend, &out_arc.read().unwrap())
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_sub_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_binary_f32(
&backend,
OpKind::SubElementwise,
&[10.0, 20.0, 30.0, 40.0],
&[1.0, 2.0, 3.0, 4.0],
);
assert_eq!(got, vec![9.0, 18.0, 27.0, 36.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_mul_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_binary_f32(
&backend,
OpKind::MulElementwise,
&[2.0, 3.0, 4.0, 5.0],
&[10.0, 10.0, 10.0, 10.0],
);
assert_eq!(got, vec![20.0, 30.0, 40.0, 50.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_div_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_binary_f32(
&backend,
OpKind::DivElementwise,
&[100.0, 80.0, 60.0, 40.0],
&[2.0, 4.0, 5.0, 8.0],
);
assert_eq!(got, vec![50.0, 20.0, 12.0, 5.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_maximum_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_binary_f32(
&backend,
OpKind::MaximumElementwise,
&[1.0, 5.0, 3.0, 7.0],
&[2.0, 4.0, 6.0, 1.0],
);
assert_eq!(got, vec![2.0, 5.0, 6.0, 7.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_minimum_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_binary_f32(
&backend,
OpKind::MinimumElementwise,
&[1.0, 5.0, 3.0, 7.0],
&[2.0, 4.0, 6.0, 1.0],
);
assert_eq!(got, vec![1.0, 4.0, 3.0, 1.0]);
}
#[test]
#[ignore]
fn vulkan_minmax_nan_propagates_a_first() {
let Some(backend) = backend_or_skip() else {
return;
};
let a_nan = f32::from_bits(0x7fc0_1234); let b_nan = f32::from_bits(0x7fc0_5678);
let one = 1.0_f32;
for op in [OpKind::MaximumElementwise, OpKind::MinimumElementwise] {
let r = run_binary_f32(&backend, op, &[a_nan, one], &[one, a_nan]);
assert_eq!(
r[0].to_bits(),
a_nan.to_bits(),
"{op:?}(NaN,1) must propagate a's NaN, got {:08x}",
r[0].to_bits()
);
assert!(
r[1].is_nan(),
"{op:?}(1,NaN) must propagate NaN, not drop to 1 (got {:08x})",
r[1].to_bits()
);
let both = run_binary_f32(&backend, op, &[a_nan], &[b_nan]);
assert_eq!(
both[0].to_bits(),
a_nan.to_bits(),
"{op:?}(NaNa,NaNb) must keep a's payload (a-first)"
);
}
let z = run_binary_f32(
&backend,
OpKind::MaximumElementwise,
&[0.0_f32, -0.0],
&[-0.0, 0.0],
);
let s = |v: f32| if v.is_sign_negative() { '-' } else { '+' };
eprintln!(
"VULKAN ±0 NOTE (device-dependent, NOT asserted): max(+0,-0)={} max(-0,+0)={}",
s(z[0]),
s(z[1])
);
}
#[test]
fn vulkan_dispatch_unary_f32_registered() {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let key = [DType::F32, DType::F32];
for op in [
OpKind::NegElementwise,
OpKind::SqrElementwise,
OpKind::SqrtElementwise,
OpKind::ExpElementwise,
OpKind::LogElementwise,
OpKind::SinElementwise,
OpKind::CosElementwise,
OpKind::TanhElementwise,
OpKind::SigmoidElementwise,
OpKind::SiluElementwise,
OpKind::GeluTanhElementwise,
OpKind::ReluElementwise,
OpKind::StepElementwise,
] {
let alts = table.lookup_alternatives(op, &key, BackendId::Vulkan);
assert_eq!(
alts.len(),
1,
"expected 1 Vulkan alternative for {op:?} f32, got {}",
alts.len(),
);
}
}
fn run_unary_f32(backend: &Arc<VulkanBackend>, op: OpKind, a_data: &[f32]) -> Vec<f32> {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let n = a_data.len();
let a_storage = upload_f32(backend, a_data);
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let alts = table.lookup_alternatives(op, &[DType::F32, DType::F32], BackendId::Vulkan);
let kernel = alts[0].kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&a_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("kernel dispatch");
download_f32(backend, &out_arc.read().unwrap())
}
fn assert_close(got: &[f32], expected: &[f32], rel_tol: f32, abs_tol: f32) {
assert_eq!(got.len(), expected.len(), "length mismatch");
for (i, (a, b)) in got.iter().zip(expected.iter()).enumerate() {
let diff = (a - b).abs();
let rel = diff / a.abs().max(b.abs()).max(1e-12);
assert!(
diff < abs_tol || rel < rel_tol,
"[{i}] got={a} want={b} diff={diff} rel={rel}",
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_neg_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::NegElementwise, &[1.0, -2.0, 3.0, -4.0]);
assert_eq!(got, vec![-1.0, 2.0, -3.0, 4.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sqr_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::SqrElementwise, &[1.0, 2.0, -3.0, 4.0]);
assert_eq!(got, vec![1.0, 4.0, 9.0, 16.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sqrt_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::SqrtElementwise, &[1.0, 4.0, 9.0, 16.0]);
assert_eq!(got, vec![1.0, 2.0, 3.0, 4.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_exp_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::ExpElementwise, &[0.0, 1.0, 2.0]);
let want = [
1.0,
std::f32::consts::E,
std::f32::consts::E * std::f32::consts::E,
];
assert_close(&got, &want, 1e-5, 1e-5);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_log_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(
&backend,
OpKind::LogElementwise,
&[
1.0,
std::f32::consts::E,
std::f32::consts::E * std::f32::consts::E,
],
);
assert_close(&got, &[0.0, 1.0, 2.0], 1e-5, 1e-5);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sin_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(
&backend,
OpKind::SinElementwise,
&[0.0, std::f32::consts::PI / 2.0, std::f32::consts::PI],
);
assert_close(&got, &[0.0, 1.0, 0.0], 1e-5, 1e-5);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_cos_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(
&backend,
OpKind::CosElementwise,
&[0.0, std::f32::consts::PI / 2.0, std::f32::consts::PI],
);
assert_close(&got, &[1.0, 0.0, -1.0], 1e-5, 1e-5);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_tanh_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::TanhElementwise, &[0.0, 1.0, -1.0]);
assert_close(&got, &[0.0, 0.7615942, -0.7615942], 1e-5, 1e-5);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sigmoid_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::SigmoidElementwise, &[0.0, 1.0, -1.0]);
assert_close(&got, &[0.5, 0.7310586, 0.26894143], 1e-5, 1e-5);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_silu_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::SiluElementwise, &[0.0, 1.0, -1.0]);
assert_close(&got, &[0.0, 0.7310586, -0.26894143], 1e-5, 1e-5);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_gelu_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::GeluTanhElementwise, &[0.0, 1.0, -1.0]);
assert_close(&got, &[0.0, 0.8413, -0.1587], 1e-3, 1e-3);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_relu_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(
&backend,
OpKind::ReluElementwise,
&[-2.0, -1.0, 0.0, 1.0, 2.0],
);
assert_eq!(got, vec![0.0, 0.0, 0.0, 1.0, 2.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_step_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(
&backend,
OpKind::StepElementwise,
&[-2.0, -0.5, 0.0, 0.5, 2.0],
);
assert_eq!(got[0], 0.0);
assert_eq!(got[1], 0.0);
assert_eq!(got[3], 1.0);
assert_eq!(got[4], 1.0);
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_add_f32_rank2_contig() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a_data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let b_data: Vec<f32> = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
let a_storage = upload_f32(&backend, &a_data);
let b_storage = upload_f32(&backend, &b_data);
let out_bytes = backend.alloc_bytes_handle(6 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::AddElementwise,
&[DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[2, 3]));
let layouts = vec![layout.clone(), layout.clone(), layout];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("kernel dispatch (rank-2 contig)");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![11.0, 22.0, 33.0, 44.0, 55.0, 66.0]);
}
#[test]
fn vulkan_dispatch_softmax_norm_registered() {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let unary = [DType::F32, DType::F32];
for op in [OpKind::SoftmaxLastDim, OpKind::RmsNormLastDim] {
let alts = table.lookup_alternatives(op, &unary, BackendId::Vulkan);
assert_eq!(
alts.len(),
1,
"expected 1 Vulkan alternative for {op:?} f32 after register_vulkan_kernels, got {}",
alts.len(),
);
}
let rope_key = [DType::F32, DType::F32, DType::F32, DType::F32];
let alts = table.lookup_alternatives(OpKind::Rope, &rope_key, BackendId::Vulkan);
assert_eq!(
alts.len(),
1,
"expected 1 Vulkan alternative for Rope [x,cos,sin,out]=f32 after register_vulkan_kernels, got {}",
alts.len(),
);
}
#[test]
#[ignore]
fn vulkan_dispatch_softmax_last_dim_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let host: Vec<f32> = vec![
1.0, 2.0, 3.0, 4.0, -1.0, 0.0, 1.0, 2.0,
];
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::SoftmaxLastDim,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::SoftmaxLastDim {
outer_count: outer,
last_dim: last,
},
)
.expect("softmax dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for row in 0..outer {
let s: f32 = got[row * last..(row + 1) * last].iter().sum();
assert!((s - 1.0).abs() < 1e-5, "softmax row {row} sum {s} != 1.0");
}
for c in 0..last {
let a = got[c];
let b = got[last + c];
assert!(
(a - b).abs() < 1e-5,
"softmax shift-invariance broken at col {c}: row0={a} row1={b}"
);
}
}
fn upload_f16(backend: &Arc<VulkanBackend>, host: &[half::f16]) -> Storage {
let bytes: &[u8] = bytemuck::cast_slice(host);
let vk_bytes = backend
.upload_bytes_handle(bytes)
.expect("vulkan upload_bytes_handle f16");
Storage::new(BackendStorage::Vulkan(vk_bytes), DType::F16)
}
fn download_f16(backend: &Arc<VulkanBackend>, s: &Storage) -> Vec<half::f16> {
let bytes = match &s.inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
bytemuck::cast_slice::<u8, half::f16>(&bytes).to_vec()
}
fn run_unary_f16(backend: &Arc<VulkanBackend>, op: OpKind, data: &[half::f16]) -> Vec<half::f16> {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let n = data.len();
let in_storage = upload_f16(backend, data);
let out_bytes = backend.alloc_bytes_handle(n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(op, &[DType::F16, DType::F16], BackendId::Vulkan)[0].kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("kernel dispatch");
download_f16(backend, &out_arc.read().unwrap())
}
fn run_binary_f16(
backend: &Arc<VulkanBackend>,
op: OpKind,
a_data: &[half::f16],
b_data: &[half::f16],
) -> Vec<half::f16> {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let n = a_data.len();
let a_storage = upload_f16(backend, a_data);
let b_storage = upload_f16(backend, b_data);
let out_bytes = backend.alloc_bytes_handle(n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(op, &[DType::F16, DType::F16, DType::F16], BackendId::Vulkan)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout.clone(), layout];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("kernel dispatch");
download_f16(backend, &out_arc.read().unwrap())
}
#[test]
fn vulkan_dispatch_binary_f16_registered() {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let key = [DType::F16, DType::F16, DType::F16];
for op in [
OpKind::AddElementwise,
OpKind::SubElementwise,
OpKind::MulElementwise,
OpKind::DivElementwise,
OpKind::MaximumElementwise,
OpKind::MinimumElementwise,
] {
let alts = table.lookup_alternatives(op, &key, BackendId::Vulkan);
assert_eq!(
alts.len(),
1,
"expected 1 Vulkan alt for {op:?} f16, got {}",
alts.len()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_add_f16() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let a: Vec<f16> = vec![1.0, 2.0, 3.0, 4.0]
.into_iter()
.map(f16::from_f32)
.collect();
let b: Vec<f16> = vec![10.0, 20.0, 30.0, 40.0]
.into_iter()
.map(f16::from_f32)
.collect();
let got = run_binary_f16(&backend, OpKind::AddElementwise, &a, &b);
let expected: Vec<f32> = vec![11.0, 22.0, 33.0, 44.0];
for (g, e) in got.iter().zip(expected.iter()) {
assert!(
(g.to_f32() - e).abs() < 0.01,
"add f16: got={}, expected={e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_mul_f16() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let a: Vec<f16> = vec![1.5, -2.0, 0.5]
.into_iter()
.map(f16::from_f32)
.collect();
let b: Vec<f16> = vec![2.0, 3.0, 4.0].into_iter().map(f16::from_f32).collect();
let got = run_binary_f16(&backend, OpKind::MulElementwise, &a, &b);
let expected: Vec<f32> = vec![3.0, -6.0, 2.0];
for (g, e) in got.iter().zip(expected.iter()) {
assert!(
(g.to_f32() - e).abs() < 0.01,
"mul f16: got={}, expected={e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_maximum_f16() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let a: Vec<f16> = vec![1.0, -2.0, 3.0]
.into_iter()
.map(f16::from_f32)
.collect();
let b: Vec<f16> = vec![-1.0, 2.0, 0.5]
.into_iter()
.map(f16::from_f32)
.collect();
let got = run_binary_f16(&backend, OpKind::MaximumElementwise, &a, &b);
let expected: Vec<f32> = vec![1.0, 2.0, 3.0];
for (g, e) in got.iter().zip(expected.iter()) {
assert!(
(g.to_f32() - e).abs() < 0.01,
"max f16: got={}, expected={e}",
g.to_f32()
);
}
}
#[test]
fn vulkan_dispatch_unary_f16_registered() {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let key = [DType::F16, DType::F16];
for op in [
OpKind::NegElementwise,
OpKind::SqrElementwise,
OpKind::ReluElementwise,
OpKind::TanhElementwise,
OpKind::SiluElementwise,
OpKind::GeluTanhElementwise,
] {
let alts = table.lookup_alternatives(op, &key, BackendId::Vulkan);
assert_eq!(
alts.len(),
1,
"expected 1 Vulkan alt for {op:?} f16, got {}",
alts.len()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_neg_f16() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let host: Vec<f16> = vec![1.0, -2.5, 0.0, 3.0]
.into_iter()
.map(f16::from_f32)
.collect();
let got = run_unary_f16(&backend, OpKind::NegElementwise, &host);
for (g, src) in got.iter().zip(host.iter()) {
assert!(
(g.to_f32() - (-src.to_f32())).abs() < 1e-3,
"neg f16: src={}, got={}",
src.to_f32(),
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_relu_f16() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let host: Vec<f16> = vec![-1.0, 0.0, 1.0, -2.5, 2.5]
.into_iter()
.map(f16::from_f32)
.collect();
let got = run_unary_f16(&backend, OpKind::ReluElementwise, &host);
let expected = [0.0, 0.0, 1.0, 0.0, 2.5];
for (g, e) in got.iter().zip(expected.iter()) {
assert!(
(g.to_f32() - e).abs() < 1e-3,
"relu f16: got={}, expected={e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_tanh_f16() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let host: Vec<f16> = vec![-1.0, 0.0, 1.0, 2.0]
.into_iter()
.map(f16::from_f32)
.collect();
let got = run_unary_f16(&backend, OpKind::TanhElementwise, &host);
for (g, src) in got.iter().zip(host.iter()) {
let expected = src.to_f32().tanh();
assert!(
(g.to_f32() - expected).abs() < 0.01,
"tanh f16: src={}, got={}, expected={expected}",
src.to_f32(),
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sigmoid_f16() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let host: Vec<f16> = vec![-2.0, 0.0, 2.0, 5.0]
.into_iter()
.map(f16::from_f32)
.collect();
let got = run_unary_f16(&backend, OpKind::SigmoidElementwise, &host);
for (g, src) in got.iter().zip(host.iter()) {
let expected = 1.0 / (1.0 + (-src.to_f32()).exp());
assert!(
(g.to_f32() - expected).abs() < 0.01,
"sigmoid f16: src={}, got={}, expected={expected}",
src.to_f32(),
g.to_f32()
);
}
}
fn upload_f64(backend: &Arc<VulkanBackend>, host: &[f64]) -> Storage {
let bytes: &[u8] = bytemuck::cast_slice(host);
let vk_bytes = backend
.upload_bytes_handle(bytes)
.expect("vulkan upload_bytes_handle f64");
Storage::new(BackendStorage::Vulkan(vk_bytes), DType::F64)
}
fn download_f64(backend: &Arc<VulkanBackend>, s: &Storage) -> Vec<f64> {
let bytes = match &s.inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
bytemuck::cast_slice::<u8, f64>(&bytes).to_vec()
}
#[test]
fn vulkan_dispatch_f64_registered() {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
for op in [
OpKind::NegElementwise,
OpKind::SqrElementwise,
OpKind::ReluElementwise,
] {
let alts = table.lookup_alternatives(op, &[DType::F64, DType::F64], BackendId::Vulkan);
assert_eq!(alts.len(), 1, "expected 1 Vulkan alt for {op:?} f64");
}
for op in [OpKind::AddElementwise, OpKind::MulElementwise] {
let alts =
table.lookup_alternatives(op, &[DType::F64, DType::F64, DType::F64], BackendId::Vulkan);
assert_eq!(alts.len(), 1, "expected 1 Vulkan alt for {op:?} f64 binary");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_neg_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f64> = vec![1.5, -2.25, 0.0, 3.125];
let n = host.len();
let in_storage = upload_f64(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::NegElementwise,
&[DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::None,
)
.expect("neg f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
let expected: Vec<f64> = host.iter().map(|x| -x).collect();
for (g, e) in got.iter().zip(expected.iter()) {
assert!((g - e).abs() < 1e-12, "neg f64: got={g}, expected={e}");
}
}
fn run_unary_f64(backend: &Arc<VulkanBackend>, op: OpKind, data: &[f64]) -> Vec<f64> {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let n = data.len();
let in_storage = upload_f64(backend, data);
let out_bytes = backend.alloc_bytes_handle(n * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(op, &[DType::F64, DType::F64], BackendId::Vulkan)[0].kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::None,
)
.expect("kernel dispatch");
download_f64(backend, &out_arc.read().unwrap())
}
fn check_f64(label: &str, got: &[f64], expected: &[f64], tol: f64) -> f64 {
assert_eq!(got.len(), expected.len(), "{label}: length mismatch");
let mut worst: f64 = 0.0;
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
if e.is_nan() {
assert!(g.is_nan(), "{label}[{i}]: expected NaN, got {g}");
continue;
}
if e.is_infinite() {
assert_eq!(
g.is_sign_positive(),
e.is_sign_positive(),
"{label}[{i}]: sign mismatch (got {g}, expected {e})"
);
assert!(g.is_infinite(), "{label}[{i}]: expected {e}, got {g}");
continue;
}
let abs_err = (g - e).abs();
if e.abs() < 1e-14 {
assert!(
abs_err < 1e-14,
"{label}[{i}]: near-zero expected {e}, got {g}, abs err {abs_err:e}"
);
continue;
}
let rel = abs_err / e.abs();
if rel > worst {
worst = rel;
}
assert!(
rel <= tol,
"{label}[{i}]: got {g}, expected {e}, rel err {rel:e} > tol {tol:e}"
);
}
worst
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_exp_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
#[allow(clippy::approx_constant)]
let host = vec![
0.0,
1e-10,
0.5,
1.0,
std::f64::consts::E,
2.71828,
10.0,
100.0,
500.0,
700.0,
-1e-10,
-0.5,
-1.0,
-10.0,
-100.0,
-700.0,
];
let got = run_unary_f64(&backend, OpKind::ExpElementwise, &host);
let expected: Vec<f64> = host.iter().map(|x| x.exp()).collect();
let worst = check_f64("exp f64", &got, &expected, 5e-16);
eprintln!("exp f64: worst rel err = {worst:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_log_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let host = vec![
f64::MIN_POSITIVE,
1e-15,
1e-10,
0.001,
0.5,
1.0,
std::f64::consts::E,
10.0,
100.0,
1e10,
1e100,
1e300,
];
let got = run_unary_f64(&backend, OpKind::LogElementwise, &host);
let expected: Vec<f64> = host.iter().map(|x| x.ln()).collect();
let worst = check_f64("log f64", &got, &expected, 5e-12);
eprintln!("log f64: worst rel err = {worst:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sin_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, FRAC_PI_6, PI};
let small = vec![
0.0,
1e-10,
FRAC_PI_6,
FRAC_PI_4,
FRAC_PI_2,
PI,
1.5 * PI,
2.0 * PI,
-FRAC_PI_4,
-PI,
10.0,
100.0,
-100.0,
];
let got = run_unary_f64(&backend, OpKind::SinElementwise, &small);
let expected: Vec<f64> = small.iter().map(|x| x.sin()).collect();
let worst_small = check_f64("sin f64 (|x| <= 100)", &got, &expected, 5e-16);
eprintln!("sin f64 (|x| <= 100): worst rel err = {worst_small:e}");
let moderate = vec![300.0, 500.0, -700.0, 1000.0];
let got = run_unary_f64(&backend, OpKind::SinElementwise, &moderate);
let expected: Vec<f64> = moderate.iter().map(|x| x.sin()).collect();
let worst_mod = check_f64("sin f64 (|x| <= 1000)", &got, &expected, 5e-16);
eprintln!("sin f64 (|x| <= 1000): worst rel err = {worst_mod:e}");
let large = vec![1e5, 5e5, 1e6, 5e6, -1e6];
let got = run_unary_f64(&backend, OpKind::SinElementwise, &large);
let expected: Vec<f64> = large.iter().map(|x| x.sin()).collect();
let worst_lg = check_f64("sin f64 (|x| <= 5e6)", &got, &expected, 5e-16);
eprintln!("sin f64 (|x| <= 5e6): worst rel err = {worst_lg:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sin_f64_huge() {
let Some(backend) = backend_or_skip() else {
return;
};
let host = vec![
1.0e10_f64,
1.0e12,
1.0e15,
(1u64 << 53) as f64 - 1.0,
((1u64 << 53) as f64) / 3.0,
1.0e10 * std::f64::consts::PI,
1.0e12 * std::f64::consts::PI,
];
let got = run_unary_f64(&backend, OpKind::SinElementwise, &host);
let expected: Vec<f64> = host.iter().map(|x| x.sin()).collect();
let worst = check_f64("sin f64 (huge)", &got, &expected, 5e-14);
eprintln!("sin f64 (huge): worst rel err = {worst:e}");
for (x, (g, e)) in host.iter().zip(got.iter().zip(expected.iter())) {
eprintln!(" sin({x:e}) = {g:e} (libm: {e:e})");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_cos_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI};
let small = vec![
0.0,
FRAC_PI_4,
FRAC_PI_2,
PI,
2.0 * PI,
-FRAC_PI_4,
-PI,
1.0,
5.0,
100.0,
];
let got = run_unary_f64(&backend, OpKind::CosElementwise, &small);
let expected: Vec<f64> = small.iter().map(|x| x.cos()).collect();
let worst_small = check_f64("cos f64 (|x| <= 100)", &got, &expected, 5e-16);
eprintln!("cos f64 (|x| <= 100): worst rel err = {worst_small:e}");
let moderate = vec![300.0, 1000.0, -500.0];
let got = run_unary_f64(&backend, OpKind::CosElementwise, &moderate);
let expected: Vec<f64> = moderate.iter().map(|x| x.cos()).collect();
let worst_mod = check_f64("cos f64 (|x| <= 1000)", &got, &expected, 5e-16);
eprintln!("cos f64 (|x| <= 1000): worst rel err = {worst_mod:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_tanh_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let host = vec![
0.0, 1e-10, 0.5, 1.0, 2.0, 5.0, 10.0, 18.0, 50.0, -0.5, -1.0, -5.0, -18.0,
];
let got = run_unary_f64(&backend, OpKind::TanhElementwise, &host);
let expected: Vec<f64> = host.iter().map(|x| x.tanh()).collect();
let worst = check_f64("tanh f64", &got, &expected, 5e-12);
eprintln!("tanh f64: worst rel err = {worst:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sigmoid_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let host = vec![-10.0, -3.0, -1.0, -0.5, 0.0, 0.5, 1.0, 3.0, 10.0];
let got = run_unary_f64(&backend, OpKind::SigmoidElementwise, &host);
let expected: Vec<f64> = host.iter().map(|x| 1.0 / (1.0 + (-x).exp())).collect();
let worst = check_f64("sigmoid f64", &got, &expected, 5e-12);
eprintln!("sigmoid f64: worst rel err = {worst:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_silu_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let host = vec![-5.0, -1.0, -0.1, 0.0, 0.1, 1.0, 5.0];
let got = run_unary_f64(&backend, OpKind::SiluElementwise, &host);
let expected: Vec<f64> = host.iter().map(|x| x / (1.0 + (-x).exp())).collect();
let worst = check_f64("silu f64", &got, &expected, 5e-12);
eprintln!("silu f64: worst rel err = {worst:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_gelu_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let host = vec![-3.0, -1.0, -0.5, 0.0, 0.5, 1.0, 3.0];
let got = run_unary_f64(&backend, OpKind::GeluTanhElementwise, &host);
let expected: Vec<f64> = host
.iter()
.map(|x| {
let inner = 0.7978845608028654 * (x + 0.044715 * x * x * x);
0.5 * x * (1.0 + inner.tanh())
})
.collect();
let worst = check_f64("gelu_tanh f64", &got, &expected, 1e-11);
eprintln!("gelu_tanh f64: worst rel err = {worst:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_add_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a = vec![1.0_f64, 2.0, 3.0, 4.0];
let b = vec![10.0_f64, 20.0, 30.0, 40.0];
let n = a.len();
let a_storage = upload_f64(&backend, &a);
let b_storage = upload_f64(&backend, &b);
let out_bytes = backend.alloc_bytes_handle(n * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::AddElementwise,
&[DType::F64, DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::None,
)
.expect("add f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![11.0, 22.0, 33.0, 44.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_write_slice_b4_1d_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let dst_init = vec![0.0_f32; 8];
let dst_bytes: &[u8] = bytemuck::cast_slice(&dst_init);
let dst_vk = backend.upload_bytes_handle(dst_bytes).expect("upload dst");
let dst_storage = Storage::new(BackendStorage::Vulkan(dst_vk), DType::F32);
let src = vec![10.0_f32, 20.0, 30.0];
let src_vk = backend
.upload_bytes_handle(bytemuck::cast_slice(&src))
.expect("upload src");
let src_storage = Storage::new(BackendStorage::Vulkan(src_vk), DType::F32);
let src_arc = Arc::new(RwLock::new(src_storage));
let dst_arc = Arc::new(RwLock::new(dst_storage));
let kernel = table.lookup_alternatives(
OpKind::WriteSlice,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[3])),
Layout::contiguous(Shape::from_dims(&[8])),
];
kernel(
&[Arc::clone(&src_arc)],
&mut [Arc::clone(&dst_arc)],
&layouts,
&OpParams::WriteSlice {
dest_shape: vec![8],
ranges: vec![(2, 5)],
deferred_dyn_offset: None,
},
)
.expect("write_slice dispatch");
let got = download_f32(&backend, &dst_arc.read().unwrap());
assert_eq!(got, vec![0.0, 0.0, 10.0, 20.0, 30.0, 0.0, 0.0, 0.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_write_slice_b4_2d_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let dst_init = vec![0.5_f32; 12];
let dst_vk = backend
.upload_bytes_handle(bytemuck::cast_slice(&dst_init))
.expect("upload dst");
let dst_storage = Storage::new(BackendStorage::Vulkan(dst_vk), DType::F32);
let src = vec![10.0_f32, 20.0, 30.0, 40.0];
let src_vk = backend
.upload_bytes_handle(bytemuck::cast_slice(&src))
.expect("upload src");
let src_storage = Storage::new(BackendStorage::Vulkan(src_vk), DType::F32);
let src_arc = Arc::new(RwLock::new(src_storage));
let dst_arc = Arc::new(RwLock::new(dst_storage));
let kernel = table.lookup_alternatives(
OpKind::WriteSlice,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[2, 2])),
Layout::contiguous(Shape::from_dims(&[3, 4])),
];
kernel(
&[Arc::clone(&src_arc)],
&mut [Arc::clone(&dst_arc)],
&layouts,
&OpParams::WriteSlice {
dest_shape: vec![3, 4],
ranges: vec![(1, 3), (1, 3)],
deferred_dyn_offset: None,
},
)
.expect("write_slice 2d dispatch");
let got = download_f32(&backend, &dst_arc.read().unwrap());
assert_eq!(
got,
vec![
0.5, 0.5, 0.5, 0.5, 0.5, 10.0, 20.0, 0.5, 0.5, 30.0, 40.0, 0.5,
]
);
}
#[test]
#[ignore]
fn vulkan_dispatch_write_slice_b2_f16_kv_cache() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let seq = 4usize;
let head_dim = 8usize;
let dst_init: Vec<f16> = vec![f16::from_f32(-1.0); seq * head_dim];
let dst_vk = backend
.upload_bytes_handle(bytemuck::cast_slice(&dst_init))
.expect("upload");
let dst_storage = Storage::new(BackendStorage::Vulkan(dst_vk), DType::F16);
let src: Vec<f16> = (0..head_dim)
.map(|i| f16::from_f32(i as f32 + 100.0))
.collect();
let src_vk = backend
.upload_bytes_handle(bytemuck::cast_slice(&src))
.expect("upload");
let src_storage = Storage::new(BackendStorage::Vulkan(src_vk), DType::F16);
let src_arc = Arc::new(RwLock::new(src_storage));
let dst_arc = Arc::new(RwLock::new(dst_storage));
let kernel = table.lookup_alternatives(
OpKind::WriteSlice,
&[DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[1, head_dim])),
Layout::contiguous(Shape::from_dims(&[seq, head_dim])),
];
kernel(
&[Arc::clone(&src_arc)],
&mut [Arc::clone(&dst_arc)],
&layouts,
&OpParams::WriteSlice {
dest_shape: vec![seq, head_dim],
ranges: vec![(2, 3), (0, head_dim)],
deferred_dyn_offset: None,
},
)
.expect("write_slice b2 dispatch");
let raw = match &dst_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!(),
};
let got: Vec<f16> = bytemuck::cast_slice::<u8, f16>(&raw).to_vec();
#[allow(clippy::erasing_op, clippy::identity_op)]
for j in 0..head_dim {
let row0 = got[0 * head_dim + j];
let row1 = got[1 * head_dim + j];
let row2 = got[2 * head_dim + j];
let row3 = got[3 * head_dim + j];
assert_eq!(row0.to_f32(), -1.0);
assert_eq!(row1.to_f32(), -1.0);
assert_eq!(row2.to_f32(), j as f32 + 100.0);
assert_eq!(row3.to_f32(), -1.0);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_write_slice_b8_f64_1d() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let dst_init: Vec<f64> = vec![0.0; 6];
let dst_vk = backend
.upload_bytes_handle(bytemuck::cast_slice(&dst_init))
.expect("upload");
let dst_storage = Storage::new(BackendStorage::Vulkan(dst_vk), DType::F64);
let src: Vec<f64> = vec![100.0, 200.0, 300.0];
let src_vk = backend
.upload_bytes_handle(bytemuck::cast_slice(&src))
.expect("upload");
let src_storage = Storage::new(BackendStorage::Vulkan(src_vk), DType::F64);
let src_arc = Arc::new(RwLock::new(src_storage));
let dst_arc = Arc::new(RwLock::new(dst_storage));
let kernel = table.lookup_alternatives(
OpKind::WriteSlice,
&[DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[3])),
Layout::contiguous(Shape::from_dims(&[6])),
];
kernel(
&[Arc::clone(&src_arc)],
&mut [Arc::clone(&dst_arc)],
&layouts,
&OpParams::WriteSlice {
dest_shape: vec![6],
ranges: vec![(1, 4)],
deferred_dyn_offset: None,
},
)
.expect("write_slice b8 dispatch");
let raw = match &dst_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!(),
};
let got: Vec<f64> = bytemuck::cast_slice::<u8, f64>(&raw).to_vec();
assert_eq!(got, vec![0.0, 100.0, 200.0, 300.0, 0.0, 0.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_write_slice_b4_kv_cache_shape() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let seq = 4usize;
let head_dim = 8usize;
let dst_init = vec![-1.0_f32; seq * head_dim];
let dst_vk = backend
.upload_bytes_handle(bytemuck::cast_slice(&dst_init))
.expect("upload dst");
let dst_storage = Storage::new(BackendStorage::Vulkan(dst_vk), DType::F32);
let src: Vec<f32> = (0..head_dim).map(|i| i as f32 + 100.0).collect();
let src_vk = backend
.upload_bytes_handle(bytemuck::cast_slice(&src))
.expect("upload src");
let src_storage = Storage::new(BackendStorage::Vulkan(src_vk), DType::F32);
let src_arc = Arc::new(RwLock::new(src_storage));
let dst_arc = Arc::new(RwLock::new(dst_storage));
let kernel = table.lookup_alternatives(
OpKind::WriteSlice,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[1, head_dim])),
Layout::contiguous(Shape::from_dims(&[seq, head_dim])),
];
kernel(
&[Arc::clone(&src_arc)],
&mut [Arc::clone(&dst_arc)],
&layouts,
&OpParams::WriteSlice {
dest_shape: vec![seq, head_dim],
ranges: vec![(2, 3), (0, head_dim)],
deferred_dyn_offset: None,
},
)
.expect("write_slice kv dispatch");
let got = download_f32(&backend, &dst_arc.read().unwrap());
#[allow(clippy::erasing_op, clippy::identity_op)]
for j in 0..head_dim {
assert_eq!(got[0 * head_dim + j], -1.0);
assert_eq!(got[1 * head_dim + j], -1.0);
assert_eq!(got[2 * head_dim + j], (j as f32) + 100.0);
assert_eq!(got[3 * head_dim + j], -1.0);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_cast_f32_to_bf16() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host_f32: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0, 1e-3, 1e3];
let n = host_f32.len();
let in_storage = upload_f32(&backend, &host_f32);
let out_bytes_h = backend.alloc_bytes_handle(n * 2).expect("alloc bf16 out");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Cast, &[DType::F32, DType::BF16], BackendId::Vulkan)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[n])),
Layout::contiguous(Shape::from_dims(&[n])),
];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("cast f32→bf16 dispatch");
let raw = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<bf16> = bytemuck::cast_slice::<u8, bf16>(&raw).to_vec();
for (i, (g, src)) in got.iter().zip(host_f32.iter()).enumerate() {
let expected = bf16::from_f32(*src);
assert_eq!(
g.to_bits(),
expected.to_bits(),
"cast[{i}]: f32={src}, got bf16 bits={:#x}, expected {:#x}",
g.to_bits(),
expected.to_bits()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_cast_bf16_to_f32() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host_bf16: Vec<bf16> = vec![
bf16::from_f32(1.0),
bf16::from_f32(-2.5),
bf16::from_f32(3.25),
bf16::from_f32(0.0),
bf16::from_f32(1e-3),
bf16::from_f32(1e3),
];
let n = host_bf16.len();
let bf16_bytes: &[u8] = bytemuck::cast_slice(&host_bf16);
let in_vk = backend
.upload_bytes_handle(bf16_bytes)
.expect("upload bf16");
let in_storage = Storage::new(BackendStorage::Vulkan(in_vk), DType::BF16);
let out_bytes_h = backend.alloc_bytes_handle(n * 4).expect("alloc f32 out");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Cast, &[DType::BF16, DType::F32], BackendId::Vulkan)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[n])),
Layout::contiguous(Shape::from_dims(&[n])),
];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("cast bf16→f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, src)) in got.iter().zip(host_bf16.iter()).enumerate() {
let expected = src.to_f32();
assert!(
(g - expected).abs() < 1e-3,
"cast[{i}]: bf16={}, got f32={g}, expected {expected}",
src.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_cast_f32_to_f16() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host_f32: Vec<f32> = vec![1.0, -2.5, 3.25, 0.0, 1e-3, 1e3];
let n = host_f32.len();
let in_storage = upload_f32(&backend, &host_f32);
let out_bytes_h = backend.alloc_bytes_handle(n * 2).expect("alloc f16 out");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Cast, &[DType::F32, DType::F16], BackendId::Vulkan)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[n])),
Layout::contiguous(Shape::from_dims(&[n])),
];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("cast f32→f16 dispatch");
let raw = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<f16> = bytemuck::cast_slice::<u8, f16>(&raw).to_vec();
for (i, (g, src)) in got.iter().zip(host_f32.iter()).enumerate() {
let expected = f16::from_f32(*src);
let g_bits = g.to_bits() as i32;
let e_bits = expected.to_bits() as i32;
assert!(
(g_bits - e_bits).abs() <= 1,
"cast[{i}]: f32={src}, got f16 bits={g_bits:#x}, expected {e_bits:#x}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_cast_f16_to_f32() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host_f16: Vec<f16> = vec![
f16::from_f32(1.0),
f16::from_f32(-2.5),
f16::from_f32(3.25),
f16::from_f32(0.0),
f16::from_f32(1e-3),
f16::from_f32(1e3),
];
let n = host_f16.len();
let f16_bytes: &[u8] = bytemuck::cast_slice(&host_f16);
let in_vk = backend.upload_bytes_handle(f16_bytes).expect("upload f16");
let in_storage = Storage::new(BackendStorage::Vulkan(in_vk), DType::F16);
let out_bytes_h = backend.alloc_bytes_handle(n * 4).expect("alloc f32 out");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Cast, &[DType::F16, DType::F32], BackendId::Vulkan)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[n])),
Layout::contiguous(Shape::from_dims(&[n])),
];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::None,
)
.expect("cast f16→f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, src)) in got.iter().zip(host_f16.iter()).enumerate() {
let expected = src.to_f32();
assert!(
(g - expected).abs() < 1e-3,
"cast[{i}]: f16={}, got f32={g}, expected {expected}",
src.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_powi_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f32> = vec![-2.0, -1.0, 0.0, 1.0, 2.0, 3.0];
let n = host.len();
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::PowIElementwise,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::PowI { exp: 3 },
)
.expect("powi dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expected: Vec<f32> = host.iter().map(|x| x.powi(3)).collect();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!((g - e).abs() < 1e-5, "powi at {i}: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_powi_squared_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f32> = vec![-2.5, -1.0, 0.0, 0.5, 1.5, 3.0];
let n = host.len();
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::PowIElementwise,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::PowI { exp: 2 },
)
.expect("powi dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expected: Vec<f32> = host.iter().map(|x| x * x).collect();
assert_eq!(got, expected);
}
#[test]
#[ignore]
fn vulkan_dispatch_clamp_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f32> = vec![-3.0, -1.5, -0.5, 0.5, 1.5, 3.0];
let n = host.len();
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ClampElementwise,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Clamp {
min: -1.0,
max: 1.0,
},
)
.expect("clamp dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![-1.0, -1.0, -0.5, 0.5, 1.0, 1.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_affine_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let n = host.len();
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Affine, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Affine { mul: 2.0, add: 3.0 },
)
.expect("affine dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expected: Vec<f32> = host.iter().map(|x| 2.0 * x + 3.0).collect();
assert_eq!(got, expected);
}
#[test]
#[ignore]
fn vulkan_dispatch_affine_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let n = host.len();
let in_storage = upload_f64(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Affine, &[DType::F64, DType::F64], BackendId::Vulkan)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Affine { mul: 2.0, add: 3.0 },
)
.expect("affine f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
let expected: Vec<f64> = host.iter().map(|x| 2.0 * x + 3.0).collect();
assert_eq!(got, expected);
}
#[test]
#[ignore]
fn vulkan_dispatch_affine_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let host: Vec<half::f16> = host_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let n = host.len();
let in_storage = upload_f16(&backend, &host);
let out_bytes = backend
.alloc_bytes_handle(((n * 2 + 3) & !3) as usize)
.expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Affine, &[DType::F16, DType::F16], BackendId::Vulkan)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Affine { mul: 2.0, add: 3.0 },
)
.expect("affine f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for (i, (g, &x)) in got.iter().take(n).zip(host_f32.iter()).enumerate() {
let expected = 2.0_f32 * x + 3.0;
assert_eq!(
g.to_f32(),
expected,
"affine f16[{i}]: got {}, expected {expected}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_affine_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let host: Vec<half::bf16> = host_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let n = host.len();
let in_storage = upload_bf16(&backend, &host);
let out_bytes = backend
.alloc_bytes_handle(((n * 2 + 3) & !3) as usize)
.expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Affine,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Affine { mul: 2.0, add: 3.0 },
)
.expect("affine bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for (i, (g, &x)) in got.iter().take(n).zip(host_f32.iter()).enumerate() {
let expected = 2.0_f32 * x + 3.0;
assert_eq!(
g.to_f32(),
expected,
"affine bf16[{i}]: got {}, expected {expected}",
g.to_f32()
);
}
}
fn run_matmul_f32(
backend: &Arc<VulkanBackend>,
lhs: &[f32],
lhs_batch: &[usize],
rhs: &[f32],
rhs_batch: &[usize],
m: usize,
n: usize,
k: usize,
) -> Vec<f32> {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let lhs_storage = upload_f32(backend, lhs);
let rhs_storage = upload_f32(backend, rhs);
let lhs_total: usize = lhs_batch.iter().product::<usize>().max(1);
let out_n = lhs_total * m * n;
let out_bytes = backend.alloc_bytes_handle(out_n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let lhs_arc = Arc::new(RwLock::new(lhs_storage));
let rhs_arc = Arc::new(RwLock::new(rhs_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let mut lhs_dims = lhs_batch.to_vec();
lhs_dims.extend_from_slice(&[m, k]);
let mut rhs_dims = rhs_batch.to_vec();
rhs_dims.extend_from_slice(&[k, n]);
let mut out_dims = lhs_batch.to_vec();
out_dims.extend_from_slice(&[m, n]);
let layouts = vec![
Layout::contiguous(Shape::from_dims(&lhs_dims)),
Layout::contiguous(Shape::from_dims(&rhs_dims)),
Layout::contiguous(Shape::from_dims(&out_dims)),
];
kernel(
&[Arc::clone(&lhs_arc), Arc::clone(&rhs_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: lhs_batch.to_vec(),
rhs_batch_dims: rhs_batch.to_vec(),
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("matmul dispatch");
download_f32(backend, &out_arc.read().unwrap())
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_f32_bf16_b_small_m() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let b_bf16: Vec<bf16> = vec![
bf16::from_f32(1.0),
bf16::from_f32(2.0),
bf16::from_f32(3.0),
bf16::from_f32(4.0),
bf16::from_f32(5.0),
bf16::from_f32(6.0),
];
let a_bytes: &[u8] = bytemuck::cast_slice(&a_f32);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_bf16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::F32);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::BF16);
let out_n = 2 * 2;
let out_bytes_h = backend.alloc_bytes_handle(out_n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F32, DType::BF16, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[2, 3])),
Layout::contiguous(Shape::from_dims(&[3, 2])),
Layout::contiguous(Shape::from_dims(&[2, 2])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m: 2,
n: 2,
k: 3,
m_compute: MatmulM::All,
},
)
.expect("mixed-bf16 matmul dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expected = [22.0_f32, 28.0, 49.0, 64.0];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 0.5,
"mixed-bf16 matmul [{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_f32_bf16_b_coop_size() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 16usize;
let n = 16usize;
let k = 16usize;
let a_f32: Vec<f32> = vec![1.0; m * k];
let mut b_bf16: Vec<bf16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_bf16.push(bf16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_f32);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_bf16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::F32);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::BF16);
let out_n = m * n;
let out_bytes_h = backend.alloc_bytes_handle(out_n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F32, DType::BF16, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("mixed-bf16 coop-size matmul dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j];
assert!(
(g - expected).abs() < 0.5,
"coop-size mixed-bf16 [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_bf16_bf16_f32_coop_size() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 16usize;
let n = 16usize;
let k = 16usize;
let a_bf16: Vec<bf16> = vec![bf16::from_f32(1.0); m * k];
let mut b_bf16: Vec<bf16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_bf16.push(bf16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_bf16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_bf16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::BF16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::BF16);
let out_n = m * n;
let out_bytes_h = backend.alloc_bytes_handle(out_n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::BF16, DType::BF16, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("bf16 coop-size matmul dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j];
assert!(
(g - expected).abs() < 0.5,
"bf16 coop-size [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_bf16_bf16_f32_multi_tile() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 32usize;
let n = 64usize;
let k = 32usize;
let a_bf16: Vec<bf16> = vec![bf16::from_f32(1.0); m * k];
let mut b_bf16: Vec<bf16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_bf16.push(bf16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_bf16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_bf16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::BF16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::BF16);
let out_bytes_h = backend.alloc_bytes_handle(m * n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::BF16, DType::BF16, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("bf16 multi-tile matmul dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j];
assert!(
(g - expected).abs() < 2.0,
"bf16 multi-tile [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_bf16_bf16_bf16_coop_size() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 16usize;
let n = 16usize;
let k = 16usize;
let a_bf16: Vec<bf16> = vec![bf16::from_f32(1.0); m * k];
let mut b_bf16: Vec<bf16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_bf16.push(bf16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_bf16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_bf16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::BF16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::BF16);
let out_bytes_h = backend.alloc_bytes_handle(m * n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::BF16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::BF16, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("bf16→bf16 coop-size matmul dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j].to_f32();
assert_eq!(
g, expected,
"bf16→bf16 coop-size [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_bf16_bf16_bf16_multi_tile() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 32usize;
let n = 64usize;
let k = 32usize;
let a_bf16: Vec<bf16> = vec![bf16::from_f32(1.0); m * k];
let mut b_bf16: Vec<bf16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_bf16.push(bf16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_bf16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_bf16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::BF16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::BF16);
let out_bytes_h = backend.alloc_bytes_handle(m * n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::BF16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::BF16, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("bf16→bf16 multi-tile matmul dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j].to_f32();
assert!(
(g - expected).abs() <= 16.0,
"bf16→bf16 multi-tile [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_bf16_bf16_f32_small() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 4usize;
let n = 8usize;
let k = 20usize;
let a_bf16: Vec<bf16> = vec![bf16::from_f32(1.0); m * k];
let mut b_bf16: Vec<bf16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_bf16.push(bf16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_bf16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_bf16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::BF16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::BF16);
let out_bytes_h = backend.alloc_bytes_handle(m * n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::BF16, DType::BF16, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("bf16 small-shape matmul dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j];
assert!(
(g - expected).abs() < 0.5,
"bf16 small [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_bf16_bf16_bf16_small() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 4usize;
let n = 8usize;
let k = 20usize;
let a_bf16: Vec<bf16> = vec![bf16::from_f32(1.0); m * k];
let mut b_bf16: Vec<bf16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_bf16.push(bf16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_bf16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_bf16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::BF16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::BF16);
let out_bytes_h = backend
.alloc_bytes_handle((m * n * 2 + 3) & !3)
.expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::BF16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::BF16, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("bf16→bf16 small-shape matmul dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j].to_f32();
assert_eq!(
g, expected,
"bf16→bf16 small [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_f16_f16_f32_coop_size() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 16usize;
let n = 16usize;
let k = 16usize;
let a_f16: Vec<f16> = vec![f16::from_f32(1.0); m * k];
let mut b_f16: Vec<f16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_f16.push(f16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_f16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_f16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::F16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::F16);
let out_bytes_h = backend.alloc_bytes_handle(m * n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F16, DType::F16, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("f16 coop-size matmul dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j];
assert!(
(g - expected).abs() < 0.5,
"f16 coop-size [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_f16_f16_f32_multi_tile() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 32usize;
let n = 64usize;
let k = 32usize;
let a_f16: Vec<f16> = vec![f16::from_f32(1.0); m * k];
let mut b_f16: Vec<f16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_f16.push(f16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_f16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_f16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::F16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::F16);
let out_bytes_h = backend.alloc_bytes_handle(m * n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F16, DType::F16, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("f16 multi-tile matmul dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j];
assert!(
(g - expected).abs() < 2.0,
"f16 multi-tile [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_f16_f16_f16_coop_size() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 16usize;
let n = 16usize;
let k = 16usize;
let a_f16: Vec<f16> = vec![f16::from_f32(1.0); m * k];
let mut b_f16: Vec<f16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_f16.push(f16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_f16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_f16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::F16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::F16);
let out_bytes_h = backend.alloc_bytes_handle(m * n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F16, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("f16→f16 coop-size matmul dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j].to_f32();
assert_eq!(
g, expected,
"f16→f16 coop-size [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_f16_f16_f16_multi_tile() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 32usize;
let n = 64usize;
let k = 32usize;
let a_f16: Vec<f16> = vec![f16::from_f32(1.0); m * k];
let mut b_f16: Vec<f16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_f16.push(f16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_f16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_f16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::F16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::F16);
let out_bytes_h = backend.alloc_bytes_handle(m * n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F16, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("f16→f16 multi-tile matmul dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j].to_f32();
assert_eq!(
g, expected,
"f16→f16 multi-tile [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_f16_f16_f32_small() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 4usize;
let n = 8usize;
let k = 20usize;
let a_f16: Vec<f16> = vec![f16::from_f32(1.0); m * k];
let mut b_f16: Vec<f16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_f16.push(f16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_f16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_f16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::F16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::F16);
let out_bytes_h = backend.alloc_bytes_handle(m * n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F16, DType::F16, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("f16 small-shape matmul dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j];
assert!(
(g - expected).abs() < 0.5,
"f16 small [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_f16_f16_f16_small() {
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 4usize;
let n = 8usize;
let k = 20usize;
let a_f16: Vec<f16> = vec![f16::from_f32(1.0); m * k];
let mut b_f16: Vec<f16> = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b_f16.push(f16::from_f32(j as f32));
}
}
let a_bytes: &[u8] = bytemuck::cast_slice(&a_f16);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_f16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::F16);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::F16);
let out_bytes_h = backend
.alloc_bytes_handle((m * n * 2 + 3) & !3)
.expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F16, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[m, k])),
Layout::contiguous(Shape::from_dims(&[k, n])),
Layout::contiguous(Shape::from_dims(&[m, n])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m,
n,
k,
m_compute: MatmulM::All,
},
)
.expect("f16→f16 small-shape matmul dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for i in 0..m {
for j in 0..n {
let expected = (k * j) as f32;
let g = got[i * n + j].to_f32();
assert_eq!(
g, expected,
"f16→f16 small [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_f32_bf16_b_matvec() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a_f32: Vec<f32> = vec![1.0, 2.0, 3.0]; let b_bf16: Vec<bf16> = vec![
bf16::from_f32(1.0),
bf16::from_f32(2.0),
bf16::from_f32(3.0),
bf16::from_f32(4.0),
bf16::from_f32(5.0),
bf16::from_f32(6.0),
];
let a_bytes: &[u8] = bytemuck::cast_slice(&a_f32);
let b_bytes: &[u8] = bytemuck::cast_slice(&b_bf16);
let a_vk = backend.upload_bytes_handle(a_bytes).expect("a upload");
let b_vk = backend.upload_bytes_handle(b_bytes).expect("b upload");
let a_storage = Storage::new(BackendStorage::Vulkan(a_vk), DType::F32);
let b_storage = Storage::new(BackendStorage::Vulkan(b_vk), DType::BF16);
#[allow(clippy::identity_op)]
let out_n = 1 * 2;
let out_bytes_h = backend.alloc_bytes_handle(out_n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MatMul,
&[DType::F32, DType::BF16, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[1, 3])),
Layout::contiguous(Shape::from_dims(&[3, 2])),
Layout::contiguous(Shape::from_dims(&[1, 2])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Matmul {
lhs_batch_dims: vec![],
rhs_batch_dims: vec![],
m: 1,
n: 2,
k: 3,
m_compute: MatmulM::All,
},
)
.expect("mixed-bf16 matvec dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expected = [22.0_f32, 28.0];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 0.5,
"mixed-bf16 matvec [{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_matvec_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_matmul_f32(
&backend,
&[1.0, 2.0, 3.0],
&[],
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
&[],
1,
2,
3,
);
assert_eq!(got, vec![22.0, 28.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_small_m_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_matmul_f32(
&backend,
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
&[],
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
&[],
2,
2,
3,
);
assert_eq!(got, vec![22.0, 28.0, 49.0, 64.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_tiled_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let m = 32usize;
let n = 3usize;
let k = 4usize;
let mut a = Vec::with_capacity(m * k);
for i in 0..m {
for _j in 0..k {
a.push(i as f32);
}
}
let mut b = Vec::with_capacity(k * n);
for _i in 0..k {
for j in 0..n {
b.push((j + 1) as f32);
}
}
let got = run_matmul_f32(&backend, &a, &[], &b, &[], m, n, k);
for i in 0..m {
for j in 0..n {
let expected = (i * k * (j + 1)) as f32;
let g = got[i * n + j];
assert!(
(g - expected).abs() < 1e-3,
"tiled matmul [{i}, {j}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_batched_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let lhs: Vec<f32> = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0, 1.0, 1.0, 2.0, 2.0, 2.0, ];
let rhs: Vec<f32> = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, ];
let got = run_matmul_f32(&backend, &lhs, &[2], &rhs, &[2], 2, 2, 3);
assert_eq!(
got,
vec![
22.0, 28.0, 49.0, 64.0, 3.0, 3.0, 6.0, 6.0, ]
);
}
#[test]
#[ignore]
fn vulkan_dispatch_matmul_gqa_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let lhs: Vec<f32> = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, ];
let rhs: Vec<f32> = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0,
];
let got = run_matmul_f32(&backend, &lhs, &[4], &rhs, &[2], 1, 2, 3);
assert_eq!(got, vec![22.0, 28.0, 49.0, 64.0, 1.0, 0.0, 0.0, 1.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_concat_n3_along_last_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a_data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
let b_data: Vec<f32> = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
let c_data: Vec<f32> = vec![100.0, 200.0];
let a_storage = upload_f32(&backend, &a_data);
let b_storage = upload_f32(&backend, &b_data);
let c_storage = upload_f32(&backend, &c_data);
let out_bytes = backend.alloc_bytes_handle(12 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let c_arc = Arc::new(RwLock::new(c_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Concat, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let layouts = vec![
Layout::contiguous(Shape::from_dims(&[2, 2])),
Layout::contiguous(Shape::from_dims(&[2, 3])),
Layout::contiguous(Shape::from_dims(&[2, 1])),
Layout::contiguous(Shape::from_dims(&[2, 6])),
];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc), Arc::clone(&c_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Concat {
outer_count: 2,
input_dim_sizes: vec![2, 3, 1],
inner_count: 1,
axis: 1,
},
)
.expect("N=3 concat dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(
got,
vec![
1.0, 2.0, 10.0, 20.0, 30.0, 100.0, 3.0, 4.0, 40.0, 50.0, 60.0, 200.0,
]
);
}
#[test]
#[ignore]
fn vulkan_dispatch_concat_n4_along_leading_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a = vec![1.0_f32, 2.0];
let b = vec![3.0_f32, 4.0, 5.0, 6.0];
let c = vec![7.0_f32, 8.0];
let d = vec![9.0_f32, 10.0];
let arc_for = |v: &[f32], dim0: usize| {
let st = upload_f32(&backend, v);
(
Arc::new(RwLock::new(st)),
Layout::contiguous(Shape::from_dims(&[dim0, 2])),
)
};
let (a_arc, a_l) = arc_for(&a, 1);
let (b_arc, b_l) = arc_for(&b, 2);
let (c_arc, c_l) = arc_for(&c, 1);
let (d_arc, d_l) = arc_for(&d, 1);
let out_n = 5 * 2;
let out_bytes = backend.alloc_bytes_handle(out_n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Concat, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let layouts = vec![
a_l,
b_l,
c_l,
d_l,
Layout::contiguous(Shape::from_dims(&[5, 2])),
];
kernel(
&[
Arc::clone(&a_arc),
Arc::clone(&b_arc),
Arc::clone(&c_arc),
Arc::clone(&d_arc),
],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Concat {
outer_count: 1,
input_dim_sizes: vec![1, 2, 1, 1],
inner_count: 2,
axis: 0,
},
)
.expect("N=4 concat dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(
got,
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0,]
);
}
#[test]
#[ignore]
fn vulkan_dispatch_concat_along_last_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a_data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]; let b_data: Vec<f32> = vec![7.0, 8.0, 9.0, 10.0];
let a_storage = upload_f32(&backend, &a_data);
let b_storage = upload_f32(&backend, &b_data);
let out_n = 2 * 5;
let out_bytes = backend.alloc_bytes_handle(out_n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Concat, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let a_layout = Layout::contiguous(Shape::from_dims(&[2, 3]));
let b_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 5]));
let layouts = vec![a_layout, b_layout, out_layout];
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Concat {
outer_count: 2,
input_dim_sizes: vec![3, 2],
inner_count: 1,
axis: 1,
},
)
.expect("concat dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![1.0, 2.0, 3.0, 7.0, 8.0, 4.0, 5.0, 6.0, 9.0, 10.0]);
}
fn run_reduce_f32(
backend: &Arc<VulkanBackend>,
op: OpKind,
data: &[f32],
dims: &[usize],
reduce_dims: Vec<usize>,
) -> Vec<f32> {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let in_storage = upload_f32(backend, data);
let rank = dims.len();
let out_elems = if reduce_dims.is_empty() || reduce_dims.len() == rank {
1
} else if reduce_dims.len() == 1 && reduce_dims[0] == rank - 1 {
dims[..rank - 1].iter().product::<usize>().max(1)
} else {
panic!("test helper only supports full + last-dim reduce")
};
let out_bytes = backend.alloc_bytes_handle(out_elems * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(op, &[DType::F32, DType::F32], BackendId::Vulkan)[0].kernel;
let layout = Layout::contiguous(Shape::from_dims(dims));
let out_layout = if reduce_dims.is_empty() || reduce_dims.len() == rank {
Layout::contiguous(Shape::from_dims(&[1]))
} else {
Layout::contiguous(Shape::from_dims(&dims[..rank - 1]))
};
let layouts = vec![layout, out_layout];
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Reduce {
dims: reduce_dims,
keepdim: false,
},
)
.expect("reduce dispatch");
download_f32(backend, &out_arc.read().unwrap())
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_sum_full_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_reduce_f32(
&backend,
OpKind::SumReduce,
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
&[2, 3],
vec![0, 1],
);
assert_eq!(got.len(), 1);
assert!((got[0] - 21.0).abs() < 1e-5, "sum got {got:?}");
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_max_full_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_reduce_f32(
&backend,
OpKind::MaxReduce,
&[1.0, 7.0, 3.0, 4.0, 2.0, 6.0],
&[2, 3],
vec![0, 1],
);
assert_eq!(got, vec![7.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_min_full_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_reduce_f32(
&backend,
OpKind::MinReduce,
&[5.0, 2.0, 3.0, -1.0, 4.0, 0.0],
&[2, 3],
vec![0, 1],
);
assert_eq!(got, vec![-1.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_mean_full_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_reduce_f32(
&backend,
OpKind::MeanReduce,
&[2.0, 4.0, 6.0, 8.0],
&[2, 2],
vec![0, 1],
);
assert_eq!(got.len(), 1);
assert!((got[0] - 5.0).abs() < 1e-5, "mean got {got:?}");
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_mean_last_dim_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_reduce_f32(
&backend,
OpKind::MeanReduce,
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
&[2, 3],
vec![1],
);
assert_eq!(got, vec![2.0, 5.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_sum_last_dim_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_reduce_f32(
&backend,
OpKind::SumReduce,
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
&[2, 3],
vec![1],
);
assert_eq!(got, vec![6.0, 15.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_index_select_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let src_data: Vec<f32> = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
];
let ids_data: Vec<u32> = vec![0, 2, 1];
let src_storage = upload_f32(&backend, &src_data);
let ids_bytes: &[u8] = bytemuck::cast_slice(&ids_data);
let ids_vk = backend.upload_bytes_handle(ids_bytes).expect("ids upload");
let ids_storage = Storage::new(BackendStorage::Vulkan(ids_vk), DType::U32);
let outer_count = 1usize; let source_dim_size = 4usize; let n_indices = 3usize; let inner_count = 3usize; let out_n = outer_count * n_indices * inner_count;
let out_bytes = backend.alloc_bytes_handle(out_n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let src_arc = Arc::new(RwLock::new(src_storage));
let ids_arc = Arc::new(RwLock::new(ids_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::IndexSelect,
&[DType::F32, DType::U32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let src_layout = Layout::contiguous(Shape::from_dims(&[4, 3]));
let ids_layout = Layout::contiguous(Shape::from_dims(&[3]));
let out_layout = Layout::contiguous(Shape::from_dims(&[3, 3]));
let layouts = vec![src_layout, ids_layout, out_layout];
kernel(
&[Arc::clone(&src_arc), Arc::clone(&ids_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::IndexSelect {
outer_count,
source_dim_size,
n_indices,
inner_count,
},
)
.expect("index_select dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![1.0, 2.0, 3.0, 7.0, 8.0, 9.0, 4.0, 5.0, 6.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_rope_identity_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 1usize;
let seq = 2usize;
let head_dim = 4usize;
let x_data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let cos_data: Vec<f32> = vec![1.0; seq * head_dim];
let sin_data: Vec<f32> = vec![0.0; seq * head_dim];
let x_storage = upload_f32(&backend, &x_data);
let cos_storage = upload_f32(&backend, &cos_data);
let sin_storage = upload_f32(&backend, &sin_data);
let n = outer * seq * head_dim;
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let x_arc = Arc::new(RwLock::new(x_storage));
let cos_arc = Arc::new(RwLock::new(cos_storage));
let sin_arc = Arc::new(RwLock::new(sin_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Rope,
&[DType::F32, DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let x_layout = Layout::contiguous(Shape::from_dims(&[outer, seq, head_dim]));
let cos_layout = Layout::contiguous(Shape::from_dims(&[seq, head_dim]));
let sin_layout = Layout::contiguous(Shape::from_dims(&[seq, head_dim]));
let out_layout = x_layout.clone();
let layouts = vec![x_layout, cos_layout, sin_layout, out_layout];
kernel(
&[
Arc::clone(&x_arc),
Arc::clone(&cos_arc),
Arc::clone(&sin_arc),
],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Rope {
outer_count: outer,
seq,
head_dim,
},
)
.expect("rope dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, x)) in got.iter().zip(x_data.iter()).enumerate() {
assert!(
(g - x).abs() < 1e-5,
"rope identity mismatch at {i}: got {g}, expected {x}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_rope_quarter_rotation_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 1usize;
let seq = 1usize;
let head_dim = 4usize;
let h = head_dim / 2;
let x_data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0]; let cos_data: Vec<f32> = vec![0.0; seq * head_dim];
let sin_data: Vec<f32> = vec![1.0; seq * head_dim];
let x_storage = upload_f32(&backend, &x_data);
let cos_storage = upload_f32(&backend, &cos_data);
let sin_storage = upload_f32(&backend, &sin_data);
let n = outer * seq * head_dim;
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let x_arc = Arc::new(RwLock::new(x_storage));
let cos_arc = Arc::new(RwLock::new(cos_storage));
let sin_arc = Arc::new(RwLock::new(sin_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Rope,
&[DType::F32, DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let x_layout = Layout::contiguous(Shape::from_dims(&[outer, seq, head_dim]));
let cs_layout = Layout::contiguous(Shape::from_dims(&[seq, head_dim]));
let layouts = vec![x_layout.clone(), cs_layout.clone(), cs_layout, x_layout];
kernel(
&[
Arc::clone(&x_arc),
Arc::clone(&cos_arc),
Arc::clone(&sin_arc),
],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::Rope {
outer_count: outer,
seq,
head_dim,
},
)
.expect("rope dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let _ = h;
let expected: Vec<f32> = vec![-3.0, -4.0, 1.0, 2.0];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"rope π/2 mismatch at {i}: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_rms_norm_last_dim_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let host: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::RmsNormLastDim,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let layouts = vec![layout.clone(), layout];
let eps = 1e-6f64;
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&layouts,
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("rmsnorm dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for row in 0..outer {
let xs = &host[row * last..(row + 1) * last];
let ys = &got[row * last..(row + 1) * last];
let mean_sq: f32 = xs.iter().map(|x| x * x).sum::<f32>() / last as f32;
let scale = (mean_sq + eps as f32).sqrt();
for (i, (x, y)) in xs.iter().zip(ys.iter()).enumerate() {
let expected = x / scale;
assert!(
(y - expected).abs() < 1e-4,
"rmsnorm row {row} col {i}: got {y}, expected {expected}"
);
}
}
}
fn layer_norm_backward_ref(x: &[f32], g: &[f32], outer: usize, last: usize, eps: f32) -> Vec<f32> {
let mut out = vec![0.0_f32; x.len()];
let n = last as f32;
for r in 0..outer {
let off = r * last;
let sum_x: f32 = x[off..off + last].iter().sum();
let sum_x2: f32 = x[off..off + last].iter().map(|&v| v * v).sum();
let sum_g: f32 = g[off..off + last].iter().sum();
let sum_gx: f32 = x[off..off + last]
.iter()
.zip(g[off..off + last].iter())
.map(|(&xi, &gi)| gi * xi)
.sum();
let mu = sum_x / n;
let var = sum_x2 / n - mu * mu;
let rstd = 1.0 / (var + eps).sqrt();
let mean_g = sum_g / n;
let mean_gxc = (sum_gx - sum_g * mu) / n;
let rstd2 = rstd * rstd;
for i in 0..last {
let xi = x[off + i];
let gi = g[off + i];
let xc = xi - mu;
out[off + i] = rstd * (gi - mean_g - xc * rstd2 * mean_gxc);
}
}
out
}
#[test]
#[ignore]
fn vulkan_dispatch_layer_norm_backward_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let x: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let g: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 0.5, 1.5, -0.5, -1.5];
let eps = 1e-5_f64;
let x_storage = upload_f32(&backend, &x);
let g_storage = upload_f32(&backend, &g);
let dx_bytes = backend.alloc_bytes_handle(outer * last * 4).expect("alloc");
let dx_storage = Storage::new(BackendStorage::Vulkan(dx_bytes), DType::F32);
let x_arc = Arc::new(RwLock::new(x_storage));
let g_arc = Arc::new(RwLock::new(g_storage));
let dx_arc = Arc::new(RwLock::new(dx_storage));
let kernel = table.lookup_alternatives(
OpKind::LayerNormLastDimBackward,
&[DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&x_arc), Arc::clone(&g_arc)],
&mut [Arc::clone(&dx_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("ln_bwd f32 dispatch");
let got = download_f32(&backend, &dx_arc.read().unwrap());
let expected = layer_norm_backward_ref(&x, &g, outer, last, eps as f32);
for (i, (a, b)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-4,
"ln_bwd f32[{i}]: got {a}, expected {b}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_layer_norm_backward_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let x_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let g_f32: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 0.5, 1.5, -0.5, -1.5];
let x: Vec<half::f16> = x_f32.iter().map(|&v| half::f16::from_f32(v)).collect();
let g: Vec<half::f16> = g_f32.iter().map(|&v| half::f16::from_f32(v)).collect();
let eps = 1e-5_f64;
let x_storage = upload_f16(&backend, &x);
let g_storage = upload_f16(&backend, &g);
let dx_bytes = backend.alloc_bytes_handle(outer * last * 2).expect("alloc");
let dx_storage = Storage::new(BackendStorage::Vulkan(dx_bytes), DType::F16);
let x_arc = Arc::new(RwLock::new(x_storage));
let g_arc = Arc::new(RwLock::new(g_storage));
let dx_arc = Arc::new(RwLock::new(dx_storage));
let kernel = table.lookup_alternatives(
OpKind::LayerNormLastDimBackward,
&[DType::F16, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&x_arc), Arc::clone(&g_arc)],
&mut [Arc::clone(&dx_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("ln_bwd f16 dispatch");
let got = download_f16(&backend, &dx_arc.read().unwrap());
let expected = layer_norm_backward_ref(&x_f32, &g_f32, outer, last, eps as f32);
for (i, (a, b)) in got.iter().zip(expected.iter()).enumerate() {
let af = a.to_f32();
assert!(
(af - b).abs() < 5e-3,
"ln_bwd f16[{i}]: got {af}, expected {b}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_layer_norm_backward_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize; let x_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let g_f32: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 0.5, 1.5, -0.5, -1.5];
let x: Vec<half::bf16> = x_f32.iter().map(|&v| half::bf16::from_f32(v)).collect();
let g: Vec<half::bf16> = g_f32.iter().map(|&v| half::bf16::from_f32(v)).collect();
let eps = 1e-5_f64;
let x_storage = upload_bf16(&backend, &x);
let g_storage = upload_bf16(&backend, &g);
let dx_bytes = backend.alloc_bytes_handle(outer * last * 2).expect("alloc");
let dx_storage = Storage::new(BackendStorage::Vulkan(dx_bytes), DType::BF16);
let x_arc = Arc::new(RwLock::new(x_storage));
let g_arc = Arc::new(RwLock::new(g_storage));
let dx_arc = Arc::new(RwLock::new(dx_storage));
let kernel = table.lookup_alternatives(
OpKind::LayerNormLastDimBackward,
&[DType::BF16, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&x_arc), Arc::clone(&g_arc)],
&mut [Arc::clone(&dx_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("ln_bwd bf16 dispatch");
let got = download_bf16(&backend, &dx_arc.read().unwrap());
let expected = layer_norm_backward_ref(&x_f32, &g_f32, outer, last, eps as f32);
for (i, (a, b)) in got.iter().zip(expected.iter()).enumerate() {
let af = a.to_f32();
assert!(
(af - b).abs() < 5e-2,
"ln_bwd bf16[{i}]: got {af}, expected {b}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_layer_norm_backward_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let x: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let g: Vec<f64> = vec![1.0, -1.0, 2.0, -2.0, 0.5, 1.5, -0.5, -1.5];
let eps = 1e-12_f64;
let x_storage = upload_f64(&backend, &x);
let g_storage = upload_f64(&backend, &g);
let dx_bytes = backend.alloc_bytes_handle(outer * last * 8).expect("alloc");
let dx_storage = Storage::new(BackendStorage::Vulkan(dx_bytes), DType::F64);
let x_arc = Arc::new(RwLock::new(x_storage));
let g_arc = Arc::new(RwLock::new(g_storage));
let dx_arc = Arc::new(RwLock::new(dx_storage));
let kernel = table.lookup_alternatives(
OpKind::LayerNormLastDimBackward,
&[DType::F64, DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&x_arc), Arc::clone(&g_arc)],
&mut [Arc::clone(&dx_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("ln_bwd f64 dispatch");
let got = download_f64(&backend, &dx_arc.read().unwrap());
let n = last as f64;
for r in 0..outer {
let off = r * last;
let sum_x: f64 = x[off..off + last].iter().sum();
let sum_x2: f64 = x[off..off + last].iter().map(|&v| v * v).sum();
let sum_g: f64 = g[off..off + last].iter().sum();
let sum_gx: f64 = x[off..off + last]
.iter()
.zip(g[off..off + last].iter())
.map(|(&xi, &gi)| gi * xi)
.sum();
let mu = sum_x / n;
let var = sum_x2 / n - mu * mu;
let rstd = 1.0 / (var + eps).sqrt();
let mean_g = sum_g / n;
let mean_gxc = (sum_gx - sum_g * mu) / n;
let rstd2 = rstd * rstd;
for i in 0..last {
let xi = x[off + i];
let gi = g[off + i];
let xc = xi - mu;
let expected = rstd * (gi - mean_g - xc * rstd2 * mean_gxc);
assert!(
(got[off + i] - expected).abs() < 1e-10,
"ln_bwd f64[{}][{}]: got {}, expected {expected}",
r,
i,
got[off + i]
);
}
}
}
fn layer_norm_ref(x: &[f32], outer: usize, last: usize, eps: f32) -> Vec<f32> {
let mut out = vec![0.0_f32; x.len()];
let inv_n = 1.0 / last as f32;
for r in 0..outer {
let off = r * last;
let mean = x[off..off + last].iter().sum::<f32>() * inv_n;
let var = x[off..off + last]
.iter()
.map(|&v| (v - mean).powi(2))
.sum::<f32>()
* inv_n;
let inv_std = 1.0 / (var + eps).sqrt();
for i in 0..last {
out[off + i] = (x[off + i] - mean) * inv_std;
}
}
out
}
#[test]
#[ignore]
fn vulkan_dispatch_layer_norm_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let host: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let eps = 1e-5_f64;
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(outer * last * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::LayerNormLastDim,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("layer_norm f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expected = layer_norm_ref(&host, outer, last, eps as f32);
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"layer_norm f32[{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_layer_norm_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let host_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let host: Vec<half::f16> = host_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let eps = 1e-5_f64;
let in_storage = upload_f16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(outer * last * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::LayerNormLastDim,
&[DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("layer_norm f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
let expected = layer_norm_ref(&host_f32, outer, last, eps as f32);
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
let got_f32 = g.to_f32();
assert!(
(got_f32 - e).abs() < 5e-3,
"layer_norm f16[{i}]: got {got_f32}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_layer_norm_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let host_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let host: Vec<half::bf16> = host_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let eps = 1e-5_f64;
let in_storage = upload_bf16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(outer * last * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::LayerNormLastDim,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("layer_norm bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
let expected = layer_norm_ref(&host_f32, outer, last, eps as f32);
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
let got_f32 = g.to_f32();
assert!(
(got_f32 - e).abs() < 5e-2,
"layer_norm bf16[{i}]: got {got_f32}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_layer_norm_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let host: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let eps = 1e-12_f64;
let in_storage = upload_f64(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(outer * last * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::LayerNormLastDim,
&[DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("layer_norm f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
let inv_n = 1.0 / last as f64;
for r in 0..outer {
let off = r * last;
let mean = host[off..off + last].iter().sum::<f64>() * inv_n;
let var = host[off..off + last]
.iter()
.map(|&v| (v - mean).powi(2))
.sum::<f64>()
* inv_n;
let inv_std = 1.0 / (var + eps).sqrt();
for i in 0..last {
let expected = (host[off + i] - mean) * inv_std;
assert!(
(got[off + i] - expected).abs() < 1e-10,
"layer_norm f64[{}][{}]: got {}, expected {expected}",
r,
i,
got[off + i]
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_gather_f32_dim1() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let src = [1.0_f32, 2.0, 3.0, 10.0, 20.0, 30.0];
let indices: Vec<u32> = vec![
2, 0, 1, 0, 1, 1, 2, 0, ];
let expected = [3.0_f32, 1.0, 2.0, 1.0, 20.0, 20.0, 30.0, 10.0];
let src_storage = upload_f32(&backend, &src);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let out_bytes = backend.alloc_bytes_handle(8 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let src_arc = Arc::new(RwLock::new(src_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Gather,
&[DType::F32, DType::U32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let src_layout = Layout::contiguous(Shape::from_dims(&[2, 3]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[2, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 4]));
kernel(
&[Arc::clone(&src_arc), Arc::clone(&idx_arc)],
&mut [Arc::clone(&out_arc)],
&[src_layout, idx_layout, out_layout],
&OpParams::Gather {
source_shape: vec![2, 3],
output_shape: vec![2, 4],
dim: 1,
},
)
.expect("gather f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "gather f32[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_gather_f64_dim0() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let src = [1.0_f64, 2.0, 10.0, 20.0, 100.0, 200.0];
let indices: Vec<u32> = vec![
2, 0, 1, 2, ];
let expected = [100.0_f64, 2.0, 10.0, 200.0];
let src_storage = upload_f64(&backend, &src);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let out_bytes = backend.alloc_bytes_handle(4 * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let src_arc = Arc::new(RwLock::new(src_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Gather,
&[DType::F64, DType::U32, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let src_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
kernel(
&[Arc::clone(&src_arc), Arc::clone(&idx_arc)],
&mut [Arc::clone(&out_arc)],
&[src_layout, idx_layout, out_layout],
&OpParams::Gather {
source_shape: vec![3, 2],
output_shape: vec![2, 2],
dim: 0,
},
)
.expect("gather f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "gather f64[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_gather_bf16_dim1() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let src_f32 = [1.0_f32, 2.0, 3.0, 4.0, 10.0, 20.0, 30.0, 40.0];
let src: Vec<half::bf16> = src_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let indices: Vec<u32> = vec![3, 0, 1, 2, 2, 1, 0, 3];
let expected_f32 = [4.0_f32, 1.0, 2.0, 3.0, 30.0, 20.0, 10.0, 40.0];
let src_storage = upload_bf16(&backend, &src);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let out_bytes = backend.alloc_bytes_handle(8 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let src_arc = Arc::new(RwLock::new(src_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Gather,
&[DType::BF16, DType::U32, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let src_layout = Layout::contiguous(Shape::from_dims(&[2, 4]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[2, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 4]));
kernel(
&[Arc::clone(&src_arc), Arc::clone(&idx_arc)],
&mut [Arc::clone(&out_arc)],
&[src_layout, idx_layout, out_layout],
&OpParams::Gather {
source_shape: vec![2, 4],
output_shape: vec![2, 4],
dim: 1,
},
)
.expect("gather bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected_f32.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"gather bf16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_masked_fill_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let mask: [u8; 8] = [0, 1, 0, 0, 1, 1, 0, 1]; let fill: f32 = -42.0;
let in_storage = upload_f32(&backend, &input);
let mask_bytes_u: &[u8] = &mask;
let mask_storage = Storage::new(
BackendStorage::Vulkan(
backend
.upload_bytes_handle(mask_bytes_u)
.expect("mask upload"),
),
DType::Bool,
);
let out_bytes = backend.alloc_bytes_handle(8 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let mask_arc = Arc::new(RwLock::new(mask_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MaskedFill,
&[DType::F32, DType::Bool, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[8]));
kernel(
&[Arc::clone(&in_arc), Arc::clone(&mask_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::MaskedFill {
fill_bytes: fill.to_le_bytes().to_vec(),
},
)
.expect("masked_fill f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expected = [1.0_f32, fill, 3.0, 4.0, fill, fill, 7.0, fill];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "masked_fill f32[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_masked_fill_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input_f32 = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let input: Vec<half::bf16> = input_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let mask: [u8; 8] = [0, 1, 0, 0, 1, 1, 0, 1];
let fill = half::bf16::from_f32(-1.0);
let in_storage = upload_bf16(&backend, &input);
let mask_bytes_u: &[u8] = &mask;
let mask_storage = Storage::new(
BackendStorage::Vulkan(
backend
.upload_bytes_handle(mask_bytes_u)
.expect("mask upload"),
),
DType::Bool,
);
let out_bytes = backend.alloc_bytes_handle(8 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let mask_arc = Arc::new(RwLock::new(mask_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MaskedFill,
&[DType::BF16, DType::Bool, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[8]));
kernel(
&[Arc::clone(&in_arc), Arc::clone(&mask_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::MaskedFill {
fill_bytes: fill.to_le_bytes().to_vec(),
},
)
.expect("masked_fill bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
let expected_f32 = [1.0_f32, -1.0, 3.0, 4.0, -1.0, -1.0, 7.0, -1.0];
for (i, (g, e)) in got.iter().zip(expected_f32.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"masked_fill bf16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_masked_fill_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [1.0_f64, 2.0, 3.0, 4.0];
let mask: [u8; 4] = [1, 0, 1, 0];
let fill = -99.5_f64;
let in_storage = upload_f64(&backend, &input);
let mask_bytes_u: &[u8] = &mask;
let mask_storage = Storage::new(
BackendStorage::Vulkan(
backend
.upload_bytes_handle(mask_bytes_u)
.expect("mask upload"),
),
DType::Bool,
);
let out_bytes = backend.alloc_bytes_handle(4 * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let mask_arc = Arc::new(RwLock::new(mask_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MaskedFill,
&[DType::F64, DType::Bool, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[4]));
kernel(
&[Arc::clone(&in_arc), Arc::clone(&mask_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::MaskedFill {
fill_bytes: fill.to_le_bytes().to_vec(),
},
)
.expect("masked_fill f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
let expected = [fill, 2.0, fill, 4.0];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "masked_fill f64[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_index_add_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [0.0_f32; 6];
let indices: Vec<u32> = vec![0, 2, 0];
let src = [1.0_f32, 2., 3., 4., 5., 6.];
let expected = [6.0_f32, 8., 0., 0., 3., 4.];
let base_storage = upload_f32(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f32(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(6 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::IndexAdd,
&[DType::F32, DType::U32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[3]));
let src_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::IndexAdd {
outer_count: 1,
base_dim_size: 3,
n_indices: 3,
inner_count: 2,
},
)
.expect("index_add f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "index_add f32[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_index_add_f32_starts_from_base() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [10.0_f32, 20., 30., 40.];
let indices: Vec<u32> = vec![0];
let src = [100.0_f32, 200.];
let expected = [110.0_f32, 220., 30., 40.];
let base_storage = upload_f32(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f32(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(4 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::IndexAdd,
&[DType::F32, DType::U32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[1]));
let src_layout = Layout::contiguous(Shape::from_dims(&[1, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::IndexAdd {
outer_count: 1,
base_dim_size: 2,
n_indices: 1,
inner_count: 2,
},
)
.expect("index_add f32 base-init dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
*g, *e,
"index_add f32 base-init[{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_index_add_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [0.0_f64; 6];
let indices: Vec<u32> = vec![0, 2, 0];
let src = [1.0_f64, 2., 3., 4., 5., 6.];
let expected = [6.0_f64, 8., 0., 0., 3., 4.];
let base_storage = upload_f64(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f64(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(6 * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::IndexAdd,
&[DType::F64, DType::U32, DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[3]));
let src_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::IndexAdd {
outer_count: 1,
base_dim_size: 3,
n_indices: 3,
inner_count: 2,
},
)
.expect("index_add f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "index_add f64[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_index_add_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base: Vec<half::bf16> = vec![half::bf16::ZERO; 6];
let indices: Vec<u32> = vec![0, 2, 0];
let src_f32 = [1.0_f32, 2., 3., 4., 5., 6.];
let src: Vec<half::bf16> = src_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let expected = [6.0_f32, 8., 0., 0., 3., 4.];
let base_storage = upload_bf16(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_bf16(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(6 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::IndexAdd,
&[DType::BF16, DType::U32, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[3]));
let src_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::IndexAdd {
outer_count: 1,
base_dim_size: 3,
n_indices: 3,
inner_count: 2,
},
)
.expect("index_add bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"index_add bf16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_index_add_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base: Vec<half::f16> = vec![half::f16::ZERO; 6];
let indices: Vec<u32> = vec![0, 2, 0];
let src_f32 = [1.0_f32, 2., 3., 4., 5., 6.];
let src: Vec<half::f16> = src_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let expected = [6.0_f32, 8., 0., 0., 3., 4.];
let base_storage = upload_f16(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f16(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(6 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::IndexAdd,
&[DType::F16, DType::U32, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[3]));
let src_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::IndexAdd {
outer_count: 1,
base_dim_size: 3,
n_indices: 3,
inner_count: 2,
},
)
.expect("index_add f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"index_add f16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_scatter_add_f32_dim0() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [0.0_f32, 0., 0., 0., 0., 0.];
let indices: Vec<u32> = vec![0, 1, 2, 0];
let src = [1.0_f32, 2., 3., 4.];
let expected = [1.0_f32, 4., 0., 2., 3., 0.];
let base_storage = upload_f32(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f32(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(6 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ScatterAdd,
&[DType::F32, DType::U32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let src_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::ScatterAdd {
base_shape: vec![3, 2],
src_shape: vec![2, 2],
dim: 0,
},
)
.expect("scatter_add f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "scatter_add f32[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_scatter_add_f32_starts_from_base() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [10.0_f32, 20., 30., 40.]; let indices: Vec<u32> = vec![0, 1]; let src = [100.0_f32, 200.]; let expected = [110.0_f32, 20., 30., 240.];
let base_storage = upload_f32(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f32(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(4 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ScatterAdd,
&[DType::F32, DType::U32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[1, 2]));
let src_layout = Layout::contiguous(Shape::from_dims(&[1, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::ScatterAdd {
base_shape: vec![2, 2],
src_shape: vec![1, 2],
dim: 0,
},
)
.expect("scatter_add f32 dispatch (base-init test)");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
*g, *e,
"scatter_add f32 base-init[{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_scatter_add_f64_dim0() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [0.0_f64, 0., 0., 0., 0., 0.];
let indices: Vec<u32> = vec![0, 1, 2, 0];
let src = [1.0_f64, 2., 3., 4.];
let expected = [1.0_f64, 4., 0., 2., 3., 0.];
let base_storage = upload_f64(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f64(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(6 * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ScatterAdd,
&[DType::F64, DType::U32, DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let src_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::ScatterAdd {
base_shape: vec![3, 2],
src_shape: vec![2, 2],
dim: 0,
},
)
.expect("scatter_add f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "scatter_add f64[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_scatter_add_f64_starts_from_base() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [10.0_f64, 20., 30., 40.];
let indices: Vec<u32> = vec![0, 1];
let src = [100.0_f64, 200.];
let expected = [110.0_f64, 20., 30., 240.];
let base_storage = upload_f64(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f64(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(4 * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ScatterAdd,
&[DType::F64, DType::U32, DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[1, 2]));
let src_layout = Layout::contiguous(Shape::from_dims(&[1, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::ScatterAdd {
base_shape: vec![2, 2],
src_shape: vec![1, 2],
dim: 0,
},
)
.expect("scatter_add f64 dispatch (base-init test)");
let got = download_f64(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
*g, *e,
"scatter_add f64 base-init[{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_scatter_add_bf16_dim0() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [
half::bf16::ZERO,
half::bf16::ZERO,
half::bf16::ZERO,
half::bf16::ZERO,
half::bf16::ZERO,
half::bf16::ZERO,
];
let indices: Vec<u32> = vec![0, 1, 2, 0];
let src = [
half::bf16::from_f32(1.0),
half::bf16::from_f32(2.0),
half::bf16::from_f32(3.0),
half::bf16::from_f32(4.0),
];
let expected = [1.0_f32, 4., 0., 2., 3., 0.];
let base_storage = upload_bf16(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_bf16(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(6 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ScatterAdd,
&[DType::BF16, DType::U32, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let src_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::ScatterAdd {
base_shape: vec![3, 2],
src_shape: vec![2, 2],
dim: 0,
},
)
.expect("scatter_add bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"scatter_add bf16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_scatter_add_bf16_starts_from_base() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [
half::bf16::from_f32(10.0),
half::bf16::from_f32(20.0),
half::bf16::from_f32(30.0),
half::bf16::from_f32(40.0),
];
let indices: Vec<u32> = vec![0, 1];
let src = [half::bf16::from_f32(100.0), half::bf16::from_f32(200.0)];
let expected = [110.0_f32, 20., 30., 240.];
let base_storage = upload_bf16(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_bf16(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(4 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ScatterAdd,
&[DType::BF16, DType::U32, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[1, 2]));
let src_layout = Layout::contiguous(Shape::from_dims(&[1, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::ScatterAdd {
base_shape: vec![2, 2],
src_shape: vec![1, 2],
dim: 0,
},
)
.expect("scatter_add bf16 dispatch (base-init)");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"scatter_add bf16 base-init[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_scatter_add_f16_dim0() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [
half::f16::from_f32(0.0),
half::f16::from_f32(0.0),
half::f16::from_f32(0.0),
half::f16::from_f32(0.0),
half::f16::from_f32(0.0),
half::f16::from_f32(0.0),
];
let indices: Vec<u32> = vec![0, 1, 2, 0];
let src = [
half::f16::from_f32(1.0),
half::f16::from_f32(2.0),
half::f16::from_f32(3.0),
half::f16::from_f32(4.0),
];
let expected = [1.0_f32, 4., 0., 2., 3., 0.];
let base_storage = upload_f16(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f16(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(6 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ScatterAdd,
&[DType::F16, DType::U32, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let src_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[3, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::ScatterAdd {
base_shape: vec![3, 2],
src_shape: vec![2, 2],
dim: 0,
},
)
.expect("scatter_add f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"scatter_add f16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_scatter_add_f16_starts_from_base() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let base = [
half::f16::from_f32(10.0),
half::f16::from_f32(20.0),
half::f16::from_f32(30.0),
half::f16::from_f32(40.0),
];
let indices: Vec<u32> = vec![0, 1];
let src = [half::f16::from_f32(100.0), half::f16::from_f32(200.0)];
let expected = [110.0_f32, 20., 30., 240.];
let base_storage = upload_f16(&backend, &base);
let idx_bytes: &[u8] = bytemuck::cast_slice(&indices);
let idx_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(idx_bytes).expect("idx upload")),
DType::U32,
);
let src_storage = upload_f16(&backend, &src);
let out_bytes = backend.alloc_bytes_handle(4 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let base_arc = Arc::new(RwLock::new(base_storage));
let idx_arc = Arc::new(RwLock::new(idx_storage));
let src_arc = Arc::new(RwLock::new(src_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ScatterAdd,
&[DType::F16, DType::U32, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let base_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let idx_layout = Layout::contiguous(Shape::from_dims(&[1, 2]));
let src_layout = Layout::contiguous(Shape::from_dims(&[1, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
kernel(
&[
Arc::clone(&base_arc),
Arc::clone(&idx_arc),
Arc::clone(&src_arc),
],
&mut [Arc::clone(&out_arc)],
&[base_layout, idx_layout, src_layout, out_layout],
&OpParams::ScatterAdd {
base_shape: vec![2, 2],
src_shape: vec![1, 2],
dim: 0,
},
)
.expect("scatter_add f16 dispatch (base-init)");
let got = download_f16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"scatter_add f16 base-init[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_argmax_last_dim_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let input = [1.0_f32, 3.0, 2.0, 0.5, 9.0, 4.0, 9.0, 7.0];
let expected: [u32; 2] = [1, 0];
let in_storage = upload_f32(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(outer * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::U32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ArgMaxDim,
&[DType::F32, DType::U32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let out_layout = Layout::contiguous(Shape::from_dims(&[outer]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout, out_layout],
&OpParams::Reduce {
dims: vec![1],
keepdim: false,
},
)
.expect("argmax f32 dispatch");
let bytes = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<u32> = bytemuck::cast_slice::<u8, u32>(&bytes).to_vec();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "argmax f32[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_argmin_last_dim_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize; let input_f32 = [5.0_f32, -1.0, 3.0, 0.0, 2.0, 4.0, -3.0, 1.0];
let input: Vec<half::bf16> = input_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let expected: [u32; 2] = [1, 2];
let in_storage = upload_bf16(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(outer * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::U32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ArgMinDim,
&[DType::BF16, DType::U32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let out_layout = Layout::contiguous(Shape::from_dims(&[outer]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout, out_layout],
&OpParams::Reduce {
dims: vec![1],
keepdim: false,
},
)
.expect("argmin bf16 dispatch");
let bytes = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<u32> = bytemuck::cast_slice::<u8, u32>(&bytes).to_vec();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "argmin bf16[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_argmax_last_dim_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 1usize;
let last = 5usize;
let input = [1.0_f64, 10.0, 5.0, 10.0, 7.0]; let expected: [u32; 1] = [1];
let in_storage = upload_f64(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(outer * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::U32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ArgMaxDim,
&[DType::F64, DType::U32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let out_layout = Layout::contiguous(Shape::from_dims(&[outer]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout, out_layout],
&OpParams::Reduce {
dims: vec![1],
keepdim: false,
},
)
.expect("argmax f64 dispatch");
let bytes = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<u32> = bytemuck::cast_slice::<u8, u32>(&bytes).to_vec();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "argmax f64[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_argmax_any_dim_f32_dim0() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [1.0_f32, 7., 2., 3., 5., 3., 6., 0., 4., 7., 9., 2.];
let expected: [u32; 4] = [1, 0, 2, 0];
let in_storage = upload_f32(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(4 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::U32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ArgMaxDim,
&[DType::F32, DType::U32],
BackendId::Vulkan,
)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[3, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[4]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Reduce {
dims: vec![0],
keepdim: false,
},
)
.expect("argmax f32 dim0 dispatch");
let bytes = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<u32> = bytemuck::cast_slice::<u8, u32>(&bytes).to_vec();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "argmax f32 dim0[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_argmin_any_dim_f32_middle() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [
5.0_f32, 1., 4., 9., 2., 8., 3., 0., 7., 6., 4., 1., 1., 2., 3., 4., 4., 3., 2., 1., 2., 2., 2., 2.,
];
let expected: [u32; 8] = [1, 0, 1, 1, 0, 0, 1, 1];
let in_storage = upload_f32(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(8 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::U32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ArgMinDim,
&[DType::F32, DType::U32],
BackendId::Vulkan,
)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[2, 3, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 4]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Reduce {
dims: vec![1],
keepdim: false,
},
)
.expect("argmin f32 middle dispatch");
let bytes = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<u32> = bytemuck::cast_slice::<u8, u32>(&bytes).to_vec();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "argmin f32 middle[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_argmax_any_dim_bf16_dim0() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input_f32 = [1.0_f32, 7., 2., 3., 5., 3., 6., 0., 4., 7., 9., 2.];
let input: Vec<half::bf16> = input_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let expected: [u32; 4] = [1, 0, 2, 0];
let in_storage = upload_bf16(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(4 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::U32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ArgMaxDim,
&[DType::BF16, DType::U32],
BackendId::Vulkan,
)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[3, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[4]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Reduce {
dims: vec![0],
keepdim: false,
},
)
.expect("argmax bf16 dim0 dispatch");
let bytes = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<u32> = bytemuck::cast_slice::<u8, u32>(&bytes).to_vec();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "argmax bf16 dim0[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_argmin_any_dim_f16_dim0() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input_f32 = [5.0_f32, -1., 3., 0., 2., 4., -3., 1., 7., 6., 4., 1.];
let input: Vec<half::f16> = input_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let expected: [u32; 4] = [1, 0, 1, 0];
let in_storage = upload_f16(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(4 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::U32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ArgMinDim,
&[DType::F16, DType::U32],
BackendId::Vulkan,
)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[3, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[4]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Reduce {
dims: vec![0],
keepdim: false,
},
)
.expect("argmin f16 dim0 dispatch");
let bytes = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<u32> = bytemuck::cast_slice::<u8, u32>(&bytes).to_vec();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "argmin f16 dim0[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_argmax_any_dim_f64_dim0() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [1.0_f64, 7., 2., 3., 5., 3., 6., 0., 4., 7., 9., 2.];
let expected: [u32; 4] = [1, 0, 2, 0];
let in_storage = upload_f64(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(4 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::U32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::ArgMaxDim,
&[DType::F64, DType::U32],
BackendId::Vulkan,
)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[3, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[4]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Reduce {
dims: vec![0],
keepdim: false,
},
)
.expect("argmax f64 dim0 dispatch");
let bytes = match &out_arc.read().unwrap().inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
let got: Vec<u32> = bytemuck::cast_slice::<u8, u32>(&bytes).to_vec();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "argmax f64 dim0[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_backward_reflect_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let grad_out: Vec<f32> = vec![10., 20., 30., 40., 50., 60., 70.];
let expected: [f32; 3] = [100., 120., 60.];
let go_storage = upload_f32(&backend, &grad_out);
let gi_bytes = backend.alloc_bytes_handle(3 * 4).expect("alloc");
let gi_storage = Storage::new(BackendStorage::Vulkan(gi_bytes), DType::F32);
let go_arc = Arc::new(RwLock::new(go_storage));
let gi_arc = Arc::new(RwLock::new(gi_storage));
let kernel = table.lookup_alternatives(
OpKind::PadBackward,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let go_layout = Layout::contiguous(Shape::from_dims(&[7]));
let gi_layout = Layout::contiguous(Shape::from_dims(&[3]));
kernel(
&[Arc::clone(&go_arc)],
&mut [Arc::clone(&gi_arc)],
&[go_layout, gi_layout],
&OpParams::PadBackward {
in_shape: vec![3],
out_shape: vec![7],
padding: vec![(2, 2)],
mode_tag: 1,
},
)
.expect("pad_backward reflect f32 dispatch");
let got = download_f32(&backend, &gi_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"pad_backward reflect[{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_backward_replicate_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let grad_out: Vec<f32> = vec![10., 20., 30., 40., 50., 60., 70., 80.];
let expected: [f32; 3] = [60., 40., 260.];
let go_storage = upload_f32(&backend, &grad_out);
let gi_bytes = backend.alloc_bytes_handle(3 * 4).expect("alloc");
let gi_storage = Storage::new(BackendStorage::Vulkan(gi_bytes), DType::F32);
let go_arc = Arc::new(RwLock::new(go_storage));
let gi_arc = Arc::new(RwLock::new(gi_storage));
let kernel = table.lookup_alternatives(
OpKind::PadBackward,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let go_layout = Layout::contiguous(Shape::from_dims(&[8]));
let gi_layout = Layout::contiguous(Shape::from_dims(&[3]));
kernel(
&[Arc::clone(&go_arc)],
&mut [Arc::clone(&gi_arc)],
&[go_layout, gi_layout],
&OpParams::PadBackward {
in_shape: vec![3],
out_shape: vec![8],
padding: vec![(2, 3)],
mode_tag: 2,
},
)
.expect("pad_backward replicate f32 dispatch");
let got = download_f32(&backend, &gi_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-5,
"pad_backward replicate[{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_backward_reflect_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let grad_out: Vec<f64> = vec![10., 20., 30., 40., 50., 60., 70.];
let expected: [f64; 3] = [100., 120., 60.];
let go_storage = upload_f64(&backend, &grad_out);
let gi_bytes = backend.alloc_bytes_handle(3 * 8).expect("alloc");
let gi_storage = Storage::new(BackendStorage::Vulkan(gi_bytes), DType::F64);
let go_arc = Arc::new(RwLock::new(go_storage));
let gi_arc = Arc::new(RwLock::new(gi_storage));
let kernel = table.lookup_alternatives(
OpKind::PadBackward,
&[DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let go_layout = Layout::contiguous(Shape::from_dims(&[7]));
let gi_layout = Layout::contiguous(Shape::from_dims(&[3]));
kernel(
&[Arc::clone(&go_arc)],
&mut [Arc::clone(&gi_arc)],
&[go_layout, gi_layout],
&OpParams::PadBackward {
in_shape: vec![3],
out_shape: vec![7],
padding: vec![(2, 2)],
mode_tag: 1,
},
)
.expect("pad_backward reflect f64 dispatch");
let got = download_f64(&backend, &gi_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-10,
"pad_backward reflect f64[{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_backward_replicate_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let grad_out: Vec<f64> = vec![10., 20., 30., 40., 50., 60., 70., 80.];
let expected: [f64; 3] = [60., 40., 260.];
let go_storage = upload_f64(&backend, &grad_out);
let gi_bytes = backend.alloc_bytes_handle(3 * 8).expect("alloc");
let gi_storage = Storage::new(BackendStorage::Vulkan(gi_bytes), DType::F64);
let go_arc = Arc::new(RwLock::new(go_storage));
let gi_arc = Arc::new(RwLock::new(gi_storage));
let kernel = table.lookup_alternatives(
OpKind::PadBackward,
&[DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let go_layout = Layout::contiguous(Shape::from_dims(&[8]));
let gi_layout = Layout::contiguous(Shape::from_dims(&[3]));
kernel(
&[Arc::clone(&go_arc)],
&mut [Arc::clone(&gi_arc)],
&[go_layout, gi_layout],
&OpParams::PadBackward {
in_shape: vec![3],
out_shape: vec![8],
padding: vec![(2, 3)],
mode_tag: 2,
},
)
.expect("pad_backward replicate f64 dispatch");
let got = download_f64(&backend, &gi_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-10,
"pad_backward replicate f64[{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_backward_reflect_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let grad_out_f32: [f32; 7] = [10., 20., 30., 40., 50., 60., 70.];
let grad_out: Vec<half::bf16> = grad_out_f32
.iter()
.map(|&x| half::bf16::from_f32(x))
.collect();
let expected: [f32; 3] = [100., 120., 60.];
let go_storage = upload_bf16(&backend, &grad_out);
let gi_bytes = backend
.alloc_bytes_handle(((3 * 2 + 3) & !3) as usize)
.expect("alloc");
let gi_storage = Storage::new(BackendStorage::Vulkan(gi_bytes), DType::BF16);
let go_arc = Arc::new(RwLock::new(go_storage));
let gi_arc = Arc::new(RwLock::new(gi_storage));
let kernel = table.lookup_alternatives(
OpKind::PadBackward,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let go_layout = Layout::contiguous(Shape::from_dims(&[7]));
let gi_layout = Layout::contiguous(Shape::from_dims(&[3]));
kernel(
&[Arc::clone(&go_arc)],
&mut [Arc::clone(&gi_arc)],
&[go_layout, gi_layout],
&OpParams::PadBackward {
in_shape: vec![3],
out_shape: vec![7],
padding: vec![(2, 2)],
mode_tag: 1,
},
)
.expect("pad_backward reflect bf16 dispatch");
let got = download_bf16(&backend, &gi_arc.read().unwrap());
for (i, (g, e)) in got.iter().take(3).zip(expected.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"pad_backward reflect bf16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_backward_replicate_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let grad_out_f32: [f32; 8] = [10., 20., 30., 40., 50., 60., 70., 80.];
let grad_out: Vec<half::f16> = grad_out_f32
.iter()
.map(|&x| half::f16::from_f32(x))
.collect();
let expected: [f32; 3] = [60., 40., 260.];
let go_storage = upload_f16(&backend, &grad_out);
let gi_bytes = backend
.alloc_bytes_handle(((3 * 2 + 3) & !3) as usize)
.expect("alloc");
let gi_storage = Storage::new(BackendStorage::Vulkan(gi_bytes), DType::F16);
let go_arc = Arc::new(RwLock::new(go_storage));
let gi_arc = Arc::new(RwLock::new(gi_storage));
let kernel = table.lookup_alternatives(
OpKind::PadBackward,
&[DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let go_layout = Layout::contiguous(Shape::from_dims(&[8]));
let gi_layout = Layout::contiguous(Shape::from_dims(&[3]));
kernel(
&[Arc::clone(&go_arc)],
&mut [Arc::clone(&gi_arc)],
&[go_layout, gi_layout],
&OpParams::PadBackward {
in_shape: vec![3],
out_shape: vec![8],
padding: vec![(2, 3)],
mode_tag: 2,
},
)
.expect("pad_backward replicate f16 dispatch");
let got = download_f16(&backend, &gi_arc.read().unwrap());
for (i, (g, e)) in got.iter().take(3).zip(expected.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"pad_backward replicate f16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_backward_const_f32_2d() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let grad_out: Vec<f32> = (0..20).map(|i| i as f32).collect();
let expected = [5.0_f32, 6.0, 7., 10., 11., 12.];
let go_storage = upload_f32(&backend, &grad_out);
let gi_bytes = backend.alloc_bytes_handle(2 * 3 * 4).expect("alloc");
let gi_storage = Storage::new(BackendStorage::Vulkan(gi_bytes), DType::F32);
let go_arc = Arc::new(RwLock::new(go_storage));
let gi_arc = Arc::new(RwLock::new(gi_storage));
let kernel = table.lookup_alternatives(
OpKind::PadBackward,
&[DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let go_layout = Layout::contiguous(Shape::from_dims(&[4, 5]));
let gi_layout = Layout::contiguous(Shape::from_dims(&[2, 3]));
kernel(
&[Arc::clone(&go_arc)],
&mut [Arc::clone(&gi_arc)],
&[go_layout, gi_layout],
&OpParams::PadBackward {
in_shape: vec![2, 3],
out_shape: vec![4, 5],
padding: vec![(1, 1), (0, 2)],
mode_tag: 0,
},
)
.expect("pad_backward const f32 dispatch");
let got = download_f32(&backend, &gi_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "pad_backward const f32[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_backward_const_bf16_1d() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let go_f32 = [10.0_f32, 20., 30., 40., 50., 60., 70., 80.];
let go: Vec<half::bf16> = go_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let expected_f32 = [30.0_f32, 40., 50., 60.];
let go_storage = upload_bf16(&backend, &go);
let gi_bytes = backend.alloc_bytes_handle(4 * 2).expect("alloc");
let gi_storage = Storage::new(BackendStorage::Vulkan(gi_bytes), DType::BF16);
let go_arc = Arc::new(RwLock::new(go_storage));
let gi_arc = Arc::new(RwLock::new(gi_storage));
let kernel = table.lookup_alternatives(
OpKind::PadBackward,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let go_layout = Layout::contiguous(Shape::from_dims(&[8]));
let gi_layout = Layout::contiguous(Shape::from_dims(&[4]));
kernel(
&[Arc::clone(&go_arc)],
&mut [Arc::clone(&gi_arc)],
&[go_layout, gi_layout],
&OpParams::PadBackward {
in_shape: vec![4],
out_shape: vec![8],
padding: vec![(2, 2)],
mode_tag: 0,
},
)
.expect("pad_backward const bf16 dispatch");
let got = download_bf16(&backend, &gi_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected_f32.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"pad_backward const bf16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_replicate_f32_1d() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [1.0_f32, 2.0, 3.0];
let expected = [1.0_f32, 1.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0];
let in_storage = upload_f32(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(8 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Pad, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[3]));
let out_layout = Layout::contiguous(Shape::from_dims(&[8]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Pad {
in_shape: vec![3],
out_shape: vec![8],
padding: vec![(2, 3)],
mode_tag: 2,
fill_bytes: vec![0u8; 4],
},
)
.expect("pad replicate f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "pad replicate f32[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_replicate_f16_2d() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input_f32 = [1.0_f32, 2.0, 3.0, 4.0];
let input: Vec<half::f16> = input_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let in_2d = |r: usize, c: usize| input_f32[r * 2 + c];
let row_map = [0_usize, 0, 1, 1];
let col_map = [0_usize, 0, 1, 1];
let mut expected = [0.0_f32; 4 * 4];
for r in 0..4 {
for c in 0..4 {
expected[r * 4 + c] = in_2d(row_map[r], col_map[c]);
}
}
let in_storage = upload_f16(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(4 * 4 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Pad, &[DType::F16, DType::F16], BackendId::Vulkan)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let out_layout = Layout::contiguous(Shape::from_dims(&[4, 4]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Pad {
in_shape: vec![2, 2],
out_shape: vec![4, 4],
padding: vec![(1, 1), (1, 1)],
mode_tag: 2,
fill_bytes: vec![0u8; 2],
},
)
.expect("pad replicate f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"pad replicate f16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_reflect_f32_1d() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [1.0_f32, 2.0, 3.0];
let expected = [3.0_f32, 2.0, 1.0, 2.0, 3.0, 2.0, 1.0];
let in_storage = upload_f32(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(7 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Pad, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[3]));
let out_layout = Layout::contiguous(Shape::from_dims(&[7]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Pad {
in_shape: vec![3],
out_shape: vec![7],
padding: vec![(2, 2)],
mode_tag: 1,
fill_bytes: vec![0u8; 4], },
)
.expect("pad reflect f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "pad reflect f32[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_reflect_bf16_2d() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input_f32 = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0];
let input: Vec<half::bf16> = input_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let in_2d = |r: usize, c: usize| input_f32[r * 3 + c];
let mut expected = [0.0_f32; 4 * 5];
let row_map = [1usize, 0, 1, 0];
let col_map = [1usize, 0, 1, 2, 1];
for r in 0..4 {
for c in 0..5 {
expected[r * 5 + c] = in_2d(row_map[r], col_map[c]);
}
}
let in_storage = upload_bf16(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(4 * 5 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Pad, &[DType::BF16, DType::BF16], BackendId::Vulkan)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[2, 3]));
let out_layout = Layout::contiguous(Shape::from_dims(&[4, 5]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Pad {
in_shape: vec![2, 3],
out_shape: vec![4, 5],
padding: vec![(1, 1), (1, 1)],
mode_tag: 1,
fill_bytes: vec![0u8; 2],
},
)
.expect("pad reflect bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
let got_f32 = g.to_f32();
assert_eq!(
got_f32, *e,
"pad reflect bf16[{i}]: got {got_f32}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_reflect_f64_1d() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [1.0_f64, 2.0, 3.0, 4.0];
let expected = [2.0_f64, 1.0, 2.0, 3.0, 4.0, 3.0, 2.0];
let in_storage = upload_f64(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(7 * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Pad, &[DType::F64, DType::F64], BackendId::Vulkan)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[7]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Pad {
in_shape: vec![4],
out_shape: vec![7],
padding: vec![(1, 2)],
mode_tag: 1,
fill_bytes: vec![0u8; 8],
},
)
.expect("pad reflect f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "pad reflect f64[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_const_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0];
let fill: f32 = -7.5;
let in_storage = upload_f32(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(4 * 5 * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Pad, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[2, 3]));
let out_layout = Layout::contiguous(Shape::from_dims(&[4, 5]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Pad {
in_shape: vec![2, 3],
out_shape: vec![4, 5],
padding: vec![(1, 1), (0, 2)],
mode_tag: 0,
fill_bytes: fill.to_le_bytes().to_vec(),
},
)
.expect("pad f32 dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expected = [
fill, fill, fill, fill, fill, 1.0, 2.0, 3.0, fill, fill, 4.0, 5.0, 6.0, fill, fill, fill,
fill, fill, fill, fill,
];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "pad f32[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_const_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input_f32 = [1.0_f32, 2.0, 3.0, 4.0];
let input: Vec<half::f16> = input_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let fill = half::f16::from_f32(0.0);
let in_storage = upload_f16(&backend, &input);
#[allow(clippy::identity_op)]
let out_bytes = backend.alloc_bytes_handle(1 * 6 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Pad, &[DType::F16, DType::F16], BackendId::Vulkan)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[1, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[1, 6]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Pad {
in_shape: vec![1, 4],
out_shape: vec![1, 6],
padding: vec![(0, 0), (1, 1)],
mode_tag: 0,
fill_bytes: fill.to_le_bytes().to_vec(),
},
)
.expect("pad f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
let expected = [0.0_f32, 1.0, 2.0, 3.0, 4.0, 0.0];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
let got_f32 = g.to_f32();
assert_eq!(got_f32, *e, "pad f16[{i}]: got {got_f32}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_pad_const_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let input = [1.0_f64, 2.0, 3.0];
let fill = -1.0_f64;
let in_storage = upload_f64(&backend, &input);
let out_bytes = backend.alloc_bytes_handle(8 * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Pad, &[DType::F64, DType::F64], BackendId::Vulkan)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[3]));
let out_layout = Layout::contiguous(Shape::from_dims(&[8]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, out_layout],
&OpParams::Pad {
in_shape: vec![3],
out_shape: vec![8],
padding: vec![(2, 3)],
mode_tag: 0,
fill_bytes: fill.to_le_bytes().to_vec(),
},
)
.expect("pad f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
let expected = [-1.0_f64, -1.0, 1.0, 2.0, 3.0, -1.0, -1.0, -1.0];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "pad f64[{i}]: got {g}, expected {e}");
}
}
fn softmax_backward_ref(y: &[f32], g: &[f32], outer: usize, last: usize) -> Vec<f32> {
let mut out = vec![0.0_f32; y.len()];
for r in 0..outer {
let off = r * last;
let dot: f32 = (0..last).map(|i| y[off + i] * g[off + i]).sum();
for i in 0..last {
out[off + i] = y[off + i] * (g[off + i] - dot);
}
}
out
}
#[test]
#[ignore]
fn vulkan_dispatch_softmax_last_dim_backward_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let y: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4, 0.4, 0.3, 0.2, 0.1];
let g: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 0.5, 1.5, -0.5, -1.5];
let y_storage = upload_f32(&backend, &y);
let g_storage = upload_f32(&backend, &g);
let dx_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let dx_storage = Storage::new(BackendStorage::Vulkan(dx_bytes), DType::F32);
let y_arc = Arc::new(RwLock::new(y_storage));
let g_arc = Arc::new(RwLock::new(g_storage));
let dx_arc = Arc::new(RwLock::new(dx_storage));
let kernel = table.lookup_alternatives(
OpKind::SoftmaxLastDimBackward,
&[DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&y_arc), Arc::clone(&g_arc)],
&mut [Arc::clone(&dx_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::SoftmaxLastDim {
outer_count: outer,
last_dim: last,
},
)
.expect("softmax_backward f32 dispatch");
let got = download_f32(&backend, &dx_arc.read().unwrap());
let expected = softmax_backward_ref(&y, &g, outer, last);
for (i, (a, b)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(a - b).abs() < 1e-5,
"softmax_bwd f32[{i}]: got {a}, expected {b}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_softmax_last_dim_backward_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let y_f32: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4, 0.4, 0.3, 0.2, 0.1];
let g_f32: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 0.5, 1.5, -0.5, -1.5];
let y: Vec<half::f16> = y_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let g: Vec<half::f16> = g_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let y_storage = upload_f16(&backend, &y);
let g_storage = upload_f16(&backend, &g);
let dx_bytes = backend.alloc_bytes_handle(n * 2).expect("alloc");
let dx_storage = Storage::new(BackendStorage::Vulkan(dx_bytes), DType::F16);
let y_arc = Arc::new(RwLock::new(y_storage));
let g_arc = Arc::new(RwLock::new(g_storage));
let dx_arc = Arc::new(RwLock::new(dx_storage));
let kernel = table.lookup_alternatives(
OpKind::SoftmaxLastDimBackward,
&[DType::F16, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&y_arc), Arc::clone(&g_arc)],
&mut [Arc::clone(&dx_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::SoftmaxLastDim {
outer_count: outer,
last_dim: last,
},
)
.expect("softmax_backward f16 dispatch");
let got = download_f16(&backend, &dx_arc.read().unwrap());
let expected = softmax_backward_ref(&y_f32, &g_f32, outer, last);
for (i, (a, b)) in got.iter().zip(expected.iter()).enumerate() {
let got_f32 = a.to_f32();
assert!(
(got_f32 - b).abs() < 5e-3,
"softmax_bwd f16[{i}]: got {got_f32}, expected {b}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_softmax_last_dim_backward_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize; let n = outer * last;
let y_f32: Vec<f32> = vec![0.1, 0.2, 0.3, 0.4, 0.4, 0.3, 0.2, 0.1];
let g_f32: Vec<f32> = vec![1.0, -1.0, 2.0, -2.0, 0.5, 1.5, -0.5, -1.5];
let y: Vec<half::bf16> = y_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let g: Vec<half::bf16> = g_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let y_storage = upload_bf16(&backend, &y);
let g_storage = upload_bf16(&backend, &g);
let dx_bytes = backend.alloc_bytes_handle(n * 2).expect("alloc");
let dx_storage = Storage::new(BackendStorage::Vulkan(dx_bytes), DType::BF16);
let y_arc = Arc::new(RwLock::new(y_storage));
let g_arc = Arc::new(RwLock::new(g_storage));
let dx_arc = Arc::new(RwLock::new(dx_storage));
let kernel = table.lookup_alternatives(
OpKind::SoftmaxLastDimBackward,
&[DType::BF16, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&y_arc), Arc::clone(&g_arc)],
&mut [Arc::clone(&dx_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::SoftmaxLastDim {
outer_count: outer,
last_dim: last,
},
)
.expect("softmax_backward bf16 dispatch");
let got = download_bf16(&backend, &dx_arc.read().unwrap());
let expected = softmax_backward_ref(&y_f32, &g_f32, outer, last);
for (i, (a, b)) in got.iter().zip(expected.iter()).enumerate() {
let got_f32 = a.to_f32();
assert!(
(got_f32 - b).abs() < 5e-2,
"softmax_bwd bf16[{i}]: got {got_f32}, expected {b}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_softmax_last_dim_backward_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let y: Vec<f64> = vec![0.1, 0.2, 0.3, 0.4, 0.4, 0.3, 0.2, 0.1];
let g: Vec<f64> = vec![1.0, -1.0, 2.0, -2.0, 0.5, 1.5, -0.5, -1.5];
let y_storage = upload_f64(&backend, &y);
let g_storage = upload_f64(&backend, &g);
let dx_bytes = backend.alloc_bytes_handle(n * 8).expect("alloc");
let dx_storage = Storage::new(BackendStorage::Vulkan(dx_bytes), DType::F64);
let y_arc = Arc::new(RwLock::new(y_storage));
let g_arc = Arc::new(RwLock::new(g_storage));
let dx_arc = Arc::new(RwLock::new(dx_storage));
let kernel = table.lookup_alternatives(
OpKind::SoftmaxLastDimBackward,
&[DType::F64, DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&y_arc), Arc::clone(&g_arc)],
&mut [Arc::clone(&dx_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::SoftmaxLastDim {
outer_count: outer,
last_dim: last,
},
)
.expect("softmax_backward f64 dispatch");
let got = download_f64(&backend, &dx_arc.read().unwrap());
for r in 0..outer {
let off = r * last;
let dot: f64 = (0..last).map(|i| y[off + i] * g[off + i]).sum();
for i in 0..last {
let expected = y[off + i] * (g[off + i] - dot);
assert!(
(got[off + i] - expected).abs() < 1e-12,
"softmax_bwd f64[{}][{}]: got {}, expected {expected}",
r,
i,
got[off + i]
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_concat_along_last_bf16_odd_a_dim() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 10.0, 20.0, 30.0];
let b_f32: Vec<f32> = vec![4.0, 5.0, 6.0, 7.0, 40.0, 50.0, 60.0, 70.0];
let a: Vec<half::bf16> = a_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let b: Vec<half::bf16> = b_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let a_storage = upload_bf16(&backend, &a);
let b_storage = upload_bf16(&backend, &b);
let out_bytes = backend.alloc_bytes_handle(2 * 7 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Concat,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let a_layout = Layout::contiguous(Shape::from_dims(&[2, 3]));
let b_layout = Layout::contiguous(Shape::from_dims(&[2, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 7]));
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&[a_layout, b_layout, out_layout],
&OpParams::Concat {
outer_count: 2,
input_dim_sizes: vec![3, 4],
inner_count: 1,
axis: 1,
},
)
.expect("concat bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
let expected_f32 = [
1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0,
];
for (i, (g, e)) in got.iter().zip(expected_f32.iter()).enumerate() {
let got_f32 = g.to_f32();
assert_eq!(got_f32, *e, "concat bf16[{i}]: got {got_f32}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_concat_along_dim_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 10.0, 20.0, 30.0];
let b_f32: Vec<f32> = vec![4.0, 5.0, 6.0, 7.0, 40.0, 50.0, 60.0, 70.0];
let a: Vec<half::f16> = a_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let b: Vec<half::f16> = b_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let a_storage = upload_f16(&backend, &a);
let b_storage = upload_f16(&backend, &b);
let out_bytes = backend.alloc_bytes_handle(2 * 7 * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Concat, &[DType::F16, DType::F16], BackendId::Vulkan)[0]
.kernel;
let a_layout = Layout::contiguous(Shape::from_dims(&[2, 3]));
let b_layout = Layout::contiguous(Shape::from_dims(&[2, 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 7]));
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&[a_layout, b_layout, out_layout],
&OpParams::Concat {
outer_count: 2,
input_dim_sizes: vec![3, 4],
inner_count: 1,
axis: 1,
},
)
.expect("concat f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
let expected_f32 = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0,
];
for (i, (g, e)) in got.iter().zip(expected_f32.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"concat f16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_concat_along_dim_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a: Vec<f64> = vec![1.0, 10.0];
let b: Vec<f64> = vec![2.0, 3.0, 20.0, 30.0];
let c: Vec<f64> = vec![4.0, 5.0, 6.0, 40.0, 50.0, 60.0];
let a_storage = upload_f64(&backend, &a);
let b_storage = upload_f64(&backend, &b);
let c_storage = upload_f64(&backend, &c);
let out_bytes = backend.alloc_bytes_handle(2 * 6 * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let c_arc = Arc::new(RwLock::new(c_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Concat, &[DType::F64, DType::F64], BackendId::Vulkan)[0]
.kernel;
let a_layout = Layout::contiguous(Shape::from_dims(&[2, 1]));
let b_layout = Layout::contiguous(Shape::from_dims(&[2, 2]));
let c_layout = Layout::contiguous(Shape::from_dims(&[2, 3]));
let out_layout = Layout::contiguous(Shape::from_dims(&[2, 6]));
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc), Arc::clone(&c_arc)],
&mut [Arc::clone(&out_arc)],
&[a_layout, b_layout, c_layout, out_layout],
&OpParams::Concat {
outer_count: 2,
input_dim_sizes: vec![1, 2, 3],
inner_count: 1,
axis: 1,
},
)
.expect("concat f64 dispatch (N=3 chain)");
let got = download_f64(&backend, &out_arc.read().unwrap());
let expected = [
1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0,
];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "concat f64[{i}]: got {g}, expected {e}");
}
}
fn index_select_test_shape() -> (usize, usize, usize, Vec<u32>) {
let outer = 1usize;
let axis_in = 4usize;
let inner = 8usize;
let ids = vec![3u32, 0u32, 2u32];
(outer, axis_in, inner, ids)
}
#[test]
#[ignore]
fn vulkan_dispatch_index_select_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let (outer, axis_in, inner, ids) = index_select_test_shape();
let n_in = outer * axis_in * inner;
let n_out = outer * ids.len() * inner;
let src_f32: Vec<f32> = (0..axis_in)
.flat_map(|r| (0..inner).map(move |c| (r * 100 + c) as f32))
.collect();
let src: Vec<half::f16> = src_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let src_storage = upload_f16(&backend, &src);
let ids_bytes: &[u8] = bytemuck::cast_slice(&ids);
let ids_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(ids_bytes).expect("ids upload")),
DType::U32,
);
let out_bytes = backend.alloc_bytes_handle(n_out * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let src_arc = Arc::new(RwLock::new(src_storage));
let ids_arc = Arc::new(RwLock::new(ids_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let _ = n_in;
let kernel = table.lookup_alternatives(
OpKind::IndexSelect,
&[DType::F16, DType::U32, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let src_layout = Layout::contiguous(Shape::from_dims(&[outer, axis_in, inner]));
let ids_layout = Layout::contiguous(Shape::from_dims(&[ids.len()]));
let out_layout = Layout::contiguous(Shape::from_dims(&[outer, ids.len(), inner]));
kernel(
&[Arc::clone(&src_arc), Arc::clone(&ids_arc)],
&mut [Arc::clone(&out_arc)],
&[src_layout, ids_layout, out_layout],
&OpParams::IndexSelect {
outer_count: outer,
source_dim_size: axis_in,
n_indices: ids.len(),
inner_count: inner,
},
)
.expect("index_select f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
let expected_f32: Vec<f32> = ids
.iter()
.flat_map(|&id| (0..inner).map(move |c| (id as usize * 100 + c) as f32))
.collect();
for (i, (g, e)) in got.iter().zip(expected_f32.iter()).enumerate() {
assert_eq!(
g.to_f32(),
*e,
"index_select_f16[{i}]: got {}, expected {e}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_index_select_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let (outer, axis_in, inner, ids) = index_select_test_shape();
let n_out = outer * ids.len() * inner;
let src_f32: Vec<f32> = (0..axis_in)
.flat_map(|r| (0..inner).map(move |c| (r * 100 + c) as f32))
.collect();
let src: Vec<half::bf16> = src_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let src_storage = upload_bf16(&backend, &src);
let ids_bytes: &[u8] = bytemuck::cast_slice(&ids);
let ids_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(ids_bytes).expect("ids upload")),
DType::U32,
);
let out_bytes = backend.alloc_bytes_handle(n_out * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let src_arc = Arc::new(RwLock::new(src_storage));
let ids_arc = Arc::new(RwLock::new(ids_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::IndexSelect,
&[DType::BF16, DType::U32, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let src_layout = Layout::contiguous(Shape::from_dims(&[outer, axis_in, inner]));
let ids_layout = Layout::contiguous(Shape::from_dims(&[ids.len()]));
let out_layout = Layout::contiguous(Shape::from_dims(&[outer, ids.len(), inner]));
kernel(
&[Arc::clone(&src_arc), Arc::clone(&ids_arc)],
&mut [Arc::clone(&out_arc)],
&[src_layout, ids_layout, out_layout],
&OpParams::IndexSelect {
outer_count: outer,
source_dim_size: axis_in,
n_indices: ids.len(),
inner_count: inner,
},
)
.expect("index_select bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
let expected_f32: Vec<f32> = ids
.iter()
.flat_map(|&id| (0..inner).map(move |c| (id as usize * 100 + c) as f32))
.collect();
for (i, (g, e)) in got.iter().zip(expected_f32.iter()).enumerate() {
let got_f32 = g.to_f32();
assert!(
(got_f32 - e).abs() < 5.0,
"index_select_bf16[{i}]: got {got_f32}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_index_select_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let (outer, axis_in, inner, ids) = index_select_test_shape();
let n_out = outer * ids.len() * inner;
let src: Vec<f64> = (0..axis_in)
.flat_map(|r| (0..inner).map(move |c| (r * 100 + c) as f64))
.collect();
let src_storage = upload_f64(&backend, &src);
let ids_bytes: &[u8] = bytemuck::cast_slice(&ids);
let ids_storage = Storage::new(
BackendStorage::Vulkan(backend.upload_bytes_handle(ids_bytes).expect("ids upload")),
DType::U32,
);
let out_bytes = backend.alloc_bytes_handle(n_out * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let src_arc = Arc::new(RwLock::new(src_storage));
let ids_arc = Arc::new(RwLock::new(ids_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::IndexSelect,
&[DType::F64, DType::U32, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let src_layout = Layout::contiguous(Shape::from_dims(&[outer, axis_in, inner]));
let ids_layout = Layout::contiguous(Shape::from_dims(&[ids.len()]));
let out_layout = Layout::contiguous(Shape::from_dims(&[outer, ids.len(), inner]));
kernel(
&[Arc::clone(&src_arc), Arc::clone(&ids_arc)],
&mut [Arc::clone(&out_arc)],
&[src_layout, ids_layout, out_layout],
&OpParams::IndexSelect {
outer_count: outer,
source_dim_size: axis_in,
n_indices: ids.len(),
inner_count: inner,
},
)
.expect("index_select f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
let expected: Vec<f64> = ids
.iter()
.flat_map(|&id| (0..inner).map(move |c| (id as usize * 100 + c) as f64))
.collect();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert_eq!(*g, *e, "index_select_f64[{i}]: got {g}, expected {e}");
}
}
#[test]
#[ignore]
fn vulkan_dispatch_rope_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 1usize;
let seq = 2usize;
let hd = 4usize;
let n_x = outer * seq * hd;
let n_table = seq * hd;
let host_x_f32: Vec<f32> = (0..n_x).map(|i| i as f32 + 1.0).collect();
let host_cos_f32: Vec<f32> = vec![1.0; n_table];
let host_sin_f32: Vec<f32> = vec![0.0; n_table];
let host_x: Vec<half::f16> = host_x_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let host_cos: Vec<half::f16> = host_cos_f32
.iter()
.map(|&x| half::f16::from_f32(x))
.collect();
let host_sin: Vec<half::f16> = host_sin_f32
.iter()
.map(|&x| half::f16::from_f32(x))
.collect();
let x_storage = upload_f16(&backend, &host_x);
let cos_storage = upload_f16(&backend, &host_cos);
let sin_storage = upload_f16(&backend, &host_sin);
let out_bytes = backend.alloc_bytes_handle(n_x * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let x_arc = Arc::new(RwLock::new(x_storage));
let cos_arc = Arc::new(RwLock::new(cos_storage));
let sin_arc = Arc::new(RwLock::new(sin_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Rope,
&[DType::F16, DType::F16, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let x_layout = Layout::contiguous(Shape::from_dims(&[outer, seq, hd]));
let table_layout = Layout::contiguous(Shape::from_dims(&[seq, hd]));
let out_layout = x_layout.clone();
kernel(
&[
Arc::clone(&x_arc),
Arc::clone(&cos_arc),
Arc::clone(&sin_arc),
],
&mut [Arc::clone(&out_arc)],
&[x_layout, table_layout.clone(), table_layout, out_layout],
&OpParams::Rope {
outer_count: outer,
seq,
head_dim: hd,
},
)
.expect("rope f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for (i, (g, h)) in got.iter().zip(host_x_f32.iter()).enumerate() {
let got_f32 = g.to_f32();
assert!(
(got_f32 - h).abs() < 5e-3,
"rope-f16[{i}] (identity): got {got_f32}, expected {h}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_rope_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 1usize;
let seq = 2usize;
let hd = 4usize;
let n_x = outer * seq * hd;
let n_table = seq * hd;
let host_x: Vec<f64> = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, ];
let q = std::f64::consts::FRAC_1_SQRT_2;
let host_cos: Vec<f64> = vec![q; n_table];
let host_sin: Vec<f64> = vec![q; n_table];
let x_storage = upload_f64(&backend, &host_x);
let cos_storage = upload_f64(&backend, &host_cos);
let sin_storage = upload_f64(&backend, &host_sin);
let out_bytes = backend.alloc_bytes_handle(n_x * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let x_arc = Arc::new(RwLock::new(x_storage));
let cos_arc = Arc::new(RwLock::new(cos_storage));
let sin_arc = Arc::new(RwLock::new(sin_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Rope,
&[DType::F64, DType::F64, DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let x_layout = Layout::contiguous(Shape::from_dims(&[outer, seq, hd]));
let table_layout = Layout::contiguous(Shape::from_dims(&[seq, hd]));
let out_layout = x_layout.clone();
kernel(
&[
Arc::clone(&x_arc),
Arc::clone(&cos_arc),
Arc::clone(&sin_arc),
],
&mut [Arc::clone(&out_arc)],
&[x_layout, table_layout.clone(), table_layout, out_layout],
&OpParams::Rope {
outer_count: outer,
seq,
head_dim: hd,
},
)
.expect("rope f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
let h = hd / 2;
for s in 0..seq {
let row = s * hd;
for i in 0..h {
let x0 = host_x[row + i];
let x1 = host_x[row + i + h];
let expected_lo = x0 * q - x1 * q;
let expected_hi = x1 * q + x0 * q;
assert!(
(got[row + i] - expected_lo).abs() < 1e-12,
"rope-f64 s={s} i={i}: got {}, expected {expected_lo}",
got[row + i]
);
assert!(
(got[row + i + h] - expected_hi).abs() < 1e-12,
"rope-f64 s={s} i+h={}: got {}, expected {expected_hi}",
i + h,
got[row + i + h]
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_rope_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 1usize;
let seq = 2usize;
let hd = 8usize; let n_x = outer * seq * hd;
let n_table = seq * hd;
let host_x_f32: Vec<f32> = vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 2.0, 4.0, 6.0, 8.0, 1.0, 3.0, 5.0, 7.0,
];
let q = std::f32::consts::FRAC_1_SQRT_2;
let host_cos_f32: Vec<f32> = vec![q; n_table];
let host_sin_f32: Vec<f32> = vec![q; n_table];
let host_x: Vec<half::bf16> = host_x_f32
.iter()
.map(|&x| half::bf16::from_f32(x))
.collect();
let host_cos: Vec<half::bf16> = host_cos_f32
.iter()
.map(|&x| half::bf16::from_f32(x))
.collect();
let host_sin: Vec<half::bf16> = host_sin_f32
.iter()
.map(|&x| half::bf16::from_f32(x))
.collect();
let x_storage = upload_bf16(&backend, &host_x);
let cos_storage = upload_bf16(&backend, &host_cos);
let sin_storage = upload_bf16(&backend, &host_sin);
let out_bytes = backend.alloc_bytes_handle(n_x * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let x_arc = Arc::new(RwLock::new(x_storage));
let cos_arc = Arc::new(RwLock::new(cos_storage));
let sin_arc = Arc::new(RwLock::new(sin_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Rope,
&[DType::BF16, DType::BF16, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let x_layout = Layout::contiguous(Shape::from_dims(&[outer, seq, hd]));
let table_layout = Layout::contiguous(Shape::from_dims(&[seq, hd]));
let out_layout = x_layout.clone();
kernel(
&[
Arc::clone(&x_arc),
Arc::clone(&cos_arc),
Arc::clone(&sin_arc),
],
&mut [Arc::clone(&out_arc)],
&[x_layout, table_layout.clone(), table_layout, out_layout],
&OpParams::Rope {
outer_count: outer,
seq,
head_dim: hd,
},
)
.expect("rope bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
let h = hd / 2;
for s in 0..seq {
let row = s * hd;
for i in 0..h {
let x0 = host_x_f32[row + i];
let x1 = host_x_f32[row + i + h];
let expected_lo = x0 * q - x1 * q;
let expected_hi = x1 * q + x0 * q;
let got_lo = got[row + i].to_f32();
let got_hi = got[row + i + h].to_f32();
let tol_lo = expected_lo.abs() * 0.01 + 5e-2;
let tol_hi = expected_hi.abs() * 0.01 + 5e-2;
assert!(
(got_lo - expected_lo).abs() < tol_lo,
"rope-bf16 s={s} i={i}: got {got_lo}, expected {expected_lo}"
);
assert!(
(got_hi - expected_hi).abs() < tol_hi,
"rope-bf16 s={s} i+h={}: got {got_hi}, expected {expected_hi}",
i + h
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_cast_f32_to_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f32> = vec![1.0, 2.5, -3.125, 0.0, f32::MIN_POSITIVE, 1.0e20];
let n = host.len();
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Cast, &[DType::F32, DType::F64], BackendId::Vulkan)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::None,
)
.expect("cast f32→f64");
let got = download_f64(&backend, &out_arc.read().unwrap());
for (i, (g, h)) in got.iter().zip(host.iter()).enumerate() {
assert_eq!(
*g, *h as f64,
"cast_f32_to_f64[{i}]: got {g}, expected {}",
*h as f64
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_cast_f64_to_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f64> = vec![
1.0,
2.5,
-3.125,
0.0,
1.0e20, 1.0e40, 1.0e-50, std::f64::consts::PI, ];
let n = host.len();
let in_storage = upload_f64(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel =
table.lookup_alternatives(OpKind::Cast, &[DType::F64, DType::F32], BackendId::Vulkan)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::None,
)
.expect("cast f64→f32");
let got = download_f32(&backend, &out_arc.read().unwrap());
for (i, (g, h)) in got.iter().zip(host.iter()).enumerate() {
let expected = *h as f32;
if expected.is_infinite() {
assert!(
g.is_infinite() && g.signum() == expected.signum(),
"cast_f64_to_f32[{i}]: got {g}, expected ±inf"
);
} else {
assert_eq!(
*g, expected,
"cast_f64_to_f32[{i}]: got {g}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_abs_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::AbsElementwise, &[1.0, -2.0, 0.0, -3.5]);
assert_eq!(got, vec![1.0, 2.0, 0.0, 3.5]);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sign_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(
&backend,
OpKind::SignElementwise,
&[5.0, -3.0, 0.0, -0.0, 7.5],
);
assert_eq!(got, vec![1.0, -1.0, 0.0, 0.0, 1.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_recip_f32() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f32(&backend, OpKind::RecipElementwise, &[1.0, 2.0, 4.0, -8.0]);
assert_close(&got, &[1.0, 0.5, 0.25, -0.125], 1e-6, 1e-6);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_sign_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let host: Vec<half::f16> = [5.0_f32, -3.0, 0.0, 7.5]
.iter()
.map(|&x| half::f16::from_f32(x))
.collect();
let got = run_unary_f16(&backend, OpKind::SignElementwise, &host);
let got_f32: Vec<f32> = got.iter().map(|x| x.to_f32()).collect();
assert_eq!(got_f32, vec![1.0, -1.0, 0.0, 1.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_recip_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let got = run_unary_f64(
&backend,
OpKind::RecipElementwise,
&[1.0_f64, 2.0, 4.0, -8.0, 16.0],
);
let expected = [1.0_f64, 0.5, 0.25, -0.125, 0.0625];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-15,
"recip-f64[{i}]: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_abs_sign_recip_bf16() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<bf16> = [1.0_f32, -2.0, 0.0, -3.5, 4.0, 0.25]
.iter()
.map(|&x| bf16::from_f32(x))
.collect();
let n = host.len();
for (op, label, expected) in [
(
OpKind::AbsElementwise,
"abs",
vec![1.0_f32, 2.0, 0.0, 3.5, 4.0, 0.25],
),
(
OpKind::SignElementwise,
"sign",
vec![1.0_f32, -1.0, 0.0, -1.0, 1.0, 1.0],
),
(
OpKind::RecipElementwise,
"recip",
vec![1.0_f32, -0.5, f32::INFINITY, -0.2857143, 0.25, 4.0],
),
] {
let kernel =
table.lookup_alternatives(op, &[DType::BF16, DType::BF16], BackendId::Vulkan)[0].kernel;
let in_storage = upload_bf16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[n]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::None,
)
.expect("bf16 unary");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
let got_f32 = g.to_f32();
if e.is_infinite() {
assert!(
got_f32.is_infinite() && got_f32.signum() == e.signum(),
"{label}-bf16[{i}]: got {got_f32}, expected ±inf"
);
} else {
let tol = e.abs() * 0.01 + 5e-3;
assert!(
(got_f32 - e).abs() < tol,
"{label}-bf16[{i}]: got {got_f32}, expected {e}"
);
}
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_sum_full_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let n = 8usize;
let host_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5];
let host: Vec<half::f16> = host_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let expected: f32 = host_f32.iter().sum();
let in_storage = upload_f16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::SumReduce,
&[DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let out_layout = Layout::contiguous(Shape::from_dims(&[1]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout, out_layout],
&OpParams::Reduce {
dims: vec![],
keepdim: false,
},
)
.expect("sum-full-reduce f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
let got_f32 = got[0].to_f32();
assert!(
(got_f32 - expected).abs() < 5e-3,
"sum-full-f16: got {got_f32}, expected {expected}"
);
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_min_full_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let n = 8usize; let host_f32: Vec<f32> = vec![3.0, -1.0, 4.0, 1.5, -5.0, 9.0, 2.0, 6.0];
let host: Vec<half::bf16> = host_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let expected = host_f32.iter().cloned().fold(f32::INFINITY, f32::min);
let in_storage = upload_bf16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MinReduce,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let out_layout = Layout::contiguous(Shape::from_dims(&[1]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout, out_layout],
&OpParams::Reduce {
dims: vec![],
keepdim: false,
},
)
.expect("min-full-reduce bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
let got_f32 = got[0].to_f32();
assert!(
(got_f32 - expected).abs() < 5e-2,
"min-full-bf16: got {got_f32}, expected {expected}"
);
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_mean_full_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let n = 8usize;
let host: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let expected: f64 = host.iter().sum::<f64>() / n as f64;
let in_storage = upload_f64(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MeanReduce,
&[DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let out_layout = Layout::contiguous(Shape::from_dims(&[1]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout, out_layout],
&OpParams::Reduce {
dims: vec![],
keepdim: false,
},
)
.expect("mean-full-reduce f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
assert!(
(got[0] - expected).abs() < 1e-12,
"mean-full-f64: got {}, expected {}",
got[0],
expected
);
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_sum_last_dim_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let host_f32: Vec<f32> = vec![
1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5, ];
let host: Vec<half::f16> = host_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let in_storage = upload_f16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(outer * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::SumReduce,
&[DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let out_layout = Layout::contiguous(Shape::from_dims(&[outer]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout, out_layout],
&OpParams::Reduce {
dims: vec![1],
keepdim: false,
},
)
.expect("sum-reduce f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
let _ = n; let expected = [10.0_f32, 8.0_f32];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
let got_f32 = g.to_f32();
assert!(
(got_f32 - e).abs() < 5e-3,
"sum-reduce-f16 row {i}: got {got_f32}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_max_last_dim_bf16_odd_rows() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 5usize;
let last = 4usize; let host_f32: Vec<f32> = vec![
1.0, 2.0, 3.0, 4.0, -1.0, 5.0, 2.0, 0.0, 0.5, 1.5, 2.5, 3.5, -5.0, -3.0, -2.0, -1.0, 7.0, 6.0, 8.0, 4.5, ];
let host: Vec<half::bf16> = host_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let in_storage = upload_bf16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(outer * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MaxReduce,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let out_layout = Layout::contiguous(Shape::from_dims(&[outer]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout, out_layout],
&OpParams::Reduce {
dims: vec![1],
keepdim: false,
},
)
.expect("max-reduce bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
let expected = [4.0_f32, 5.0, 3.5, -1.0, 8.0];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
let got_f32 = g.to_f32();
assert!(
(got_f32 - e).abs() < 5e-2,
"max-reduce-bf16 row {i}: got {got_f32}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_reduce_mean_last_dim_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let host: Vec<f64> = vec![
1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5, ];
let in_storage = upload_f64(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(outer * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::MeanReduce,
&[DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let out_layout = Layout::contiguous(Shape::from_dims(&[outer]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout, out_layout],
&OpParams::Reduce {
dims: vec![1],
keepdim: false,
},
)
.expect("mean-reduce f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
let expected = [2.5_f64, 2.0];
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g - e).abs() < 1e-12,
"mean-reduce-f64 row {i}: got {g}, expected {e}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_softmax_last_dim_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let host_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5];
let host: Vec<half::f16> = host_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let in_storage = upload_f16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::SoftmaxLastDim,
&[DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::SoftmaxLastDim {
outer_count: outer,
last_dim: last,
},
)
.expect("softmax f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for row in 0..outer {
let xs = &host_f32[row * last..(row + 1) * last];
let ys = &got[row * last..(row + 1) * last];
let max = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = xs.iter().map(|x| (x - max).exp()).collect();
let sum: f32 = exps.iter().sum();
for (i, (e, y)) in exps.iter().zip(ys.iter()).enumerate() {
let expected = e / sum;
let got_f32 = y.to_f32();
assert!(
(got_f32 - expected).abs() < 5e-3,
"softmax-f16 row {row} col {i}: got {got_f32}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_softmax_last_dim_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize; let n = outer * last;
let host_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5];
let host: Vec<half::bf16> = host_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let in_storage = upload_bf16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::SoftmaxLastDim,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::SoftmaxLastDim {
outer_count: outer,
last_dim: last,
},
)
.expect("softmax bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for row in 0..outer {
let xs = &host_f32[row * last..(row + 1) * last];
let ys = &got[row * last..(row + 1) * last];
let max = xs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = xs.iter().map(|x| (x - max).exp()).collect();
let sum: f32 = exps.iter().sum();
for (i, (e, y)) in exps.iter().zip(ys.iter()).enumerate() {
let expected = e / sum;
let got_f32 = y.to_f32();
assert!(
(got_f32 - expected).abs() < 5e-2,
"softmax-bf16 row {row} col {i}: got {got_f32}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_softmax_last_dim_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let host: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 0.5, 1.5, 2.5, 3.5];
let in_storage = upload_f64(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::SoftmaxLastDim,
&[DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::SoftmaxLastDim {
outer_count: outer,
last_dim: last,
},
)
.expect("softmax f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
for row in 0..outer {
let xs = &host[row * last..(row + 1) * last];
let ys = &got[row * last..(row + 1) * last];
let max = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let exps: Vec<f64> = xs.iter().map(|x| (x - max).exp()).collect();
let sum: f64 = exps.iter().sum();
for (i, (e, y)) in exps.iter().zip(ys.iter()).enumerate() {
let expected = e / sum;
assert!(
(y - expected).abs() < 1e-7,
"softmax-f64 row {row} col {i}: got {y}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_rms_norm_last_dim_f16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let host_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let host: Vec<half::f16> = host_f32.iter().map(|&x| half::f16::from_f32(x)).collect();
let in_storage = upload_f16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::RmsNormLastDim,
&[DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let eps = 1e-6f64;
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("rmsnorm f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
for row in 0..outer {
let xs = &host_f32[row * last..(row + 1) * last];
let ys = &got[row * last..(row + 1) * last];
let mean_sq: f32 = xs.iter().map(|x| x * x).sum::<f32>() / last as f32;
let scale = (mean_sq + eps as f32).sqrt();
for (i, (x, y)) in xs.iter().zip(ys.iter()).enumerate() {
let expected = x / scale;
let got_f32 = y.to_f32();
assert!(
(got_f32 - expected).abs() < 5e-3,
"rmsnorm-f16 row {row} col {i}: got {got_f32}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_rms_norm_last_dim_bf16() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize; let n = outer * last;
let host_f32: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let host: Vec<half::bf16> = host_f32.iter().map(|&x| half::bf16::from_f32(x)).collect();
let in_storage = upload_bf16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::RmsNormLastDim,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let eps = 1e-6f64;
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("rmsnorm bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for row in 0..outer {
let xs = &host_f32[row * last..(row + 1) * last];
let ys = &got[row * last..(row + 1) * last];
let mean_sq: f32 = xs.iter().map(|x| x * x).sum::<f32>() / last as f32;
let scale = (mean_sq + eps as f32).sqrt();
for (i, (x, y)) in xs.iter().zip(ys.iter()).enumerate() {
let expected = x / scale;
let got_f32 = y.to_f32();
assert!(
(got_f32 - expected).abs() < 5e-2,
"rmsnorm-bf16 row {row} col {i}: got {got_f32}, expected {expected}"
);
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_rms_norm_last_dim_f64() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let outer = 2usize;
let last = 4usize;
let n = outer * last;
let host: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0, 2.0, 4.0, 6.0, 8.0];
let in_storage = upload_f64(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(n * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::RmsNormLastDim,
&[DType::F64, DType::F64],
BackendId::Vulkan,
)[0]
.kernel;
let layout = Layout::contiguous(Shape::from_dims(&[outer, last]));
let eps = 1e-12f64;
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::NormLastDim {
outer_count: outer,
last_dim: last,
eps,
},
)
.expect("rmsnorm f64 dispatch");
let got = download_f64(&backend, &out_arc.read().unwrap());
for row in 0..outer {
let xs = &host[row * last..(row + 1) * last];
let ys = &got[row * last..(row + 1) * last];
let mean_sq: f64 = xs.iter().map(|x| x * x).sum::<f64>() / last as f64;
let scale = (mean_sq + eps).sqrt();
for (i, (x, y)) in xs.iter().zip(ys.iter()).enumerate() {
let expected = x / scale;
assert!(
(y - expected).abs() < 1e-10,
"rmsnorm-f64 row {row} col {i}: got {y}, expected {expected}"
);
}
}
}
fn upload_bf16(backend: &Arc<VulkanBackend>, host: &[half::bf16]) -> Storage {
let bytes: &[u8] = bytemuck::cast_slice(host);
let vk_bytes = backend
.upload_bytes_handle(bytes)
.expect("vulkan upload bf16");
Storage::new(BackendStorage::Vulkan(vk_bytes), DType::BF16)
}
fn download_bf16(backend: &Arc<VulkanBackend>, s: &Storage) -> Vec<half::bf16> {
let bytes = match &s.inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
};
bytemuck::cast_slice::<u8, half::bf16>(&bytes).to_vec()
}
fn upload_raw(backend: &Arc<VulkanBackend>, bytes: &[u8], dtype: DType) -> Storage {
let vk_bytes = backend
.upload_bytes_handle(bytes)
.expect("vulkan upload raw");
Storage::new(BackendStorage::Vulkan(vk_bytes), dtype)
}
fn download_raw(backend: &Arc<VulkanBackend>, s: &Storage) -> Vec<u8> {
match &s.inner {
BackendStorage::Vulkan(v) => backend.download_bytes(v).expect("d2h"),
_ => panic!("not on Vulkan"),
}
}
#[test]
#[ignore]
fn vulkan_dispatch_triu_f32() {
use fuel_dispatch::kernel::OpParams;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let rows = 4usize;
let cols = 4usize;
let mat = vec![1.0_f32; rows * cols];
for diagonal in [0i64, 1, -1] {
let kernel =
table.lookup_alternatives(OpKind::Triu, &[DType::F32, DType::F32], BackendId::Vulkan)
[0]
.kernel;
let in_storage = upload_f32(&backend, &mat);
let out_bytes = backend.alloc_bytes_handle(rows * cols * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[rows, cols]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::Triangular {
batch_count: 1,
rows,
cols,
diagonal,
},
)
.expect("triu");
let got = download_f32(&backend, &out_arc.read().unwrap());
for i in 0..rows {
for j in 0..cols {
let expected = if (j as i64) >= (i as i64) + diagonal {
1.0
} else {
0.0
};
assert_eq!(
got[i * cols + j],
expected,
"triu(diag={diagonal})[{i},{j}]: got {} expected {expected}",
got[i * cols + j]
);
}
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_tril_f32() {
use fuel_dispatch::kernel::OpParams;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let rows = 4usize;
let cols = 4usize;
let mat = vec![1.0_f32; rows * cols];
for diagonal in [0i64, 1, -1] {
let kernel =
table.lookup_alternatives(OpKind::Tril, &[DType::F32, DType::F32], BackendId::Vulkan)
[0]
.kernel;
let in_storage = upload_f32(&backend, &mat);
let out_bytes = backend.alloc_bytes_handle(rows * cols * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[rows, cols]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::Triangular {
batch_count: 1,
rows,
cols,
diagonal,
},
)
.expect("tril");
let got = download_f32(&backend, &out_arc.read().unwrap());
for i in 0..rows {
for j in 0..cols {
let expected = if (j as i64) <= (i as i64) + diagonal {
1.0
} else {
0.0
};
assert_eq!(
got[i * cols + j],
expected,
"tril(diag={diagonal})[{i},{j}]: got {} expected {expected}",
got[i * cols + j]
);
}
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_triu_f16() {
use fuel_dispatch::kernel::OpParams;
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let rows = 3usize;
let cols = 4usize; let mat: Vec<f16> = (0..rows * cols)
.map(|k| f16::from_f32(k as f32 + 1.0))
.collect();
let kernel =
table.lookup_alternatives(OpKind::Triu, &[DType::F16, DType::F16], BackendId::Vulkan)[0]
.kernel;
let in_storage = upload_f16(&backend, &mat);
let out_bytes = backend.alloc_bytes_handle(rows * cols * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[rows, cols]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::Triangular {
batch_count: 1,
rows,
cols,
diagonal: 0,
},
)
.expect("triu f16");
let got = download_f16(&backend, &out_arc.read().unwrap());
for i in 0..rows {
for j in 0..cols {
let v = got[i * cols + j].to_f32();
let expected = if j >= i {
(i * cols + j) as f32 + 1.0
} else {
0.0
};
assert_eq!(v, expected, "triu f16[{i},{j}]");
}
}
}
#[test]
#[ignore]
fn vulkan_dispatch_flip_f32() {
use fuel_dispatch::kernel::OpParams;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host = vec![0.0_f32, 1.0, 2.0, 3.0, 4.0];
let kernel =
table.lookup_alternatives(OpKind::Flip, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[host.len()]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::Flip {
outer_count: 1,
dim_size: host.len(),
inner_count: 1,
axis: 0,
},
)
.expect("flip 1D");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![4.0, 3.0, 2.0, 1.0, 0.0]);
let host: Vec<f32> = (0..12).map(|k| k as f32).collect();
let kernel =
table.lookup_alternatives(OpKind::Flip, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[2, 3, 2]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::Flip {
outer_count: 2,
dim_size: 3,
inner_count: 2,
axis: 1,
},
)
.expect("flip 3D");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(
got,
vec![4.0, 5.0, 2.0, 3.0, 0.0, 1.0, 10.0, 11.0, 8.0, 9.0, 6.0, 7.0]
);
}
#[test]
#[ignore]
fn vulkan_dispatch_roll_f32() {
use fuel_dispatch::kernel::OpParams;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host = vec![0.0_f32, 1.0, 2.0, 3.0, 4.0];
let kernel =
table.lookup_alternatives(OpKind::Roll, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[host.len()]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout.clone()],
&OpParams::Roll {
outer_count: 1,
dim_size: host.len(),
inner_count: 1,
shift: 2,
axis: 0,
},
)
.expect("roll +2");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![3.0, 4.0, 0.0, 1.0, 2.0]);
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout.clone()],
&OpParams::Roll {
outer_count: 1,
dim_size: host.len(),
inner_count: 1,
shift: -1,
axis: 0,
},
)
.expect("roll -1");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![1.0, 2.0, 3.0, 4.0, 0.0]);
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::Roll {
outer_count: 1,
dim_size: host.len(),
inner_count: 1,
shift: 7,
axis: 0,
},
)
.expect("roll +7");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![3.0, 4.0, 0.0, 1.0, 2.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_cumsum_f32_1d() {
use fuel_dispatch::kernel::OpParams;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host = vec![1.0_f32, 2.0, 3.0, 4.0, 5.0];
let kernel =
table.lookup_alternatives(OpKind::CumSum, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[host.len()]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::CumSum {
outer_count: 1,
dim_size: host.len(),
inner_count: 1,
axis: 0,
},
)
.expect("cumsum 1D");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![1.0, 3.0, 6.0, 10.0, 15.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_cumsum_f32_middle_axis() {
use fuel_dispatch::kernel::OpParams;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f32> = vec![
1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 10.0, 20.0, 10.0, 20.0, 10.0, 20.0, 10.0, 20.0,
];
let kernel =
table.lookup_alternatives(OpKind::CumSum, &[DType::F32, DType::F32], BackendId::Vulkan)[0]
.kernel;
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[2, 4, 2]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::CumSum {
outer_count: 2,
dim_size: 4,
inner_count: 2,
axis: 1,
},
)
.expect("cumsum middle axis");
let got = download_f32(&backend, &out_arc.read().unwrap());
assert_eq!(
got,
vec![
1.0, 1.0, 3.0, 3.0, 6.0, 6.0, 10.0, 10.0, 10.0, 20.0, 20.0, 40.0, 30.0, 60.0, 40.0, 80.0,
]
);
}
#[test]
#[ignore]
fn vulkan_dispatch_cumsum_f64_1d() {
use fuel_dispatch::kernel::OpParams;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f64> = vec![1.0, 2.0, 3.0, 4.0];
let alts =
table.lookup_alternatives(OpKind::CumSum, &[DType::F64, DType::F64], BackendId::Vulkan);
if alts.is_empty() {
return;
}
let kernel = alts[0].kernel;
let in_storage = upload_f64(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 8).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F64);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[host.len()]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::CumSum {
outer_count: 1,
dim_size: host.len(),
inner_count: 1,
axis: 0,
},
)
.expect("cumsum f64 1D");
let got = download_f64(&backend, &out_arc.read().unwrap());
assert_eq!(got, vec![1.0, 3.0, 6.0, 10.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_cumsum_f16_1d() {
use fuel_dispatch::kernel::OpParams;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host_f32 = [1.0_f32, 2.0, 3.0, 4.0];
let host: Vec<half::f16> = host_f32.iter().map(|&v| half::f16::from_f32(v)).collect();
let kernel =
table.lookup_alternatives(OpKind::CumSum, &[DType::F16, DType::F16], BackendId::Vulkan)[0]
.kernel;
let in_storage = upload_f16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[host.len()]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::CumSum {
outer_count: 1,
dim_size: host.len(),
inner_count: 1,
axis: 0,
},
)
.expect("cumsum f16 1D");
let got = download_f16(&backend, &out_arc.read().unwrap());
let got_f32: Vec<f32> = got.iter().map(|v| v.to_f32()).collect();
assert_eq!(got_f32, vec![1.0, 3.0, 6.0, 10.0]);
}
#[test]
#[ignore]
fn vulkan_dispatch_unary_bf16() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<bf16> = [1.0_f32, -2.0, 3.5, -4.25]
.iter()
.map(|&x| bf16::from_f32(x))
.collect();
let kernel = table.lookup_alternatives(
OpKind::NegElementwise,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let in_storage = upload_bf16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[host.len()]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::None,
)
.expect("neg bf16");
let got = download_bf16(&backend, &out_arc.read().unwrap());
let expected: Vec<f32> = host.iter().map(|x| -x.to_f32()).collect();
for (i, (g, e)) in got.iter().zip(expected.iter()).enumerate() {
assert!(
(g.to_f32() - e).abs() < 1e-2,
"neg bf16[{i}]: got {} expected {e}",
g.to_f32()
);
}
let host: Vec<bf16> = [0.0_f32, 0.5, 1.0, 2.0, -1.0, -0.5]
.iter()
.map(|&x| bf16::from_f32(x))
.collect();
let kernel = table.lookup_alternatives(
OpKind::ExpElementwise,
&[DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let in_storage = upload_bf16(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len() * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[host.len()]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout],
&OpParams::None,
)
.expect("exp bf16");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for (i, (h, g)) in host.iter().zip(got.iter()).enumerate() {
let truth = h.to_f32().exp();
let abs_err = (g.to_f32() - truth).abs();
let tol = truth.abs() * 0.01 + 1e-3;
assert!(
abs_err <= tol,
"exp bf16[{i}]: got {} truth {truth} abs_err {abs_err}",
g.to_f32()
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_binary_bf16() {
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let a: Vec<bf16> = (0..8).map(|k| bf16::from_f32(k as f32 + 1.0)).collect();
let b: Vec<bf16> = (0..8).map(|k| bf16::from_f32(k as f32 + 0.5)).collect();
let kernel = table.lookup_alternatives(
OpKind::AddElementwise,
&[DType::BF16, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let a_storage = upload_bf16(&backend, &a);
let b_storage = upload_bf16(&backend, &b);
let out_bytes = backend.alloc_bytes_handle(a.len() * 2).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::BF16);
let a_arc = Arc::new(RwLock::new(a_storage));
let b_arc = Arc::new(RwLock::new(b_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let layout = Layout::contiguous(Shape::from_dims(&[a.len()]));
kernel(
&[Arc::clone(&a_arc), Arc::clone(&b_arc)],
&mut [Arc::clone(&out_arc)],
&[layout.clone(), layout.clone(), layout],
&OpParams::None,
)
.expect("add bf16");
let got = download_bf16(&backend, &out_arc.read().unwrap());
for i in 0..a.len() {
let expected = a[i].to_f32() + b[i].to_f32();
let actual = got[i].to_f32();
assert!(
(actual - expected).abs() < 1e-2,
"add bf16[{i}]: got {actual} expected {expected}"
);
}
}
#[test]
#[ignore]
fn vulkan_dispatch_cast_f8e4m3_roundtrip() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host = vec![
0.0_f32, 0.5, 1.0, 1.5, 2.0, 3.5, 7.0, 12.0, -0.5, -1.0, -7.0, -12.0, 448.0, -448.0, 600.0,
-600.0, ];
let kernel = table.lookup_alternatives(
OpKind::Cast,
&[DType::F32, DType::F8E4M3],
BackendId::Vulkan,
)[0]
.kernel;
let in_storage = upload_f32(&backend, &host);
let out_bytes = backend.alloc_bytes_handle(host.len()).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F8E4M3);
let in_arc = Arc::new(RwLock::new(in_storage));
let mid_arc = Arc::new(RwLock::new(out_storage));
let layout_in = Layout::contiguous(Shape::from_dims(&[host.len()]));
kernel(
&[Arc::clone(&in_arc)],
&mut [Arc::clone(&mid_arc)],
&[layout_in.clone(), layout_in.clone()],
&OpParams::None,
)
.expect("f32 -> f8e4m3");
let kernel = table.lookup_alternatives(
OpKind::Cast,
&[DType::F8E4M3, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let out_bytes = backend.alloc_bytes_handle(host.len() * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let out_arc = Arc::new(RwLock::new(out_storage));
kernel(
&[Arc::clone(&mid_arc)],
&mut [Arc::clone(&out_arc)],
&[layout_in.clone(), layout_in],
&OpParams::None,
)
.expect("f8e4m3 -> f32");
let got = download_f32(&backend, &out_arc.read().unwrap());
let expect = vec![
0.0, 0.5, 1.0, 1.5, 2.0, 3.5, 7.0, 12.0, -0.5, -1.0, -7.0, -12.0, 448.0, -448.0, 448.0,
-448.0,
];
for (i, (g, e)) in got.iter().zip(expect.iter()).enumerate() {
let abs_err = (g - e).abs();
let rel_tol = e.abs() * 0.05 + 1e-3;
assert!(
abs_err <= rel_tol,
"roundtrip[{i}]: input {} got {g} expected {e}",
host[i]
);
}
}
#[test]
#[ignore]
#[allow(clippy::identity_op)]
fn vulkan_dispatch_write_slice_b1_u8() {
use fuel_dispatch::kernel::OpParams;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let src_bytes: Vec<u8> = vec![10, 20, 30, 40, 50, 60, 70, 80]; let dst_bytes: Vec<u8> = vec![0; 4 * 8];
let kernel = table.lookup_alternatives(
OpKind::WriteSlice,
&[DType::U8, DType::U8],
BackendId::Vulkan,
)[0]
.kernel;
let src_storage = upload_raw(&backend, &src_bytes, DType::U8);
let dst_storage = upload_raw(&backend, &dst_bytes, DType::U8);
let src_arc = Arc::new(RwLock::new(src_storage));
let dst_arc = Arc::new(RwLock::new(dst_storage));
let layout_src = Layout::contiguous(Shape::from_dims(&[2, 4]));
let layout_dst = Layout::contiguous(Shape::from_dims(&[4, 8]));
kernel(
&[Arc::clone(&src_arc)],
&mut [Arc::clone(&dst_arc)],
&[layout_src, layout_dst],
&OpParams::WriteSlice {
dest_shape: vec![4, 8],
ranges: vec![(1, 3), (4, 8)],
deferred_dyn_offset: None,
},
)
.expect("write_slice b1");
let got = download_raw(&backend, &dst_arc.read().unwrap());
let mut expected = vec![0u8; 4 * 8];
expected[1 * 8 + 4] = 10;
expected[1 * 8 + 5] = 20;
expected[1 * 8 + 6] = 30;
expected[1 * 8 + 7] = 40;
expected[2 * 8 + 4] = 50;
expected[2 * 8 + 5] = 60;
expected[2 * 8 + 6] = 70;
expected[2 * 8 + 7] = 80;
assert_eq!(got, expected);
}
fn cpu_reference_q4_0(
a: &[f32],
blocks: &[fuel_quantized::BlockQ4_0],
m: usize,
k: usize,
n: usize,
) -> Vec<f32> {
let mut out = vec![0.0_f32; m * n];
fuel_quantized::matmul::<fuel_quantized::BlockQ4_0>((m, k, n), a, blocks, &mut out)
.expect("Q4_0 CPU ref matmul");
out
}
#[test]
#[ignore]
fn vulkan_dispatch_qmatmul_q4_0_m1() {
use fuel_graph::QuantType;
use fuel_quantized::{BlockQ4_0, GgmlType};
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 1usize;
let k = 128usize;
let n = 64usize;
let blocks_per_row = k / BlockQ4_0::BLCK_SIZE; let a: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.013).sin() * 0.5).collect();
let w: Vec<f32> = (0..n * k)
.map(|i| ((i as f32 + 1.0) * 0.007).cos() * 0.3)
.collect();
let mut w_blocks = vec![BlockQ4_0::zeros(); n * blocks_per_row];
BlockQ4_0::from_float(&w, &mut w_blocks);
let w_bytes_per_block = std::mem::size_of::<BlockQ4_0>();
let w_bytes: Vec<u8> = unsafe {
std::slice::from_raw_parts(
w_blocks.as_ptr() as *const u8,
w_blocks.len() * w_bytes_per_block,
)
}
.to_vec();
let a_storage = upload_f32(&backend, &a);
let w_storage = upload_raw(&backend, &w_bytes, DType::U32);
let out_bytes = backend.alloc_bytes_handle(m * n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let w_arc = Arc::new(RwLock::new(w_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let alts = table.lookup_alternatives(
OpKind::QMatMul,
&[DType::F32, DType::U32, DType::F32],
BackendId::Vulkan,
);
assert!(
!alts.is_empty(),
"QMatMul Q4_0 must have a Vulkan registration"
);
let kernel = alts[0].kernel;
let lhs_layout = Layout::contiguous(Shape::from_dims(&[m, k]));
let rhs_layout = Layout::contiguous(Shape::from_dims(&[w_bytes.len() / 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[m, n]));
kernel(
&[Arc::clone(&a_arc), Arc::clone(&w_arc)],
&mut [Arc::clone(&out_arc)],
&[lhs_layout, rhs_layout, out_layout],
&OpParams::QMatMul {
quant_type: QuantType::Q4_0,
batch_count: 1,
m,
n,
k,
},
)
.expect("qmatmul Q4_0 m=1");
let got = download_f32(&backend, &out_arc.read().unwrap());
let ref_out = cpu_reference_q4_0(&a, &w_blocks, m, k, n);
let mut max_rel = 0.0_f32;
for (g, r) in got.iter().zip(ref_out.iter()) {
let abs = (g - r).abs();
if r.abs() < 1e-6 {
assert!(abs < 1e-4, "near-zero mismatch: got {g}, ref {r}");
} else {
let rel = abs / r.abs();
if rel > max_rel {
max_rel = rel;
}
}
}
eprintln!("Q4_0 m=1: max rel err vs CPU ref = {max_rel:e}");
assert!(max_rel < 5e-3, "Q4_0 m=1 worst rel err {max_rel:e} > 5e-3");
}
#[test]
#[ignore]
fn vulkan_dispatch_qmatmul_q4_0_tiled() {
use fuel_graph::QuantType;
use fuel_quantized::{BlockQ4_0, GgmlType};
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 8usize; let k = 128usize;
let n = 32usize;
let blocks_per_row = k / BlockQ4_0::BLCK_SIZE;
let a: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.011).sin() * 0.4).collect();
let w: Vec<f32> = (0..n * k)
.map(|i| ((i as f32 + 1.0) * 0.005).cos() * 0.25)
.collect();
let mut w_blocks = vec![BlockQ4_0::zeros(); n * blocks_per_row];
BlockQ4_0::from_float(&w, &mut w_blocks);
let bpb = std::mem::size_of::<BlockQ4_0>();
let w_bytes: Vec<u8> =
unsafe { std::slice::from_raw_parts(w_blocks.as_ptr() as *const u8, w_blocks.len() * bpb) }
.to_vec();
let a_storage = upload_f32(&backend, &a);
let w_storage = upload_raw(&backend, &w_bytes, DType::U32);
let out_bytes = backend.alloc_bytes_handle(m * n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let w_arc = Arc::new(RwLock::new(w_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::QMatMul,
&[DType::F32, DType::U32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let lhs_layout = Layout::contiguous(Shape::from_dims(&[m, k]));
let rhs_layout = Layout::contiguous(Shape::from_dims(&[w_bytes.len() / 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[m, n]));
kernel(
&[Arc::clone(&a_arc), Arc::clone(&w_arc)],
&mut [Arc::clone(&out_arc)],
&[lhs_layout, rhs_layout, out_layout],
&OpParams::QMatMul {
quant_type: QuantType::Q4_0,
batch_count: 1,
m,
n,
k,
},
)
.expect("qmatmul Q4_0 m>1 tiled");
let got = download_f32(&backend, &out_arc.read().unwrap());
let ref_out = cpu_reference_q4_0(&a, &w_blocks, m, k, n);
let mut max_rel = 0.0_f32;
for (g, r) in got.iter().zip(ref_out.iter()) {
let abs = (g - r).abs();
if r.abs() < 1e-6 {
assert!(abs < 1e-4, "near-zero mismatch: got {g}, ref {r}");
} else {
let rel = abs / r.abs();
if rel > max_rel {
max_rel = rel;
}
}
}
eprintln!("Q4_0 tiled (m={m}): max rel err vs CPU ref = {max_rel:e}");
assert!(
max_rel < 1e-1,
"Q4_0 tiled worst rel err {max_rel:e} > 1e-1"
);
}
#[test]
#[ignore]
fn vulkan_dispatch_qmatmul_q4_km() {
use fuel_graph::QuantType;
use fuel_quantized::{BlockQ4K, GgmlType};
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 4usize;
let k = 256usize; let n = 32usize;
let blocks_per_row = k / 256;
let a: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.017).sin() * 0.6).collect();
let w: Vec<f32> = (0..n * k)
.map(|i| ((i as f32 + 1.0) * 0.003).cos() * 0.2)
.collect();
let mut w_blocks = vec![BlockQ4K::zeros(); n * blocks_per_row];
BlockQ4K::from_float(&w, &mut w_blocks);
let bpb = std::mem::size_of::<BlockQ4K>();
let w_bytes: Vec<u8> =
unsafe { std::slice::from_raw_parts(w_blocks.as_ptr() as *const u8, w_blocks.len() * bpb) }
.to_vec();
let a_storage = upload_f32(&backend, &a);
let w_storage = upload_raw(&backend, &w_bytes, DType::U32);
let out_bytes = backend.alloc_bytes_handle(m * n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let w_arc = Arc::new(RwLock::new(w_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::QMatMul,
&[DType::F32, DType::U32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let lhs_layout = Layout::contiguous(Shape::from_dims(&[m, k]));
let rhs_layout = Layout::contiguous(Shape::from_dims(&[w_bytes.len() / 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[m, n]));
kernel(
&[Arc::clone(&a_arc), Arc::clone(&w_arc)],
&mut [Arc::clone(&out_arc)],
&[lhs_layout, rhs_layout, out_layout],
&OpParams::QMatMul {
quant_type: QuantType::Q4_K_M,
batch_count: 1,
m,
n,
k,
},
)
.expect("qmatmul Q4_K_M");
let got = download_f32(&backend, &out_arc.read().unwrap());
let mut ref_out = vec![0.0_f32; m * n];
fuel_quantized::matmul::<BlockQ4K>((m, k, n), &a, &w_blocks, &mut ref_out)
.expect("Q4_K_M CPU ref matmul");
let mut max_rel = 0.0_f32;
for (g, r) in got.iter().zip(ref_out.iter()) {
let abs = (g - r).abs();
if r.abs() < 1e-6 {
assert!(abs < 1e-3, "near-zero mismatch: got {g}, ref {r}");
} else {
let rel = abs / r.abs();
if rel > max_rel {
max_rel = rel;
}
}
}
eprintln!("Q4_K_M m={m}: max rel err vs CPU ref = {max_rel:e}");
assert!(max_rel < 1e-1, "Q4_K_M worst rel err {max_rel:e} > 1e-1");
}
#[test]
#[ignore]
fn vulkan_dispatch_qmatmul_q8_0() {
use fuel_graph::QuantType;
use fuel_quantized::{BlockQ8_0, GgmlType};
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let m = 2usize;
let k = 64usize;
let n = 16usize;
let blocks_per_row = k / BlockQ8_0::BLCK_SIZE; let a: Vec<f32> = (0..m * k).map(|i| (i as f32 * 0.021).sin() * 0.7).collect();
let w: Vec<f32> = (0..n * k)
.map(|i| ((i as f32 + 1.0) * 0.009).cos() * 0.35)
.collect();
let mut w_blocks = vec![BlockQ8_0::zeros(); n * blocks_per_row];
BlockQ8_0::from_float(&w, &mut w_blocks);
let bpb = std::mem::size_of::<BlockQ8_0>();
let w_bytes: Vec<u8> =
unsafe { std::slice::from_raw_parts(w_blocks.as_ptr() as *const u8, w_blocks.len() * bpb) }
.to_vec();
let a_storage = upload_f32(&backend, &a);
let w_storage = upload_raw(&backend, &w_bytes, DType::U32);
let out_bytes = backend.alloc_bytes_handle(m * n * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let a_arc = Arc::new(RwLock::new(a_storage));
let w_arc = Arc::new(RwLock::new(w_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::QMatMul,
&[DType::F32, DType::U32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let lhs_layout = Layout::contiguous(Shape::from_dims(&[m, k]));
let rhs_layout = Layout::contiguous(Shape::from_dims(&[w_bytes.len() / 4]));
let out_layout = Layout::contiguous(Shape::from_dims(&[m, n]));
kernel(
&[Arc::clone(&a_arc), Arc::clone(&w_arc)],
&mut [Arc::clone(&out_arc)],
&[lhs_layout, rhs_layout, out_layout],
&OpParams::QMatMul {
quant_type: QuantType::Q8_0,
batch_count: 1,
m,
n,
k,
},
)
.expect("qmatmul Q8_0");
let got = download_f32(&backend, &out_arc.read().unwrap());
let mut ref_out = vec![0.0_f32; m * n];
fuel_quantized::matmul::<BlockQ8_0>((m, k, n), &a, &w_blocks, &mut ref_out)
.expect("Q8_0 CPU ref matmul");
let mut max_rel = 0.0_f32;
for (g, r) in got.iter().zip(ref_out.iter()) {
let abs = (g - r).abs();
if r.abs() < 1e-6 {
assert!(abs < 1e-4, "near-zero mismatch: got {g}, ref {r}");
} else {
let rel = abs / r.abs();
if rel > max_rel {
max_rel = rel;
}
}
}
eprintln!("Q8_0 m={m}: max rel err vs CPU ref = {max_rel:e}");
assert!(max_rel < 2e-2, "Q8_0 worst rel err {max_rel:e} > 2e-2");
}
#[test]
#[ignore]
fn vulkan_dispatch_conv2d_f32() {
use fuel_conv::{ConvShape, conv2d_direct};
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let shape = ConvShape {
batch: 1,
c_in: 3,
c_out: 4,
h: 8,
w: 8,
k_h: 3,
k_w: 3,
stride: (1, 1),
padding: (1, 1),
groups: 1,
};
shape.validate().unwrap();
let n_in = shape.batch * shape.c_in * shape.h * shape.w;
let n_w = shape.c_out * shape.c_in * shape.k_h * shape.k_w;
let n_out = shape.output_len();
let input: Vec<f32> = (0..n_in).map(|i| (i as f32 * 0.013).sin() * 0.5).collect();
let weight: Vec<f32> = (0..n_w)
.map(|i| ((i as f32 + 1.0) * 0.027).cos() * 0.3)
.collect();
let mut cpu_out = vec![0.0_f32; n_out];
conv2d_direct(&input, &weight, None, &shape, &mut cpu_out);
let in_storage = upload_f32(&backend, &input);
let w_storage = upload_f32(&backend, &weight);
let out_bytes = backend.alloc_bytes_handle(n_out * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let in_arc = Arc::new(RwLock::new(in_storage));
let w_arc = Arc::new(RwLock::new(w_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Conv2D,
&[DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[
shape.batch,
shape.c_in,
shape.h,
shape.w,
]));
let w_layout = Layout::contiguous(Shape::from_dims(&[
shape.c_out,
shape.c_in,
shape.k_h,
shape.k_w,
]));
let out_layout = Layout::contiguous(Shape::from_dims(&[
shape.batch,
shape.c_out,
shape.h_out(),
shape.w_out(),
]));
kernel(
&[Arc::clone(&in_arc), Arc::clone(&w_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, w_layout, out_layout],
&OpParams::Conv2D {
x_shape: [shape.batch, shape.c_in, shape.h, shape.w],
w_shape: [shape.c_out, shape.c_in, shape.k_h, shape.k_w],
out_shape: [shape.batch, shape.c_out, shape.h_out(), shape.w_out()],
stride: shape.stride,
padding: shape.padding,
dilation: (1, 1),
groups: 1,
},
)
.expect("conv2d dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let mut max_abs = 0.0_f32;
let mut max_rel = 0.0_f32;
for (g, r) in got.iter().zip(cpu_out.iter()) {
let abs = (g - r).abs();
if abs > max_abs {
max_abs = abs;
}
if r.abs() > 1e-4 {
let rel = abs / r.abs();
if rel > max_rel {
max_rel = rel;
}
}
}
eprintln!("conv2d f32: max abs err = {max_abs:e}, max rel err = {max_rel:e}");
assert!(max_abs < 5e-5, "conv2d worst abs err {max_abs:e} > 5e-5");
assert!(max_rel < 1e-4, "conv2d worst rel err {max_rel:e} > 1e-4");
}
#[test]
#[ignore]
fn vulkan_dispatch_conv2d_bf16() {
use fuel_conv::{ConvShape, conv2d_direct};
use half::bf16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let shape = ConvShape {
batch: 1,
c_in: 3,
c_out: 16,
h: 8,
w: 8,
k_h: 3,
k_w: 3,
stride: (1, 1),
padding: (1, 1),
groups: 1,
};
shape.validate().unwrap();
let n_in = shape.batch * shape.c_in * shape.h * shape.w;
let n_w = shape.c_out * shape.c_in * shape.k_h * shape.k_w;
let n_out = shape.output_len();
let input_f32: Vec<f32> = (0..n_in).map(|i| (i as f32 * 0.013).sin() * 0.5).collect();
let weight_f32: Vec<f32> = (0..n_w)
.map(|i| ((i as f32 + 1.0) * 0.027).cos() * 0.3)
.collect();
let input_bf16: Vec<bf16> = input_f32.iter().map(|&x| bf16::from_f32(x)).collect();
let weight_bf16: Vec<bf16> = weight_f32.iter().map(|&x| bf16::from_f32(x)).collect();
let input_q: Vec<f32> = input_bf16.iter().map(|x| x.to_f32()).collect();
let weight_q: Vec<f32> = weight_bf16.iter().map(|x| x.to_f32()).collect();
let mut cpu_out_f32 = vec![0.0_f32; n_out];
conv2d_direct(&input_q, &weight_q, None, &shape, &mut cpu_out_f32);
let cpu_ref: Vec<f32> = cpu_out_f32
.iter()
.map(|&x| bf16::from_f32(x).to_f32())
.collect();
let in_storage = upload_bf16(&backend, &input_bf16);
let w_storage = upload_bf16(&backend, &weight_bf16);
let out_bytes_h = backend
.alloc_bytes_handle((n_out * 2 + 3) & !3)
.expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::BF16);
let in_arc = Arc::new(RwLock::new(in_storage));
let w_arc = Arc::new(RwLock::new(w_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Conv2D,
&[DType::BF16, DType::BF16, DType::BF16],
BackendId::Vulkan,
)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[
shape.batch,
shape.c_in,
shape.h,
shape.w,
]));
let w_layout = Layout::contiguous(Shape::from_dims(&[
shape.c_out,
shape.c_in,
shape.k_h,
shape.k_w,
]));
let out_layout = Layout::contiguous(Shape::from_dims(&[
shape.batch,
shape.c_out,
shape.h_out(),
shape.w_out(),
]));
kernel(
&[Arc::clone(&in_arc), Arc::clone(&w_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, w_layout, out_layout],
&OpParams::Conv2D {
x_shape: [shape.batch, shape.c_in, shape.h, shape.w],
w_shape: [shape.c_out, shape.c_in, shape.k_h, shape.k_w],
out_shape: [shape.batch, shape.c_out, shape.h_out(), shape.w_out()],
stride: shape.stride,
padding: shape.padding,
dilation: (1, 1),
groups: 1,
},
)
.expect("conv2d bf16 dispatch");
let got = download_bf16(&backend, &out_arc.read().unwrap());
let mut max_abs = 0.0_f32;
for (i, (g, r)) in got.iter().zip(cpu_ref.iter()).enumerate() {
let abs = (g.to_f32() - r).abs();
if abs > max_abs {
max_abs = abs;
}
assert!(
abs < 0.05,
"conv2d bf16[{i}]: got {} vs cpu_ref {r}, |diff| = {abs}",
g.to_f32()
);
}
eprintln!("conv2d bf16: max abs err = {max_abs:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_conv2d_f16() {
use fuel_conv::{ConvShape, conv2d_direct};
use half::f16;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let shape = ConvShape {
batch: 1,
c_in: 3,
c_out: 16,
h: 8,
w: 8,
k_h: 3,
k_w: 3,
stride: (1, 1),
padding: (1, 1),
groups: 1,
};
shape.validate().unwrap();
let n_in = shape.batch * shape.c_in * shape.h * shape.w;
let n_w = shape.c_out * shape.c_in * shape.k_h * shape.k_w;
let n_out = shape.output_len();
let input_f32: Vec<f32> = (0..n_in).map(|i| (i as f32 * 0.013).sin() * 0.5).collect();
let weight_f32: Vec<f32> = (0..n_w)
.map(|i| ((i as f32 + 1.0) * 0.027).cos() * 0.3)
.collect();
let input_f16: Vec<f16> = input_f32.iter().map(|&x| f16::from_f32(x)).collect();
let weight_f16: Vec<f16> = weight_f32.iter().map(|&x| f16::from_f32(x)).collect();
let input_q: Vec<f32> = input_f16.iter().map(|x| x.to_f32()).collect();
let weight_q: Vec<f32> = weight_f16.iter().map(|x| x.to_f32()).collect();
let mut cpu_out_f32 = vec![0.0_f32; n_out];
conv2d_direct(&input_q, &weight_q, None, &shape, &mut cpu_out_f32);
let cpu_ref: Vec<f32> = cpu_out_f32
.iter()
.map(|&x| f16::from_f32(x).to_f32())
.collect();
let in_storage = upload_f16(&backend, &input_f16);
let w_storage = upload_f16(&backend, &weight_f16);
let out_bytes_h = backend
.alloc_bytes_handle((n_out * 2 + 3) & !3)
.expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F16);
let in_arc = Arc::new(RwLock::new(in_storage));
let w_arc = Arc::new(RwLock::new(w_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::Conv2D,
&[DType::F16, DType::F16, DType::F16],
BackendId::Vulkan,
)[0]
.kernel;
let in_layout = Layout::contiguous(Shape::from_dims(&[
shape.batch,
shape.c_in,
shape.h,
shape.w,
]));
let w_layout = Layout::contiguous(Shape::from_dims(&[
shape.c_out,
shape.c_in,
shape.k_h,
shape.k_w,
]));
let out_layout = Layout::contiguous(Shape::from_dims(&[
shape.batch,
shape.c_out,
shape.h_out(),
shape.w_out(),
]));
kernel(
&[Arc::clone(&in_arc), Arc::clone(&w_arc)],
&mut [Arc::clone(&out_arc)],
&[in_layout, w_layout, out_layout],
&OpParams::Conv2D {
x_shape: [shape.batch, shape.c_in, shape.h, shape.w],
w_shape: [shape.c_out, shape.c_in, shape.k_h, shape.k_w],
out_shape: [shape.batch, shape.c_out, shape.h_out(), shape.w_out()],
stride: shape.stride,
padding: shape.padding,
dilation: (1, 1),
groups: 1,
},
)
.expect("conv2d f16 dispatch");
let got = download_f16(&backend, &out_arc.read().unwrap());
let mut max_abs = 0.0_f32;
for (i, (g, r)) in got.iter().zip(cpu_ref.iter()).enumerate() {
let abs = (g.to_f32() - r).abs();
if abs > max_abs {
max_abs = abs;
}
assert!(
abs < 0.01,
"conv2d f16[{i}]: got {} vs cpu_ref {r}, |diff| = {abs}",
g.to_f32()
);
}
eprintln!("conv2d f16: max abs err = {max_abs:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_flash_attn_f32_causal() {
use fuel_cpu_backend::CpuStorageBytes;
use fuel_cpu_backend::byte_kernels::flash_attn_f32;
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let b = 1usize;
let hq = 2usize;
let hkv = 2usize;
let sq = 4usize;
let sk = 4usize;
let d = 8usize;
let scale: f32 = 1.0 / (d as f32).sqrt();
let n_q = b * hq * sq * d;
let n_kv = b * hkv * sk * d;
let n_out = n_q;
let q_host: Vec<f32> = (0..n_q).map(|i| (i as f32 * 0.013).sin() * 0.4).collect();
let k_host: Vec<f32> = (0..n_kv)
.map(|i| ((i as f32 + 1.0) * 0.027).cos() * 0.3)
.collect();
let v_host: Vec<f32> = (0..n_kv)
.map(|i| ((i as f32 + 2.0) * 0.019).sin() * 0.5)
.collect();
let q_cpu = CpuStorageBytes::from_bytes(bytemuck::cast_slice(&q_host));
let k_cpu = CpuStorageBytes::from_bytes(bytemuck::cast_slice(&k_host));
let v_cpu = CpuStorageBytes::from_bytes(bytemuck::cast_slice(&v_host));
let mut out_cpu = CpuStorageBytes::from_bytes(&vec![0u8; n_out * 4]);
flash_attn_f32(
&q_cpu,
&k_cpu,
&v_cpu,
None,
&mut out_cpu,
b,
hq,
hkv,
sq,
sk,
d,
sk,
scale,
true,
None,
None,
None,
)
.expect("cpu flash_attn ref");
let cpu_ref: Vec<f32> = bytemuck::cast_slice::<u8, f32>(out_cpu.bytes()).to_vec();
let q_storage = upload_f32(&backend, &q_host);
let k_storage = upload_f32(&backend, &k_host);
let v_storage = upload_f32(&backend, &v_host);
let out_bytes_h = backend.alloc_bytes_handle(n_out * 4).expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes_h), DType::F32);
let q_arc = Arc::new(RwLock::new(q_storage));
let k_arc = Arc::new(RwLock::new(k_storage));
let v_arc = Arc::new(RwLock::new(v_storage));
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
OpKind::FlashAttn,
&[DType::F32, DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let q_layout = Layout::contiguous(Shape::from_dims(&[b, hq, sq, d]));
let k_layout = Layout::contiguous(Shape::from_dims(&[b, hkv, sk, d]));
let v_layout = Layout::contiguous(Shape::from_dims(&[b, hkv, sk, d]));
let out_layout = Layout::contiguous(Shape::from_dims(&[b, hq, sq, d]));
kernel(
&[Arc::clone(&q_arc), Arc::clone(&k_arc), Arc::clone(&v_arc)],
&mut [Arc::clone(&out_arc)],
&[q_layout, k_layout, v_layout, out_layout],
&OpParams::FlashAttn {
b,
hq,
hkv,
sq,
sk,
d,
k_len: sk,
softmax_scale: scale,
causal: true,
window_size_left: None,
window_size_right: None,
softcap: None,
},
)
.expect("vulkan flash_attn dispatch");
let got = download_f32(&backend, &out_arc.read().unwrap());
let mut max_abs = 0.0_f32;
for (i, (g, r)) in got.iter().zip(cpu_ref.iter()).enumerate() {
let abs = (g - r).abs();
if abs > max_abs {
max_abs = abs;
}
assert!(
abs < 1e-4,
"fa f32 causal[{i}]: got {g} vs cpu {r}, |diff| = {abs}"
);
}
eprintln!("flash_attn f32 causal: max abs err = {max_abs:e}");
}
#[test]
#[ignore]
fn vulkan_dispatch_flash_attn_backward_f32_causal() {
use fuel_cpu_backend::CpuStorageBytes;
use fuel_cpu_backend::byte_kernels::{FaBackwardWhich, flash_attn_backward_f32};
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let b = 1usize;
let hq = 2usize;
let hkv = 1usize;
let sq = 4usize;
let sk = 4usize;
let d = 8usize;
let scale: f32 = 1.0 / (d as f32).sqrt();
let q_n = b * hq * sq * d;
let kv_n = b * hkv * sk * d;
let q_host: Vec<f32> = (0..q_n).map(|i| (i as f32 * 0.013).sin() * 0.4).collect();
let k_host: Vec<f32> = (0..kv_n)
.map(|i| ((i as f32 + 1.0) * 0.027).cos() * 0.3)
.collect();
let v_host: Vec<f32> = (0..kv_n)
.map(|i| ((i as f32 + 2.0) * 0.019).sin() * 0.5)
.collect();
let do_host: Vec<f32> = (0..q_n)
.map(|i| ((i as f32 + 3.0) * 0.031).cos() * 0.4)
.collect();
let q_cpu = CpuStorageBytes::from_bytes(bytemuck::cast_slice(&q_host));
let k_cpu = CpuStorageBytes::from_bytes(bytemuck::cast_slice(&k_host));
let v_cpu = CpuStorageBytes::from_bytes(bytemuck::cast_slice(&v_host));
let do_cpu = CpuStorageBytes::from_bytes(bytemuck::cast_slice(&do_host));
let mut dq_cpu = CpuStorageBytes::from_bytes(&vec![0u8; q_n * 4]);
let mut dk_cpu = CpuStorageBytes::from_bytes(&vec![0u8; kv_n * 4]);
let mut dv_cpu = CpuStorageBytes::from_bytes(&vec![0u8; kv_n * 4]);
flash_attn_backward_f32(
&q_cpu,
&k_cpu,
&v_cpu,
&do_cpu,
None,
&mut dq_cpu,
FaBackwardWhich::Q,
b,
hq,
hkv,
sq,
sk,
d,
scale,
true,
None,
None,
None,
)
.expect("cpu dq ref");
flash_attn_backward_f32(
&q_cpu,
&k_cpu,
&v_cpu,
&do_cpu,
None,
&mut dk_cpu,
FaBackwardWhich::K,
b,
hq,
hkv,
sq,
sk,
d,
scale,
true,
None,
None,
None,
)
.expect("cpu dk ref");
flash_attn_backward_f32(
&q_cpu,
&k_cpu,
&v_cpu,
&do_cpu,
None,
&mut dv_cpu,
FaBackwardWhich::V,
b,
hq,
hkv,
sq,
sk,
d,
scale,
true,
None,
None,
None,
)
.expect("cpu dv ref");
let dq_ref: Vec<f32> = bytemuck::cast_slice::<u8, f32>(dq_cpu.bytes()).to_vec();
let dk_ref: Vec<f32> = bytemuck::cast_slice::<u8, f32>(dk_cpu.bytes()).to_vec();
let dv_ref: Vec<f32> = bytemuck::cast_slice::<u8, f32>(dv_cpu.bytes()).to_vec();
let q_st = upload_f32(&backend, &q_host);
let k_st = upload_f32(&backend, &k_host);
let v_st = upload_f32(&backend, &v_host);
let do_st = upload_f32(&backend, &do_host);
let q_arc = Arc::new(RwLock::new(q_st));
let k_arc = Arc::new(RwLock::new(k_st));
let v_arc = Arc::new(RwLock::new(v_st));
let do_arc = Arc::new(RwLock::new(do_st));
let q_layout = Layout::contiguous(Shape::from_dims(&[b, hq, sq, d]));
let k_layout = Layout::contiguous(Shape::from_dims(&[b, hkv, sk, d]));
let v_layout = Layout::contiguous(Shape::from_dims(&[b, hkv, sk, d]));
let do_layout = q_layout.clone();
let op_params = OpParams::FlashAttn {
b,
hq,
hkv,
sq,
sk,
d,
k_len: sk,
softmax_scale: scale,
causal: true,
window_size_left: None,
window_size_right: None,
softcap: None,
};
for (kind, expected, expected_size, name) in [
(OpKind::FlashAttnBackwardQ, &dq_ref, q_n, "dQ"),
(OpKind::FlashAttnBackwardK, &dk_ref, kv_n, "dK"),
(OpKind::FlashAttnBackwardV, &dv_ref, kv_n, "dV"),
] {
let out_bytes = backend
.alloc_bytes_handle(expected_size * 4)
.expect("alloc");
let out_storage = Storage::new(BackendStorage::Vulkan(out_bytes), DType::F32);
let out_arc = Arc::new(RwLock::new(out_storage));
let kernel = table.lookup_alternatives(
kind,
&[DType::F32, DType::F32, DType::F32, DType::F32, DType::F32],
BackendId::Vulkan,
)[0]
.kernel;
let out_layout = if matches!(kind, OpKind::FlashAttnBackwardQ) {
q_layout.clone()
} else {
k_layout.clone()
};
kernel(
&[
Arc::clone(&q_arc),
Arc::clone(&k_arc),
Arc::clone(&v_arc),
Arc::clone(&do_arc),
],
&mut [Arc::clone(&out_arc)],
&[
q_layout.clone(),
k_layout.clone(),
v_layout.clone(),
do_layout.clone(),
out_layout,
],
&op_params,
)
.unwrap_or_else(|e| panic!("vulkan {name} dispatch: {e}"));
let got = download_f32(&backend, &out_arc.read().unwrap());
let mut max_abs = 0.0_f32;
for (i, (g, r)) in got.iter().zip(expected.iter()).enumerate() {
let abs = (g - r).abs();
if abs > max_abs {
max_abs = abs;
}
assert!(
abs < 5e-4,
"fa bw {name}[{i}]: got {g} vs cpu {r}, |diff| = {abs}",
);
}
eprintln!("flash_attn_backward_{name}: max abs err = {max_abs:e}");
}
}
#[test]
fn vulkan_dispatch_per_kernel_precision_and_cost_coverage() {
use fuel_dispatch::fused::PrecisionGuarantee;
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let unaudited_notes = PrecisionGuarantee::UNAUDITED.notes;
let mut precision_failures: Vec<String> = Vec::new();
for (op, dtypes, backend, precision) in table.iter_precision() {
if backend != BackendId::Vulkan {
continue;
}
if precision.notes == unaudited_notes {
precision_failures.push(format!(
"(OpKind::{:?}, {:?}, Vulkan) is `PrecisionGuarantee::\
UNAUDITED` (placeholder for not-yet-audited kernels). \
Either annotate via `register_with_precision` in \
`vulkan_dispatch::register_vulkan_kernels` with a real \
VULKAN_*_PRECISION constant, or use \
`PrecisionGuarantee::none(\"<audit reason>\")` if the \
audit concluded no static bound applies.",
op, dtypes,
));
}
}
let mut cost_failures: Vec<String> = Vec::new();
let unknown_cost_sentinel =
fuel_dispatch::kernel::unknown_cost as fuel_dispatch::kernel::CostFn as usize;
for (op, dtypes, backend, cost) in table.iter_cost() {
if backend != BackendId::Vulkan {
continue;
}
if (cost as usize) == unknown_cost_sentinel {
cost_failures.push(format!(
"(OpKind::{:?}, {:?}, Vulkan) still uses `unknown_cost`. \
Either add a Vulkan-aware arm in `default_cost_for_op_kind` \
or call `register_full` with an explicit CostFn before \
`fill_unset_cost_for_backend(Vulkan, ...)` at the end of \
`register_vulkan_kernels`.",
op, dtypes,
));
}
}
assert!(
precision_failures.is_empty(),
"Vulkan PrecisionGuarantee coverage lint failed:\n{}",
precision_failures.join("\n"),
);
assert!(
cost_failures.is_empty(),
"Vulkan CostFn coverage lint failed:\n{}",
cost_failures.join("\n"),
);
}
#[test]
fn vulkan_dispatch_copy_f32_registered() {
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let alts =
table.lookup_alternatives(OpKind::Copy, &[DType::F32, DType::F32], BackendId::Vulkan);
assert_eq!(
alts.len(),
1,
"expected 1 Vulkan alternative for OpKind::Copy [F32, F32] after \
register_vulkan_kernels, got {}",
alts.len(),
);
}
#[test]
#[ignore]
fn vulkan_fill_bytes_zero_lives_on_device() {
let Some(backend) = backend_or_skip() else {
return;
};
let n_bytes = 64 * 4;
let storage = backend
.alloc_bytes_handle(n_bytes)
.expect("alloc_bytes_handle");
backend.fill_bytes_zero(&storage).expect("fill_bytes_zero");
let bytes = backend.download_bytes(&storage).expect("download_bytes");
assert_eq!(bytes.len(), n_bytes);
assert!(
bytes.iter().all(|&b| b == 0),
"fill_bytes_zero must produce all-zero bytes; got first few = {:?}",
&bytes[..bytes.len().min(8)],
);
}
#[test]
#[ignore]
fn vulkan_dispatch_copy_to_cpu_f32_direct_wrapper() {
let Some(backend) = backend_or_skip() else {
return;
};
let mut table = KernelBindingTable::new();
register_vulkan_kernels(&mut table);
let host: Vec<f32> = (0..32).map(|i| i as f32 * 0.5).collect();
let n = host.len();
let vk_storage = upload_f32(&backend, &host);
let cpu_out = alloc_cpu_zeroed(DType::F32, n).expect("alloc cpu out");
let vk_arc = Arc::new(RwLock::new(vk_storage));
let cpu_arc = Arc::new(RwLock::new(cpu_out));
let alts =
table.lookup_alternatives(OpKind::Copy, &[DType::F32, DType::F32], BackendId::Vulkan);
assert!(!alts.is_empty(), "no Vulkan Op::Copy registration");
let kernel = alts[0].kernel;
let layout = Layout::contiguous(Shape::from_dims(&[n]));
let layouts = vec![layout.clone(), layout];
kernel(
&[Arc::clone(&vk_arc)],
&mut [Arc::clone(&cpu_arc)],
&layouts,
&OpParams::None,
)
.expect("copy_to_cpu_vulkan dispatch");
let guard = cpu_arc.read().unwrap();
let got: &[f32] = match &guard.inner {
BackendStorage::Cpu(c) => c.as_slice().expect("f32 cast"),
_ => panic!("output must be BackendStorage::Cpu after Op::Copy {{ target: Cpu }}"),
};
assert_eq!(got, host.as_slice(), "Vulkan→CPU Op::Copy bytes mismatch");
}