#![allow(missing_docs)]
use cubecl::prelude::*;
use cubecl_utils_rs::prelude::*;
use rayon::prelude::*;
use crate::errors::BixverseErrors;
use crate::gpu::linalg::sparse_gpu::GpuCompressedSparseData;
use crate::prelude::CompressedSparseFormat;
pub const B_ARGMIN_BLOCKS: u32 = 1024;
pub const B_REDUCE_WG: u32 = 256;
pub const B_ARGMIN_WG_TIERS: [(u32, usize); 3] = [(128, 32), (256, 32), (512, 20)];
pub fn b_argmin_workgroup(k: usize) -> Option<u32> {
B_ARGMIN_WG_TIERS
.iter()
.find(|&&(wg, max_slots)| k.div_ceil(wg as usize) <= max_slots)
.map(|&(wg, _)| wg)
}
pub const A_COLUMNS_BLOCKS: u32 = 1024;
pub const A_COLUMNS_WG: u32 = 128;
pub const A_COLUMNS_MAX_SLOTS: usize = 32;
pub const A_COLUMNS_WG_TIERS: [(u32, usize); 3] = [(128, 32), (256, 32), (512, 24)];
pub const MIN_PLANE_WIDTH: u32 = 32;
pub const fn reduce_scratch_len(wg_size: u32, use_plane: bool) -> usize {
if use_plane {
let planes = wg_size / MIN_PLANE_WIDTH;
if planes == 0 { 1 } else { planes as usize }
} else {
wg_size as usize
}
}
pub const A_RENORM_FLOOR: f32 = 1e-15;
#[cube]
pub fn lower_bound(indices: &Tensor<u32>, start: u32, end: u32, target: u32) -> u32 {
let mut lo = start;
let mut hi = end;
while lo < hi {
let mid = lo + (hi - lo) / 2u32;
if indices[mid as usize] < target {
lo = mid + 1u32;
} else {
hi = mid;
}
}
lo
}
#[cube]
pub fn accumulate_segmented_row<F: Float>(
acc: &mut Array<F>,
indices: &Tensor<u32>,
values: &Tensor<F>,
seg: &Tensor<u32>,
seg_base: u32,
tx: u32,
base: u32,
scale: F,
#[comptime] slots: usize,
) {
let mut p = seg[(seg_base + tx) as usize];
let hi = seg[(seg_base + tx + 1u32) as usize];
while p < hi {
let col = indices[p as usize];
let v = values[p as usize] * scale;
#[unroll]
for s in 0..slots {
if col == base + comptime!(s as u32) {
acc[s] += v;
}
}
p += 1u32;
}
}
#[cube]
pub fn accumulate_sparse_row<F: Float>(
acc: &mut Array<F>,
indices: &Tensor<u32>,
values: &Tensor<F>,
row_start: u32,
row_end: u32,
base: u32,
scale: F,
#[comptime] slots: usize,
) {
let stop = base + comptime!(slots as u32);
let mut p = lower_bound(indices, row_start, row_end, base);
let mut walking = p < row_end;
while walking {
let col = indices[p as usize];
if col < stop {
let v = values[p as usize] * scale;
#[unroll]
for s in 0..slots {
if col == base + comptime!(s as u32) {
acc[s] += v;
}
}
p += 1u32;
walking = p < row_end;
} else {
walking = false;
}
}
}
#[allow(clippy::too_many_arguments)]
#[cube(launch_unchecked)]
pub fn fw_argmin_b<F: Float>(
k2b_indptr: &Tensor<u32>,
k2b_indices: &Tensor<u32>,
k2b_values: &Tensor<F>,
t1_indices: &Tensor<u32>,
t1_values: &Tensor<F>,
t1_seg: &Tensor<u32>,
t2_indptr: &Tensor<u32>,
t2_indices: &Tensor<u32>,
t2_values: &Tensor<F>,
b_indptr: &Tensor<u32>,
b_indices: &Tensor<u32>,
b_values: &Tensor<F>,
best_val: &mut Tensor<F>,
best_idx: &mut Tensor<u32>,
gap_partial: &mut Tensor<F>,
n: u32,
#[comptime] k: usize,
#[comptime] slots: usize,
#[comptime] wg_size: u32,
) {
let block = CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X;
let tx = UNIT_POS_X;
let mut acc = Array::<F>::new(slots);
let mut run_val = Array::<F>::new(slots);
let mut run_idx = Array::<u32>::new(slots);
#[unroll]
for s in 0..slots {
run_val[s] = F::max_value();
run_idx[s] = 0u32;
}
let base = tx * comptime!(slots as u32);
let mut gap = F::new(0.0);
let mut row = block;
while row < n {
let row_us = row as usize;
#[unroll]
for s in 0..slots {
acc[s] = F::new(0.0);
}
let k2b_start = k2b_indptr[row_us];
let k2b_end = k2b_indptr[row_us + 1];
let mut p = k2b_start;
while p < k2b_end {
let m = k2b_indices[p as usize];
accumulate_segmented_row::<F>(
&mut acc,
t1_indices,
t1_values,
t1_seg,
m * comptime!(wg_size + 1),
tx,
base,
k2b_values[p as usize],
slots,
);
p += 1u32;
}
let t2_start = t2_indptr[row_us];
let t2_end = t2_indptr[row_us + 1];
let mut q = t2_start;
while q < t2_end {
let col = t2_indices[q as usize];
let v = t2_values[q as usize];
#[unroll]
for s in 0..slots {
if base + comptime!(s as u32) == col {
acc[s] -= v;
}
}
q += 1u32;
}
let b_start = b_indptr[row_us];
let b_end = b_indptr[row_us + 1];
let mut r = b_start;
while r < b_end {
let col = b_indices[r as usize];
let v = b_values[r as usize];
#[unroll]
for s in 0..slots {
if base + comptime!(s as u32) == col {
gap += v * acc[s];
}
}
r += 1u32;
}
#[unroll]
for s in 0..slots {
let col = base + comptime!(s as u32);
if col < comptime!(k as u32) && acc[s] < run_val[s] {
run_val[s] = acc[s];
run_idx[s] = row;
}
}
row += CUBE_COUNT_X * CUBE_COUNT_Y;
}
let out_base = block as usize * k;
#[unroll]
for s in 0..slots {
let col = base + comptime!(s as u32);
if col < comptime!(k as u32) {
best_val[out_base + col as usize] = run_val[s];
best_idx[out_base + col as usize] = run_idx[s];
}
}
let mut shared = SharedMemory::<F>::new(wg_size as usize);
shared[tx as usize] = gap;
sync_cube();
let mut stride = (wg_size / 2u32).runtime();
while stride > 0u32 {
if tx < stride {
let other = shared[(tx + stride) as usize];
shared[tx as usize] += other;
}
sync_cube();
stride /= 2u32;
}
if tx == 0u32 {
gap_partial[block as usize] = shared[0];
}
}
#[cube(launch_unchecked)]
pub fn reduce_argmin_blocks<F: Float>(
part_val: &Tensor<F>,
part_idx: &Tensor<u32>,
out_val: &mut Tensor<F>,
out_idx: &mut Tensor<u32>,
n_blocks: u32,
k: u32,
#[comptime] wg_size: u32,
) {
let col = (CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X) * wg_size + UNIT_POS_X;
if col >= k {
terminate!();
}
let mut best = F::max_value();
let mut best_row = u32::MAX.runtime();
let mut block = 0u32;
while block < n_blocks {
let offset = (block * k + col) as usize;
let val = part_val[offset];
let idx = part_idx[offset];
if val < best || (val == best && idx < best_row) {
best = val;
best_row = idx;
}
block += 1u32;
}
out_val[col as usize] = best;
out_idx[col as usize] = best_row;
}
#[allow(clippy::too_many_arguments, unused_assignments)]
#[cube(launch_unchecked)]
pub fn fw_columns_a_gpu<F: Float>(
t1_indices: &Tensor<u32>,
t1_values: &Tensor<F>,
t1_seg: &Tensor<u32>,
ap_indptr: &Tensor<u32>,
ap_indices: &Tensor<u32>,
ap_values: &Tensor<F>,
k2b_indptr: &Tensor<u32>,
k2b_indices: &Tensor<u32>,
k2b_values: &Tensor<F>,
atom_idx: &mut Tensor<u32>,
atom_val: &mut Tensor<F>,
atom_cnt: &mut Tensor<u32>,
threshold: &Tensor<F>,
n: u32,
n_iters: u32,
cap: u32,
#[comptime] k: usize,
#[comptime] slots: usize,
#[comptime] wg_size: u32,
#[comptime] cap_pad: usize,
#[comptime] pruning: bool,
#[comptime] use_plane: bool,
) {
let block = CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X;
let tx = UNIT_POS_X;
let mut w = Array::<F>::new(slots);
let mut k2b = Array::<F>::new(slots);
let mut s_atom_idx = SharedMemory::<u32>::new(cap_pad);
let mut s_atom_val = SharedMemory::<F>::new(cap_pad);
let mut s_dropped = SharedMemory::<F>::new(cap_pad);
let mut s_val = SharedMemory::<F>::new(comptime!(reduce_scratch_len(wg_size, use_plane)));
let mut s_idx = SharedMemory::<u32>::new(comptime!(reduce_scratch_len(wg_size, use_plane)));
let mut s_count = SharedMemory::<u32>::new(1usize);
let mut s_found = SharedMemory::<u32>::new(1usize);
let mut s_any_drop = SharedMemory::<u32>::new(1usize);
let mut s_amin = SharedMemory::<u32>::new(1usize);
let mut s_total = SharedMemory::<F>::new(1usize);
let base = tx * comptime!(slots as u32);
let mut cell = block;
while cell < n {
let cell_us = cell as usize;
#[unroll]
for s in 0..slots {
k2b[s] = F::new(0.0);
w[s] = F::new(0.0);
}
accumulate_sparse_row::<F>(
&mut k2b,
k2b_indices,
k2b_values,
k2b_indptr[cell_us],
k2b_indptr[cell_us + 1],
base,
F::new(1.0),
slots,
);
let seed_start = ap_indptr[cell_us];
let seed_end = ap_indptr[cell_us + 1];
let seed_len = seed_end - seed_start;
if tx < cap {
s_atom_idx[tx as usize] = u32::MAX.runtime();
s_atom_val[tx as usize] = F::new(0.0);
}
if tx < seed_len {
s_atom_idx[tx as usize] = ap_indices[(seed_start + tx) as usize];
s_atom_val[tx as usize] = ap_values[(seed_start + tx) as usize];
}
if tx == 0u32 {
s_count[0] = seed_len;
}
sync_cube();
let mut q = seed_start;
while q < seed_end {
let row = ap_indices[q as usize];
accumulate_segmented_row::<F>(
&mut w,
t1_indices,
t1_values,
t1_seg,
row * comptime!(wg_size + 1),
tx,
base,
ap_values[q as usize],
slots,
);
q += 1u32;
}
let mut t = 0u32;
while t < n_iters {
let mut best = F::max_value();
let mut best_col = 0u32;
#[unroll]
for s in 0..slots {
let col = base + comptime!(s as u32);
if col < comptime!(k as u32) {
let grad = w[s] - k2b[s];
if grad < best {
best = grad;
best_col = col;
}
}
}
let mut amin = u32::MAX.runtime();
if use_plane {
let plane_best = plane_min(best);
let mut candidate = u32::MAX.runtime();
if best == plane_best {
candidate = best_col;
}
let plane_col = plane_min(candidate);
let plane_id = tx / PLANE_DIM;
if UNIT_POS_PLANE == 0u32 {
s_val[plane_id as usize] = plane_best;
s_idx[plane_id as usize] = plane_col;
}
sync_cube();
let n_planes = CUBE_DIM_X / PLANE_DIM;
let mut lvl2_val = F::max_value();
let mut lvl2_idx = u32::MAX.runtime();
if tx < n_planes {
lvl2_val = s_val[tx as usize];
lvl2_idx = s_idx[tx as usize];
}
let lvl2_best = plane_min(lvl2_val);
let mut lvl2_cand = u32::MAX.runtime();
if lvl2_val == lvl2_best {
lvl2_cand = lvl2_idx;
}
let lvl2_col = plane_min(lvl2_cand);
if tx == 0u32 {
s_amin[0] = lvl2_col;
}
sync_cube();
amin = s_amin[0];
} else {
s_val[tx as usize] = best;
s_idx[tx as usize] = best_col;
sync_cube();
let mut stride = (wg_size / 2u32).runtime();
while stride > 0u32 {
if tx < stride {
let other_val = s_val[(tx + stride) as usize];
let other_idx = s_idx[(tx + stride) as usize];
let mine_val = s_val[tx as usize];
let mine_idx = s_idx[tx as usize];
if other_val < mine_val || (other_val == mine_val && other_idx < mine_idx) {
s_val[tx as usize] = other_val;
s_idx[tx as usize] = other_idx;
}
}
sync_cube();
stride /= 2u32;
}
amin = s_idx[0];
}
let gamma = F::new(2.0) / (F::cast_from(t) + F::new(2.0));
let retain = F::new(1.0) - gamma;
let count = s_count[0];
if tx < count {
s_atom_val[tx as usize] *= retain;
}
if tx == 0u32 {
s_found[0] = u32::MAX.runtime();
}
sync_cube();
if tx < count && s_atom_idx[tx as usize] == amin {
s_found[0] = tx;
}
sync_cube();
if tx == 0u32 {
let found = s_found[0];
if found == u32::MAX.runtime() {
s_atom_idx[count as usize] = amin;
s_atom_val[count as usize] = gamma;
s_count[0] = count + 1u32;
} else {
s_atom_val[found as usize] += gamma;
}
}
#[unroll]
for s in 0..slots {
w[s] *= retain;
}
accumulate_segmented_row::<F>(
&mut w,
t1_indices,
t1_values,
t1_seg,
amin * comptime!(wg_size + 1),
tx,
base,
gamma,
slots,
);
sync_cube();
if pruning {
let live = s_count[0];
if tx == 0u32 {
s_any_drop[0] = 0u32;
}
if tx < cap {
s_dropped[tx as usize] = F::new(0.0);
}
sync_cube();
let thr = threshold[0];
if tx < live {
let weight = s_atom_val[tx as usize];
let above = weight > thr || weight < -thr;
if !above && weight != F::new(0.0) {
s_dropped[tx as usize] = weight;
s_atom_val[tx as usize] = F::new(0.0);
s_any_drop[0] = 1u32;
}
}
sync_cube();
if s_any_drop[0] == 1u32 {
let mut a = 0u32;
while a < live {
let weight = s_dropped[a as usize];
if weight != F::new(0.0) {
let row = s_atom_idx[a as usize];
accumulate_segmented_row::<F>(
&mut w,
t1_indices,
t1_values,
t1_seg,
row * comptime!(wg_size + 1),
tx,
base,
F::new(0.0) - weight,
slots,
);
}
a += 1u32;
}
}
let mut mass = F::new(0.0);
if tx < live {
mass = s_atom_val[tx as usize];
}
let mut total = F::new(0.0);
if use_plane {
let plane_mass = plane_sum(mass);
let plane_id = tx / PLANE_DIM;
if UNIT_POS_PLANE == 0u32 {
s_val[plane_id as usize] = plane_mass;
}
sync_cube();
let n_planes = CUBE_DIM_X / PLANE_DIM;
let mut lvl2 = F::new(0.0);
if tx < n_planes {
lvl2 = s_val[tx as usize];
}
let lvl2_total = plane_sum(lvl2);
if tx == 0u32 {
s_total[0] = lvl2_total;
}
sync_cube();
total = s_total[0];
} else {
s_val[tx as usize] = mass;
sync_cube();
let mut sum_stride = (wg_size / 2u32).runtime();
while sum_stride > 0u32 {
if tx < sum_stride {
let other = s_val[(tx + sum_stride) as usize];
s_val[tx as usize] += other;
}
sync_cube();
sum_stride /= 2u32;
}
total = s_val[0];
}
let mut renorm = F::new(1.0);
if total > F::new(A_RENORM_FLOOR) {
renorm = F::new(1.0) / total;
}
if tx < live {
s_atom_val[tx as usize] *= renorm;
}
#[unroll]
for s in 0..slots {
w[s] *= renorm;
}
sync_cube();
}
t += 1u32;
}
let final_count = s_count[0];
let out_base = cell_us * cap as usize;
if tx < final_count {
atom_idx[out_base + tx as usize] = s_atom_idx[tx as usize];
atom_val[out_base + tx as usize] = s_atom_val[tx as usize];
}
if tx == 0u32 {
atom_cnt[cell_us] = final_count;
}
sync_cube();
cell += CUBE_COUNT_X * CUBE_COUNT_Y;
}
}
#[allow(clippy::too_many_arguments)]
pub fn launch_fw_argmin_b<R, F>(
k2b: &GpuCompressedSparseData<R, F>,
t1: &GpuCompressedSparseData<R, F>,
t1_seg: &GpuTensor<R, u32>,
t2: &GpuCompressedSparseData<R, F>,
b_mat: &GpuCompressedSparseData<R, F>,
part_val: &GpuTensor<R, F>,
part_idx: &GpuTensor<R, u32>,
gap_partial: &GpuTensor<R, F>,
out_val: &GpuTensor<R, F>,
out_idx: &GpuTensor<R, u32>,
n: usize,
k: usize,
client: &ComputeClient<R>,
) -> Result<(), BixverseErrors>
where
R: Runtime,
F: Float + cubecl::CubeElement,
{
for mat in [k2b, t1, t2, b_mat] {
if !mat.cs_type.is_csr() {
return Err(BixverseErrors::SparseLayoutMismatch {
expected: CompressedSparseFormat::Csr,
got: mat.cs_type,
});
}
}
let limits = GpuLimits::from_client(client);
let limit = limits.max_binding_bytes as usize;
let indptr_bytes = (n + 1) * size_of::<u32>();
let checked = [
("K2B values", k2b.nnz * size_of::<F>()),
("K2B indices", k2b.nnz * size_of::<u32>()),
("K2B indptr", indptr_bytes),
("K2At values", t2.nnz * size_of::<F>()),
("K2At indices", t2.nnz * size_of::<u32>()),
("K2At indptr", indptr_bytes),
("B values", b_mat.nnz * size_of::<F>()),
("B indices", b_mat.nnz * size_of::<u32>()),
("B indptr", indptr_bytes),
("t1 values", t1.nnz * size_of::<F>()),
("t1 indices", t1.nnz * size_of::<u32>()),
("t1 segments", t1_seg.len() * size_of::<u32>()),
("argmin partial values", part_val.len() * size_of::<F>()),
("argmin partial indices", part_idx.len() * size_of::<u32>()),
];
for (buffer, bytes) in checked {
if bytes > limit {
return Err(BixverseErrors::GpuBindingTooLarge {
buffer,
bytes,
limit,
});
}
}
let wg = b_argmin_workgroup(k).ok_or_else(|| {
BixverseErrors::InvalidArgument(format!(
"fw_argmin_b: no workgroup tier covers k = {}, tiers {:?}",
k, B_ARGMIN_WG_TIERS
))
})?;
let blocks = B_ARGMIN_BLOCKS.min(n.max(1) as u32);
let (gx, gy) = grid_2d(blocks, &limits)?;
let count = checked_cube_count("fw_argmin_b", gx, gy, 1, &limits)?;
macro_rules! dispatch {
($wg:expr) => {{
unsafe {
fw_argmin_b::launch_unchecked::<F, R>(
client,
count,
CubeDim::new_1d($wg),
k2b.indptr.clone().into_tensor_arg(),
k2b.indices.clone().into_tensor_arg(),
k2b.values.clone().into_tensor_arg(),
t1.indices.clone().into_tensor_arg(),
t1.values.clone().into_tensor_arg(),
t1_seg.clone().into_tensor_arg(),
t2.indptr.clone().into_tensor_arg(),
t2.indices.clone().into_tensor_arg(),
t2.values.clone().into_tensor_arg(),
b_mat.indptr.clone().into_tensor_arg(),
b_mat.indices.clone().into_tensor_arg(),
b_mat.values.clone().into_tensor_arg(),
part_val.clone().into_tensor_arg(),
part_idx.clone().into_tensor_arg(),
gap_partial.clone().into_tensor_arg(),
n as u32,
k,
k.div_ceil($wg as usize),
$wg,
);
}
}};
}
match wg {
128 => dispatch!(128u32),
256 => dispatch!(256u32),
512 => dispatch!(512u32),
other => {
return Err(BixverseErrors::InvalidArgument(format!(
"fw_argmin_b: workgroup width {} is not one of the compiled tiers {:?}",
other, B_ARGMIN_WG_TIERS
)));
}
}
let reduce_cubes = (k as u32).div_ceil(B_REDUCE_WG);
let (rx, ry) = grid_2d(reduce_cubes, &limits)?;
let reduce_count = checked_cube_count("reduce_argmin_blocks", rx, ry, 1, &limits)?;
unsafe {
reduce_argmin_blocks::launch_unchecked::<F, R>(
client,
reduce_count,
CubeDim::new_1d(B_REDUCE_WG),
part_val.clone().into_tensor_arg(),
part_idx.clone().into_tensor_arg(),
out_val.clone().into_tensor_arg(),
out_idx.clone().into_tensor_arg(),
blocks,
k as u32,
B_REDUCE_WG,
);
}
Ok(())
}
pub fn plane_reduce_viable(wg_size: u32, limits: &GpuLimits) -> bool {
plane_partitions(wg_size, limits).is_some_and(|n_planes| n_planes <= wg_size / MIN_PLANE_WIDTH)
}
#[allow(clippy::too_many_arguments)]
pub fn launch_fw_columns_a<R, F>(
t1: &GpuCompressedSparseData<R, F>,
t1_seg: &GpuTensor<R, u32>,
a_prev_t: &GpuCompressedSparseData<R, F>,
k2b: &GpuCompressedSparseData<R, F>,
atom_idx: &GpuTensor<R, u32>,
atom_val: &GpuTensor<R, F>,
atom_cnt: &GpuTensor<R, u32>,
threshold: &GpuTensor<R, F>,
n: usize,
k: usize,
n_iters: usize,
cap: u32,
pruning: Option<f32>,
use_plane: Option<bool>,
wg_override: Option<u32>,
client: &ComputeClient<R>,
) -> Result<(), BixverseErrors>
where
R: Runtime,
F: Float + cubecl::CubeElement,
{
for mat in [t1, a_prev_t, k2b] {
if !mat.cs_type.is_csr() {
return Err(BixverseErrors::SparseLayoutMismatch {
expected: CompressedSparseFormat::Csr,
got: mat.cs_type,
});
}
}
let wg = match wg_override.or_else(|| a_columns_workgroup(k, cap as usize)) {
Some(wg) => wg,
None => {
return Err(BixverseErrors::InvalidArgument(format!(
"fw_columns_a: no workgroup tier covers k = {} at atom capacity {}, tiers {:?} \
within {} slots",
k, cap, A_COLUMNS_WG_TIERS, A_COLUMNS_MAX_SLOTS
)));
}
};
if cap > wg {
return Err(BixverseErrors::InvalidArgument(format!(
"fw_columns_a: atom capacity {} exceeds the workgroup width {}",
cap, wg
)));
}
let limits = GpuLimits::from_client(client);
let limit = limits.max_binding_bytes as usize;
let indptr_bytes = (n + 1) * size_of::<u32>();
let checked = [
("t1 values", t1.nnz * size_of::<F>()),
("t1 indices", t1.nnz * size_of::<u32>()),
("t1 segments", t1_seg.len() * size_of::<u32>()),
("A_prev^T values", a_prev_t.nnz * size_of::<F>()),
("A_prev^T indices", a_prev_t.nnz * size_of::<u32>()),
("A_prev^T indptr", indptr_bytes),
("K2B values", k2b.nnz * size_of::<F>()),
("K2B indices", k2b.nnz * size_of::<u32>()),
("K2B indptr", indptr_bytes),
("atom indices", n * cap as usize * size_of::<u32>()),
("atom weights", n * cap as usize * size_of::<F>()),
("atom counts", n * size_of::<u32>()),
];
for (buffer, bytes) in checked {
if bytes > limit {
return Err(BixverseErrors::GpuBindingTooLarge {
buffer,
bytes,
limit,
});
}
}
let blocks = A_COLUMNS_BLOCKS.min(n.max(1) as u32);
let (gx, gy) = grid_2d(blocks, &limits)?;
let count = checked_cube_count("fw_columns_a_gpu", gx, gy, 1, &limits)?;
let cap_pad = (cap as usize).next_power_of_two().max(1);
let planes = use_plane.unwrap_or_else(|| plane_reduce_viable(wg, &limits));
macro_rules! dispatch {
($wg:expr) => {{
unsafe {
fw_columns_a_gpu::launch_unchecked::<F, R>(
client,
count,
CubeDim::new_1d($wg),
t1.indices.clone().into_tensor_arg(),
t1.values.clone().into_tensor_arg(),
t1_seg.clone().into_tensor_arg(),
a_prev_t.indptr.clone().into_tensor_arg(),
a_prev_t.indices.clone().into_tensor_arg(),
a_prev_t.values.clone().into_tensor_arg(),
k2b.indptr.clone().into_tensor_arg(),
k2b.indices.clone().into_tensor_arg(),
k2b.values.clone().into_tensor_arg(),
atom_idx.clone().into_tensor_arg(),
atom_val.clone().into_tensor_arg(),
atom_cnt.clone().into_tensor_arg(),
threshold.clone().into_tensor_arg(),
n as u32,
n_iters as u32,
cap,
k,
k.div_ceil($wg as usize),
$wg,
cap_pad,
pruning.is_some(),
planes,
);
}
}};
}
match wg {
128 => dispatch!(128u32),
256 => dispatch!(256u32),
512 => dispatch!(512u32),
1024 => dispatch!(1024u32),
other => {
return Err(BixverseErrors::InvalidArgument(format!(
"fw_columns_a: workgroup width {} is not one of the compiled tiers {:?}",
other, A_COLUMNS_WG_TIERS
)));
}
}
Ok(())
}
pub fn a_columns_workgroup(k: usize, cap: usize) -> Option<u32> {
A_COLUMNS_WG_TIERS
.iter()
.find(|&&(wg, max_slots)| {
k.div_ceil(wg as usize) <= max_slots.min(A_COLUMNS_MAX_SLOTS) && cap <= wg as usize
})
.map(|&(wg, _)| wg)
}
pub fn a_columns_segments(
t1: &crate::prelude::CompressedSparseData2<f32>,
wg: u32,
slots: usize,
) -> Vec<u32> {
let k = t1.shape.0;
let stride = wg as usize + 1;
let mut seg = vec![0u32; k * stride];
seg.par_chunks_mut(stride)
.enumerate()
.for_each(|(row, out)| {
let start = t1.indptr[row] as usize;
let end = t1.indptr[row + 1] as usize;
let mut p = start;
for (t, slot) in out.iter_mut().enumerate() {
let bound = (t * slots) as u32;
while p < end && t1.indices[p] < bound {
p += 1;
}
*slot = p as u32;
}
out[wg as usize] = end as u32;
});
seg
}
pub fn a_columns_capacity(
a_prev_t: &crate::prelude::CompressedSparseData2<f32>,
n_iters: usize,
) -> usize {
let widest = (0..a_prev_t.shape.0)
.map(|row| (a_prev_t.indptr[row + 1] - a_prev_t.indptr[row]) as usize)
.max()
.unwrap_or(0);
widest + n_iters
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use cubecl::wgpu::{WgpuDevice, WgpuRuntime};
fn try_device() -> Option<WgpuDevice> {
let device = WgpuDevice::DefaultDevice;
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
WgpuRuntime::client(&device);
}))
.ok()
.map(|_| device)
}
fn dense_to_csr(a: &[f32], n: usize, m: usize) -> (Vec<f32>, Vec<u32>, Vec<u32>) {
let mut values = Vec::new();
let mut indices = Vec::new();
let mut indptr = vec![0u32];
for i in 0..n {
for j in 0..m {
let v = a[i * m + j];
if v != 0.0 {
values.push(v);
indices.push(j as u32);
}
}
indptr.push(values.len() as u32);
}
(values, indices, indptr)
}
fn cpu_fw_argmin_b(
k2b: &[f32],
t1: &[f32],
t2: &[f32],
b_mat: &[f32],
n: usize,
k: usize,
) -> (Vec<f32>, Vec<u32>, f32) {
let mut best_val = vec![f32::MAX; k];
let mut best_idx = vec![0u32; k];
let mut gap = 0.0f32;
for c in 0..k {
for i in 0..n {
let mut g = 0.0f32;
for m in 0..k {
g += k2b[i * k + m] * t1[m * k + c];
}
g -= t2[i * k + c];
if b_mat[i * k + c] != 0.0 {
gap += b_mat[i * k + c] * g;
}
if g < best_val[c] {
best_val[c] = g;
best_idx[c] = i as u32;
}
}
}
(best_val, best_idx, gap)
}
fn gpu_fw_argmin_b(
k2b: &[f32],
t1: &[f32],
t2: &[f32],
b_mat: &[f32],
n: usize,
k: usize,
device: &WgpuDevice,
) -> (Vec<f32>, Vec<u32>, f32) {
let client = WgpuRuntime::client(device);
let upload = |dense: &[f32]| {
let (values, indices, indptr) = dense_to_csr(dense, n, k);
GpuCompressedSparseData::<WgpuRuntime, f32>::from_parts(
&values,
&indices,
&indptr,
CompressedSparseFormat::Csr,
(n, k),
&client,
)
.unwrap()
};
let k2b_gpu = upload(k2b);
let t2_gpu = upload(t2);
let b_gpu = upload(b_mat);
let (t1_vals, t1_idx, t1_ptr) = dense_to_csr(t1, k, k);
let t1_csr = crate::prelude::CompressedSparseData2::<f32>::new_csr(
&t1_vals,
&t1_idx,
&t1_ptr,
None,
(k, k),
);
let t1_gpu = GpuCompressedSparseData::<WgpuRuntime, f32>::from_parts(
&t1_vals,
&t1_idx,
&t1_ptr,
CompressedSparseFormat::Csr,
(k, k),
&client,
)
.unwrap();
let b_wg = b_argmin_workgroup(k).expect("no tier for k");
let seg_host = a_columns_segments(&t1_csr, b_wg, k.div_ceil(b_wg as usize));
let t1_seg =
GpuTensor::<WgpuRuntime, u32>::from_slice(&seg_host, vec![seg_host.len()], &client)
.unwrap();
let blocks = B_ARGMIN_BLOCKS.min(n.max(1) as u32) as usize;
let part_val = GpuTensor::<WgpuRuntime, f32>::empty(vec![blocks * k], &client).unwrap();
let part_idx = GpuTensor::<WgpuRuntime, u32>::empty(vec![blocks * k], &client).unwrap();
let gap_partial = GpuTensor::<WgpuRuntime, f32>::empty(vec![blocks], &client).unwrap();
let out_val = GpuTensor::<WgpuRuntime, f32>::empty(vec![k], &client).unwrap();
let out_idx = GpuTensor::<WgpuRuntime, u32>::empty(vec![k], &client).unwrap();
launch_fw_argmin_b(
&k2b_gpu,
&t1_gpu,
&t1_seg,
&t2_gpu,
&b_gpu,
&part_val,
&part_idx,
&gap_partial,
&out_val,
&out_idx,
n,
k,
&client,
)
.expect("launch failed");
let vals = out_val.read(&client).expect("read failed");
let idx = out_idx.read(&client).expect("read failed");
let gaps = gap_partial.read(&client).expect("read failed");
let gap: f32 = gaps.iter().sum();
(vals, idx, gap)
}
fn cpu_grad_at(k2b: &[f32], t1: &[f32], t2: &[f32], k: usize, i: usize, c: usize) -> f32 {
let mut g = 0.0f32;
for m in 0..k {
g += k2b[i * k + m] * t1[m * k + c];
}
g - t2[i * k + c]
}
#[allow(clippy::too_many_arguments)]
fn assert_argmins_agree(
k2b: &[f32],
t1: &[f32],
t2: &[f32],
got_idx: &[u32],
want_idx: &[u32],
want_val: &[f32],
n: usize,
k: usize,
) {
let mut ties = 0usize;
for c in 0..k {
if got_idx[c] == want_idx[c] {
continue;
}
ties += 1;
let got = cpu_grad_at(k2b, t1, t2, k, got_idx[c] as usize, c);
assert_relative_eq!(got, want_val[c], max_relative = 1e-5, epsilon = 1e-6);
}
assert!(
ties * 50 <= k,
"{} / {} columns disagree at ({}, {}), well past a tie-break rate",
ties,
k,
n,
k
);
}
fn fixture(len: usize, seed: usize, zero_every: usize) -> Vec<f32> {
(0..len)
.map(|i| {
if (i + seed).is_multiple_of(zero_every) {
0.0
} else {
(((i * 37 + seed * 17) % 23) as f32 - 11.0) * 0.1
}
})
.collect()
}
fn check_argmin_arm(n: usize, k: usize, device: &WgpuDevice) {
let k2b = fixture(n * k, 1, 3);
let t1 = fixture(k * k, 5, 4);
let t2 = fixture(n * k, 9, 5);
let b_mat = fixture(n * k, 13, 11);
let (want_val, want_idx, want_gap) = cpu_fw_argmin_b(&k2b, &t1, &t2, &b_mat, n, k);
let (got_val, got_idx, got_gap) = gpu_fw_argmin_b(&k2b, &t1, &t2, &b_mat, n, k, device);
for c in 0..k {
assert_relative_eq!(got_val[c], want_val[c], max_relative = 1e-4, epsilon = 1e-5);
}
assert_argmins_agree(&k2b, &t1, &t2, &got_idx, &want_idx, &want_val, n, k);
assert_relative_eq!(got_gap, want_gap, max_relative = 1e-3, epsilon = 1e-4);
}
#[test]
fn test_fw_argmin_b_matches_cpu() {
let Some(device) = try_device() else { return };
for (n, k) in [(40usize, 7usize), (257, 33), (1000, 130)] {
check_argmin_arm(n, k, &device);
}
}
#[test]
#[cfg(feature = "large_scale_diagnostics")]
fn test_fw_argmin_b_matches_cpu_large() {
let Some(device) = try_device() else { return };
for (n, k) in [(3000usize, 300usize), (300, 1100)] {
check_argmin_arm(n, k, &device);
}
}
#[test]
fn test_fw_argmin_b_handles_empty_rows() {
let Some(device) = try_device() else { return };
let (n, k) = (64usize, 9usize);
let mut k2b = fixture(n * k, 1, 3);
for i in (0..n).step_by(3) {
for c in 0..k {
k2b[i * k + c] = 0.0;
}
}
let t1 = fixture(k * k, 5, 4);
let t2 = fixture(n * k, 9, 5);
let b_mat = fixture(n * k, 13, 11);
let (want_val, want_idx, want_gap) = cpu_fw_argmin_b(&k2b, &t1, &t2, &b_mat, n, k);
let (got_val, got_idx, got_gap) = gpu_fw_argmin_b(&k2b, &t1, &t2, &b_mat, n, k, &device);
for c in 0..k {
assert_relative_eq!(got_val[c], want_val[c], max_relative = 1e-4, epsilon = 1e-5);
assert_eq!(got_idx[c], want_idx[c], "argmin differs at column {}", c);
}
assert_relative_eq!(got_gap, want_gap, max_relative = 1e-3, epsilon = 1e-4);
}
#[test]
fn test_fw_argmin_b_tie_breaks_on_lowest_row() {
let Some(device) = try_device() else { return };
let (n, k) = (2048usize, 4usize);
let mut t1 = vec![0.0f32; k * k];
for c in 0..k {
t1[c * k + c] = 1.0;
}
let t2 = vec![0.0f32; n * k];
let b_mat = vec![0.0f32; n * k];
let mut k2b = vec![0.0f32; n * k];
for i in 0..n {
for c in 0..k {
k2b[i * k + c] = -1.0 - c as f32;
}
}
let (_, got_idx, _) = gpu_fw_argmin_b(&k2b, &t1, &t2, &b_mat, n, k, &device);
for c in 0..k {
assert_eq!(got_idx[c], 0, "column {} did not tie-break to row 0", c);
}
}
}