use crate::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Target {
Cpu,
Cuda,
Rocm,
Metal,
Vulkan,
WebGpu,
}
impl Target {
pub fn of<R: Runtime>(client: &ComputeClient<R>) -> Target {
Target::from_runtime_name(R::name(client))
}
pub fn from_runtime_name(name: &str) -> Target {
match name {
"cuda" => Target::Cuda,
"hip" => Target::Rocm,
n if n.contains("spirv") => Target::Vulkan,
n if n.contains("msl") => Target::Metal,
n if n.starts_with("wgpu") => Target::WebGpu,
_ => Target::Cpu,
}
}
}
#[kernel(targets(cuda, metal, vulkan, webgpu, cpu), unchecked)]
pub fn matvec_island<F: Float>(
wq: &Array<Vector<i8, Const<4>>>, xq: &Array<Vector<i8, Const<4>>>, wd: &Array<F>, out: &mut Array<F>,
#[comptime] k: usize,
#[comptime] target: Target,
) {
let row = ABSOLUTE_POS;
if row < out.len() {
let ng = k / 4;
let nb = k / 32;
let wbase = row * ng;
let dbase = row * nb;
let mut acc = F::new(0.0);
for g in 0..ng {
let w = Vector::<i32, Const<4>>::cast_from(wq[wbase + g]);
let x = Vector::<i32, Const<4>>::cast_from(xq[g]);
let dp = island! {
cuda | rocm | metal | vulkan => { w.dot(x) }
default => { w[0] * x[0] + w[1] * x[1] + w[2] * x[2] + w[3] * x[3] }
};
acc += wd[dbase + g / 8] * F::cast_from(dp);
}
out[row] = acc;
}
}
pub fn matvec_island_run<R: Runtime>(
client: &ComputeClient<R>,
wq: &[i8],
xq: &[i8],
wd: &[f32],
rows: usize,
k: usize,
) -> Vec<f32> {
matvec_island_run_with(client, wq, xq, wd, rows, k, Target::of(client))
}
pub fn matvec_island_run_with<R: Runtime>(
client: &ComputeClient<R>,
wq: &[i8],
xq: &[i8],
wd: &[f32],
rows: usize,
k: usize,
target: Target,
) -> Vec<f32> {
let wqh = client.create_from_slice(i8::as_bytes(wq));
let xqh = client.create_from_slice(i8::as_bytes(xq));
let wdh = client.create_from_slice(f32::as_bytes(wd));
let oh = client.create_from_slice(f32::as_bytes(&vec![0.0f32; rows]));
let block = 64u32;
let grid = (rows as u32).div_ceil(block);
let ng = k / 4;
unsafe {
matvec_island::launch_unchecked::<f32, R>(
client,
Grid::Static(grid, 1, 1),
Block::new_1d(block),
ArrayArg::from_raw_parts(wqh.clone(), rows * ng),
ArrayArg::from_raw_parts(xqh.clone(), ng),
ArrayArg::from_raw_parts(wdh.clone(), wd.len()),
ArrayArg::from_raw_parts(oh.clone(), rows),
k,
target,
);
}
f32::from_bytes(&client.read_one_unchecked(oh)).to_vec()
}
pub fn matvec_island_ref(wq: &[i8], xq: &[i8], wd: &[f32], rows: usize, k: usize) -> Vec<f32> {
let nb = k / 32;
(0..rows)
.map(|row| {
let mut acc = 0.0f32;
for g in 0..k / 4 {
let mut dp = 0i32;
for l in 0..4 {
dp += wq[row * k + g * 4 + l] as i32 * xq[g * 4 + l] as i32;
}
acc += wd[row * nb + g / 8] * dp as f32;
}
acc
})
.collect()
}
pub fn gen_island(rows: usize, k: usize) -> (Vec<i8>, Vec<i8>, Vec<f32>) {
let mut s = 0xD1B5_4A32_D192_ED03u64;
let mut next = || {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
s
};
let wq: Vec<i8> = (0..rows * k).map(|_| next() as i8).collect();
let xq: Vec<i8> = (0..k).map(|_| next() as i8).collect();
let wd: Vec<f32> = (0..rows * (k / 32))
.map(|_| half::f16::from_f32((next() % 1000) as f32 / 8000.0 + 0.01).to_f32())
.collect();
(wq, xq, wd)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn runtime_name_maps_to_target() {
assert_eq!(Target::from_runtime_name("cpu"), Target::Cpu);
assert_eq!(Target::from_runtime_name("cuda"), Target::Cuda);
assert_eq!(Target::from_runtime_name("hip"), Target::Rocm);
assert_eq!(Target::from_runtime_name("wgpu<spirv>"), Target::Vulkan);
assert_eq!(Target::from_runtime_name("wgpu<msl>"), Target::Metal);
assert_eq!(Target::from_runtime_name("wgpu<wgsl>"), Target::WebGpu);
assert_eq!(Target::from_runtime_name("something-else"), Target::Cpu);
}
#[cfg(feature = "cpu")]
fn cpu_client() -> ComputeClient<cubecl::cpu::CpuRuntime> {
use cubecl::cpu::{CpuDevice, CpuRuntime};
CpuRuntime::client(&CpuDevice)
}
#[cfg(feature = "cpu")]
fn bits(v: &[f32]) -> Vec<u32> {
v.iter().map(|x| x.to_bits()).collect()
}
#[cfg(feature = "cpu")]
#[test]
fn island_arm_equals_default_oracle_bit_exact() {
use cubecl::cpu::CpuRuntime;
let client = cpu_client();
let (rows, k) = (12usize, 256usize);
let (wq, xq, wd) = gen_island(rows, k);
let default = matvec_island_run_with::<CpuRuntime>(&client, &wq, &xq, &wd, rows, k, Target::Cpu);
let reference = matvec_island_ref(&wq, &xq, &wd, rows, k);
assert_eq!(bits(&default), bits(&reference), "default arm != plain-Rust oracle");
for accel in [Target::Cuda, Target::Rocm, Target::Metal, Target::Vulkan, Target::WebGpu] {
let out = matvec_island_run_with::<CpuRuntime>(&client, &wq, &xq, &wd, rows, k, accel);
assert_eq!(bits(&out), bits(&default), "island arm {accel:?} diverged from default");
}
}
#[cfg(feature = "cpu")]
#[test]
fn production_run_selects_oracle_on_cpu() {
use cubecl::cpu::CpuRuntime;
let client = cpu_client();
let (rows, k) = (7usize, 128usize);
let (wq, xq, wd) = gen_island(rows, k);
let out = matvec_island_run::<CpuRuntime>(&client, &wq, &xq, &wd, rows, k);
assert_eq!(bits(&out), bits(&matvec_island_ref(&wq, &xq, &wd, rows, k)));
}
}