#![allow(missing_docs)]
use cubecl::prelude::*;
use cubecl_utils_rs::prelude::*;
use crate::errors::BixverseErrors;
use crate::gpu::linalg::sparse_gpu::GpuCompressedSparseData;
use crate::prelude::CompressedSparseFormat;
pub const B_REDUCE_WG: u32 = 256;
pub const B_TILED_WG: u32 = 32;
pub const B_TILED_TILE: u32 = 256;
pub const B_TILED_GROUPS: u32 = 1024;
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 done = p >= row_end;
while !done {
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;
done = p >= row_end;
} else {
done = true;
}
}
}
#[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)]
#[cube(launch_unchecked)]
pub fn fw_argmin_b_tiled<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_indices: &Tensor<u32>,
t2_values: &Tensor<F>,
t2_seg: &Tensor<u32>,
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] tile: u32,
#[comptime] n_tiles: u32,
#[comptime] wg_size: u32,
) {
let tx = UNIT_POS_X;
let grp = CUBE_POS_X;
let tile_id = CUBE_POS_Y;
let c0 = tile_id * tile;
let rem = comptime!(k as u32) - c0;
let mut width = tile.runtime();
if rem < width {
width = rem;
}
let segs = comptime!(n_tiles + 1);
let per_unit = comptime!((tile / wg_size) as usize);
let mut buf = Shared::<[F]>::new_slice(tile as usize);
let mut stage_m = Shared::<[u32]>::new_slice(wg_size as usize);
let mut stage_v = Shared::<[F]>::new_slice(wg_size as usize);
let mut run_val = Array::<F>::new(per_unit);
let mut run_idx = Array::<u32>::new(per_unit);
#[unroll]
for j in 0..per_unit {
run_val[j] = F::max_value();
run_idx[j] = 0u32;
}
let mut gap = F::new(0.0_f32);
let mut row = grp;
while row < n {
let row_us = row as usize;
#[unroll]
for j in 0..per_unit {
buf[(tx + comptime!(j as u32 * wg_size)) as usize] = F::new(0.0_f32);
}
sync_cube();
let hi = k2b_indptr[row_us + 1];
let mut base = k2b_indptr[row_us];
while base < hi {
let e = base + tx;
if e < hi {
stage_m[tx as usize] = k2b_indices[e as usize];
stage_v[tx as usize] = k2b_values[e as usize];
}
sync_cube();
let mut cnt = wg_size.runtime();
if hi - base < cnt {
cnt = hi - base;
}
let mut t = 0u32;
while t < cnt {
let m = stage_m[t as usize];
let v = stage_v[t as usize];
let seg_base = (m * segs + tile_id) as usize;
let s1 = t1_seg[seg_base + 1];
let mut p = t1_seg[seg_base] + tx;
while p < s1 {
let col = t1_indices[p as usize] - c0;
buf[col as usize] += v * t1_values[p as usize];
p += wg_size;
}
sync_cube();
t += 1u32;
}
base += wg_size;
}
let q_base = (row * segs + tile_id) as usize;
let q1 = t2_seg[q_base + 1];
let mut q = t2_seg[q_base] + tx;
while q < q1 {
let col = t2_indices[q as usize] - c0;
buf[col as usize] -= t2_values[q as usize];
q += wg_size;
}
sync_cube();
let b1 = b_indptr[row_us + 1];
let mut r = b_indptr[row_us] + tx;
while r < b1 {
let col = b_indices[r as usize];
if col >= c0 && col < c0 + width {
gap += b_values[r as usize] * buf[(col - c0) as usize];
}
r += wg_size;
}
#[unroll]
for j in 0..per_unit {
let col = tx + comptime!(j as u32 * wg_size);
let g = buf[col as usize];
if col < width && g < run_val[j] {
run_val[j] = g;
run_idx[j] = row;
}
}
sync_cube();
row += CUBE_COUNT_X;
}
let out_base = grp as usize * k + c0 as usize;
#[unroll]
for j in 0..per_unit {
let col = tx + comptime!(j as u32 * wg_size);
if col < width {
best_val[out_base + col as usize] = run_val[j];
best_idx[out_base + col as usize] = run_idx[j];
}
}
stage_v[tx as usize] = gap;
sync_cube();
let mut stride = (wg_size / 2u32).runtime();
while stride > 0u32 {
if tx < stride {
let other = stage_v[(tx + stride) as usize];
stage_v[tx as usize] += other;
}
sync_cube();
stride /= 2u32;
}
if tx == 0u32 {
gap_partial[(tile_id * CUBE_COUNT_X + grp) as usize] = stage_v[0];
}
}
#[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 = Shared::<[u32]>::new_slice(cap_pad);
let mut s_atom_val = Shared::<[F]>::new_slice(cap_pad);
let mut s_dropped = Shared::<[F]>::new_slice(cap_pad);
let mut s_val = Shared::<[F]>::new_slice(comptime!(reduce_scratch_len(wg_size, use_plane)));
let mut s_idx = Shared::<[u32]>::new_slice(comptime!(reduce_scratch_len(wg_size, use_plane)));
let mut s_count = Shared::<[u32]>::new_slice(1usize);
let mut s_found = Shared::<[u32]>::new_slice(1usize);
let mut s_any_drop = Shared::<[u32]>::new_slice(1usize);
let mut s_amin = Shared::<[u32]>::new_slice(1usize);
let mut s_total = Shared::<[F]>::new_slice(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_f32);
w[s] = F::new(0.0_f32);
}
accumulate_sparse_row::<F>(
&mut k2b,
k2b_indices,
k2b_values,
k2b_indptr[cell_us],
k2b_indptr[cell_us + 1],
base,
F::new(1.0_f32),
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_f32);
}
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_f32) / (F::cast_from(t) + F::new(2.0_f32));
let retain = F::new(1.0_f32) - 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_f32);
}
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_f32) {
s_dropped[tx as usize] = weight;
s_atom_val[tx as usize] = F::new(0.0_f32);
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_f32) {
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_f32) - weight,
slots,
);
}
a += 1u32;
}
}
let mut mass = F::new(0.0_f32);
if tx < live {
mass = s_atom_val[tx as usize];
}
let mut total = F::new(0.0_f32);
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_f32);
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_f32);
if total > F::new(A_RENORM_FLOOR) {
renorm = F::new(1.0_f32) / 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;
}
}
pub fn b_tiled_layout(k: usize) -> (usize, usize) {
let tile = k
.min(B_TILED_TILE as usize)
.next_multiple_of(B_TILED_WG as usize);
(tile, k.div_ceil(tile))
}
#[allow(clippy::too_many_arguments)]
pub fn launch_fw_argmin_b_tiled<F>(
k2b: &GpuCompressedSparseData<F>,
t1: &GpuCompressedSparseData<F>,
t1_seg: &GpuTensor<u32>,
t2: &GpuCompressedSparseData<F>,
t2_seg: &GpuTensor<u32>,
b_mat: &GpuCompressedSparseData<F>,
part_val: &GpuTensor<F>,
part_idx: &GpuTensor<u32>,
gap_partial: &GpuTensor<F>,
out_val: &GpuTensor<F>,
out_idx: &GpuTensor<u32>,
groups: u32,
n: usize,
k: usize,
client: &Client,
) -> Result<(), BixverseErrors>
where
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 segments", t2_seg.len() * size_of::<u32>()),
("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 (tile, n_tiles) = b_tiled_layout(k);
let count = checked_cube_count("fw_argmin_b_tiled", groups, n_tiles as u32, 1, &limits)?;
unsafe {
fw_argmin_b_tiled::launch_unchecked::<F>(
client,
count,
CubeDim::new_1d(B_TILED_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.indices.clone().into_tensor_arg(),
t2.values.clone().into_tensor_arg(),
t2_seg.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,
tile as u32,
n_tiles as u32,
B_TILED_WG,
);
}
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>(
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(),
groups,
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<F>(
t1: &GpuCompressedSparseData<F>,
t1_seg: &GpuTensor<u32>,
a_prev_t: &GpuCompressedSparseData<F>,
k2b: &GpuCompressedSparseData<F>,
atom_idx: &GpuTensor<u32>,
atom_val: &GpuTensor<F>,
atom_cnt: &GpuTensor<u32>,
threshold: &GpuTensor<F>,
n: usize,
k: usize,
n_iters: usize,
cap: u32,
pruning: Option<f32>,
use_plane: Option<bool>,
wg_override: Option<u32>,
client: &Client,
) -> Result<(), BixverseErrors>
where
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>(
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> {
crate::single_cell::mc_generation::seacells::csr_column_segments(t1, wg as usize, slots)
}
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;
fn try_device() -> Option<WgpuDevice> {
let device = WgpuDevice::default();
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
cubecl::Device::from(device.clone()).client();
}))
.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 = cubecl::Device::from(device.clone()).client();
let upload = |dense: &[f32]| {
let (values, indices, indptr) = dense_to_csr(dense, n, k);
GpuCompressedSparseData::<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 csr = |dense: &[f32], rows: usize| {
let (vals, idx, ptr) = dense_to_csr(dense, rows, k);
crate::prelude::CompressedSparseData2::<f32>::new_csr(
&vals,
&idx,
&ptr,
None,
(rows, k),
)
};
let t1_csr = csr(t1, k);
let t2_csr = csr(t2, n);
let t1_gpu = GpuCompressedSparseData::<f32>::from_parts(
&t1_csr.data,
&t1_csr.indices,
&t1_csr.indptr,
CompressedSparseFormat::Csr,
(k, k),
&client,
)
.unwrap();
let (tile, n_tiles) = b_tiled_layout(k);
let seg = |m: &crate::prelude::CompressedSparseData2<f32>| {
let host =
crate::single_cell::mc_generation::seacells::csr_column_segments(m, n_tiles, tile);
GpuTensor::<u32>::from_slice(&host, vec![host.len()], &client).unwrap()
};
let t1_seg = seg(&t1_csr);
let t2_seg = seg(&t2_csr);
let groups = (B_TILED_GROUPS as usize).min(n.max(1));
let part_val = GpuTensor::<f32>::empty(vec![groups * k], &client).unwrap();
let part_idx = GpuTensor::<u32>::empty(vec![groups * k], &client).unwrap();
let gap_partial = GpuTensor::<f32>::empty(vec![n_tiles * groups], &client).unwrap();
let out_val = GpuTensor::<f32>::empty(vec![k], &client).unwrap();
let out_idx = GpuTensor::<u32>::empty(vec![k], &client).unwrap();
launch_fw_argmin_b_tiled(
&k2b_gpu,
&t1_gpu,
&t1_seg,
&t2_gpu,
&t2_seg,
&b_gpu,
&part_val,
&part_idx,
&gap_partial,
&out_val,
&out_idx,
groups as u32,
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), (1030, 270)] {
check_argmin_arm(n, k, &device);
}
}
#[test]
#[cfg(feature = "large-test")]
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);
}
}
}