use ruda_core::ir::{ElemType, FloatKind, features::MmaConfig};
use ruda_kernel::{dsl as kernel_dsl, dsl::{cmma, prelude::*}, tensor::RudaTensor};
use ruda_kernel::dsl::prelude::barrier::{copy_async, commit_copy_group, wait_copy_group};
#[derive(Clone, Copy)]
struct Config { bm: usize, bn: usize, warp_m: usize, warp_n: usize, stages: usize, resident: usize, efficiency: f64 }
const CONFIGS: [Config; 3] = [
Config { bm: 128, bn: 128, warp_m: 32, warp_n: 64, stages: 2, resident: 2, efficiency: 1.0 },
Config { bm: 128, bn: 64, warp_m: 32, warp_n: 32, stages: 3, resident: 2, efficiency: 0.85 },
Config { bm: 64, bn: 64, warp_m: 32, warp_n: 32, stages: 3, resident: 3, efficiency: 0.55 },
];
#[cfg(test)]
mod tests {
use super::CONFIGS;
#[test]
fn natural_b_fragments_have_unique_banks_and_match_the_copy_swizzle() {
for bn in [64usize, 128] {
for wn in (0..bn).step_by(32) {
for tile in 0..4 {
for s in 0..4 {
let mut banks = [false; 32];
for lane in 0..32 {
let (g, t) = (lane / 4, lane % 4);
let column = wn + tile * 8 + g;
let row = s * 8 + t;
let index = row * bn + (column ^ (t * 8));
assert!(!banks[index % 32]);
banks[index % 32] = true;
for half in 0..2 {
let row = row + half * 4;
let copied = (row * (bn / 4) + ((column / 4) ^ ((row % 4) * 2))) * 4 + column % 4;
assert_eq!(index + half * 4 * bn, copied);
}
}
}
}
}
}
}
#[test]
fn affine_copy_offsets_preserve_every_swizzled_element() {
for (mut config, matrix_load) in CONFIGS.into_iter().flat_map(|config| [(config, false), (config, true)]) {
if matrix_load && config.bm >= 128 { config.warp_m = 64; }
let threads = config.bm / config.warp_m * (config.bn / config.warp_n) * 32;
let quads = config.bn / 4;
assert_eq!(config.bm * 8 % threads, 0);
assert_eq!(config.bn * 8 % threads, 0);
assert_eq!(threads % 32, 0);
assert_eq!(threads % (quads * 2), 0);
for pitch in [config.bm + 4, config.bn + 12, 1024] {
for thread in 0..threads {
let a_base = thread / 8 * pitch + thread % 8;
let b_base = thread / quads * pitch + thread % quads;
let a_swizzle = if matrix_load { thread / 8 % 8 } else { (thread / 8 % 4) * 2 };
let a_shared = thread / 8 * 8 + (thread % 8 ^ a_swizzle);
let divisor = if matrix_load { 1 } else { 2 };
let b_shared = thread / quads * quads + (thread % quads ^ (thread / quads / divisor % 4) * 2);
for i in 0..config.bm * 8 / threads {
let p = thread + i * threads;
assert_eq!(a_base + i * (threads / 8) * pitch, p / 8 * pitch + p % 8);
let swizzle = if matrix_load { p / 8 % 8 } else { (p / 8 % 4) * 2 };
assert_eq!(a_shared + i * threads, p / 8 * 8 + (p % 8 ^ swizzle));
}
for i in 0..config.bn * 8 / threads {
let p = thread + i * threads;
assert_eq!(b_base + i * (threads / quads) * pitch, p / quads * pitch + p % quads);
let phase = (i * threads / (quads * divisor) % 4) * 2;
assert_eq!((b_shared ^ phase) + i * threads, p / quads * quads + (p % quads ^ (p / quads / divisor % 4) * 2));
}
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn launch<R: Runtime>(a: &RudaTensor<R>, b: &RudaTensor<R>, out: &RudaTensor<R>,
m: usize, n: usize, k: usize, lda: usize, ldb: usize, a_batch: usize, b_batch: usize,
batch: usize) -> bool {
if std::env::var("RUDA_TF32_GEMM").is_ok_and(|value| value == "legacy") { return false; }
let client = &a.client;
let hw = &client.properties().hardware;
let storage = ElemType::Float(FloatKind::TF32).into();
if !client.features().copy_async || k % 32 != 0 || m % 32 != 0 || n % 32 != 0
|| !client.features().matmul.mma.contains(&MmaConfig { a_type: storage, b_type: storage,
cd_type: ElemType::Float(FloatKind::F32).into(), m: 16, n: 8, k: 8 })
|| hw.plane_size_min != 32 || hw.plane_size_max != 32
|| out.handle.offset_start.unwrap_or(0) % 16 != 0
|| batch > hw.max_ruda_count.1 as usize
{ return false; }
let sms = hw.num_streaming_multiprocessors.unwrap_or(108).max(1) as usize;
let mut best: Option<(f64, Config)> = None;
for config in CONFIGS {
let threads = config.bm / config.warp_m * (config.bn / config.warp_n) * 32;
let shared = config.stages * (config.bm * 32 + 32 * config.bn) * 4;
let tiles = m.div_ceil(config.bm) * n.div_ceil(config.bn);
if threads as u32 > hw.max_units_per_ruda || shared > hw.max_shared_memory_size
|| tiles > hw.max_ruda_count.0 as usize { continue; }
let resident = config.resident.min(hw.max_shared_memory_size / shared).max(1);
let waves = (tiles * batch).div_ceil(sms * resident);
let cost = (waves * resident * config.bm * config.bn) as f64 / config.efficiency;
if best.is_none_or(|(value, _)| cost < value) { best = Some((cost, config)); }
}
let Some((_, mut config)) = best else { return false; };
let matrix_load = true;
if k >= 1024 && config.bm >= 128 && m % config.bm == 0 && n % config.bn == 0 { config.warp_m = 64; }
let tiles_m = m.div_ceil(config.bm);
let group = if tiles_m % 8 == 0 { 8 } else { tiles_m };
let threads = config.bm / config.warp_m * (config.bn / config.warp_n) * 32;
unsafe { pipelined::launch_unchecked::<R>(client,
RudaCount::Static((tiles_m * n.div_ceil(config.bn)) as u32, batch as u32, 1),
RudaDim::new_1d(threads as u32), a.clone().into_array_arg(), b.clone().into_array_arg(),
out.clone().into_array_arg(), m, n, k, lda, ldb, a_batch, b_batch,
config.bm, config.bn, config.warp_m, config.warp_n, config.stages, group, matrix_load); }
true
}
#[ruda]
fn load_stage(a: &Array<Vector<f32, Const<4>>>, b: &Array<Vector<f32, Const<4>>>,
sa: &mut SharedMemory<Vector<f32, Const<4>>>, sb: &mut SharedMemory<Vector<f32, Const<4>>>,
a_source: usize, b_source: usize, a_target: usize, b_target: usize,
a_row: usize, b_col: usize, block: usize, bank: usize,
#[comptime] m: usize, #[comptime] n: usize, #[comptime] lda: usize, #[comptime] ldb: usize,
#[comptime] bm: usize, #[comptime] bn: usize, #[comptime] threads: usize, #[comptime] matrix_load: bool) {
let a_global = a_source + block * 8;
let b_global = b_source + block * (32 * (ldb / 4));
let a_shared = bank * (bm * 8) + a_target;
let b_shared = bank * (bn * 8) + b_target;
#[unroll]
for i in 0usize..comptime!(bm * 8 / threads) {
let target = a_shared + i * threads;
let source = a_global + i * ((threads / 8) * (lda / 4));
let mut valid = true;
if comptime!(m % bm != 0) { valid = a_row + i * (threads / 8) < m; }
if valid {
copy_async(&a.slice(source, source + 1), &mut sa.slice_mut(target, target + 1), 4u32);
} else {
sa[target] = Vector::new(0.0f32);
}
}
let mut b_valid = true;
if comptime!(n % bn != 0) { b_valid = b_col < n; }
#[unroll]
for i in 0usize..comptime!(bn * 8 / threads) {
let phase = comptime!((i * threads / ((bn / 4) * if matrix_load { 1 } else { 2 }) % 4) * 2);
let target = (b_shared ^ phase) + i * threads;
let source = b_global + i * ((threads / (bn / 4)) * (ldb / 4));
if b_valid {
copy_async(&b.slice(source, source + 1), &mut sb.slice_mut(target, target + 1), 4u32);
} else {
sb[target] = Vector::new(0.0f32);
}
}
}
#[ruda(launch_unchecked)]
fn pipelined(a: &Array<Vector<f32, Const<4>>>, b: &Array<Vector<f32, Const<4>>>,
out: &mut Array<Vector<f32, Const<4>>>,
#[comptime] m: usize, #[comptime] n: usize, #[comptime] k: usize,
#[comptime] lda: usize, #[comptime] ldb: usize, #[comptime] a_batch: usize, #[comptime] b_batch: usize,
#[comptime] bm: usize, #[comptime] bn: usize, #[comptime] warp_m: usize, #[comptime] warp_n: usize,
#[comptime] stages: usize, #[comptime] group: usize, #[comptime] matrix_load: bool) {
let thread = UNIT_POS as usize;
let lane = thread % 32;
let warp = thread / 32;
let g = lane / 4;
let t = lane % 4;
let warp_cols = comptime!(bn / warp_n);
let threads = comptime!(bm / warp_m * (bn / warp_n) * 32);
let per_group = comptime!(group * n.div_ceil(bn));
let pid = RUDA_POS_X as usize;
let local = pid % per_group;
let row = (pid / per_group * group + local % group) * bm;
let col = local / group * bn;
let wm = warp / warp_cols * warp_m;
let wn = warp % warp_cols * warp_n;
let mt = comptime!(warp_m / 16);
let ntile = comptime!(warp_n / 8);
let quads = comptime!(warp_n / 32);
let a_stage = comptime!(bm * 8);
let b_stage = comptime!(bn * 8);
let b_row = comptime!(bn / 4);
let blocks = comptime!(k / 32);
let a_copy_row = row + thread / 8;
let b_copy_col = col + (thread % b_row) * 4;
let a_source = RUDA_POS_Y as usize * (a_batch / 4) + a_copy_row * (lda / 4) + thread % 8;
let b_source = RUDA_POS_Y as usize * (b_batch / 4) + (thread / b_row) * (ldb / 4) + b_copy_col / 4;
let mut a_swizzle = (thread / 8 % 4) * 2;
if comptime!(matrix_load) { a_swizzle = thread / 8 % 8; }
let a_target = (thread / 8) * 8 + ((thread % 8) ^ a_swizzle);
let b_divisor = comptime!(if matrix_load { 1usize } else { 2usize });
let b_target = (thread / b_row) * b_row + ((thread % b_row) ^ ((thread / b_row / b_divisor % 4) * 2));
let mut sa = SharedMemory::<Vector<f32, Const<4>>>::new_aligned(a_stage * stages, 32usize);
let mut sb = SharedMemory::<Vector<f32, Const<4>>>::new_aligned(b_stage * stages, 32usize);
let pairs = sa.to_slice().with_vector_size::<Const<2>>();
let b_words = sb.to_slice().with_vector_size::<Const<1>>();
let natural_n = comptime!(matrix_load && warp_m == 64);
let mut acc = Sequence::<Vector<f32, Const<4>>>::new();
#[unroll] for _ in 0usize..(mt * ntile) { acc.push(Vector::new(0.0f32)); }
let def = cmma::MmaDefinition::<tf32, tf32, f32>::new(16usize, 8usize, 8usize);
#[unroll]
for stage in 0usize..(stages - 1) {
if comptime!(stage < k / 32) {
load_stage(a, b, &mut sa, &mut sb, a_source, b_source, a_target, b_target,
a_copy_row, b_copy_col, stage, stage, m, n, lda, ldb, bm, bn, threads, matrix_load);
}
commit_copy_group();
}
let xa = (g % 4) * 2;
let a_base = (wm + g) * 16 + t % 2;
let a_matrix_base = (wm + lane % 16) * 8 + ((lane / 16) ^ (lane % 8));
let b_col = wn / 4 + (g ^ (t * 2));
let b_base = t * b_divisor * b_row + b_col;
let mut bank = 0usize;
let mut write_bank = usize::cast_from(stages - 1);
for block in 0usize..blocks {
wait_copy_group(comptime!((stages - 2) as u32));
sync_ruda();
let next = block + stages - 1;
if next < blocks {
load_stage(a, b, &mut sa, &mut sb, a_source, b_source, a_target, b_target,
a_copy_row, b_copy_col, next, write_bank, m, n, lda, ldb, bm, bn, threads, matrix_load);
}
commit_copy_group();
let a_read = bank * (a_stage * 2) + a_base;
let b_read = bank * b_stage + b_base;
#[unroll]
for s in 0usize..4 {
let mut ar = Sequence::<Vector<tf32, Const<4>>>::new();
let chunk = ((s * 2) ^ xa) + t / 2;
#[unroll]
for i in 0usize..mt {
let mut fragment = Vector::<tf32, Const<4>>::empty();
if comptime!(matrix_load) {
let index = bank * a_stage + (a_matrix_base ^ (s * 2)) + i * 128;
let words = def.load_matrix_vector::<_, Const<4>>(&sa.slice(index, index + 1), 4usize, false);
#[unroll]
for j in 0usize..4 { fragment[j] = tf32::cast_from(words[j]); }
} else {
let index = a_read + i * 256 + chunk * 2;
let low = pairs[index];
let high = pairs[index + 128];
fragment[0usize] = tf32::cast_from(low[0usize]);
fragment[1usize] = tf32::cast_from(high[0usize]);
fragment[2usize] = tf32::cast_from(low[1usize]);
fragment[3usize] = tf32::cast_from(high[1usize]);
}
ar.push(fragment);
}
if comptime!(natural_n) {
#[unroll]
for j in 0usize..ntile {
let index = bank * (b_stage * 4) + (s * 8 + t) * bn + ((wn + j * 8 + g) ^ (t * 8));
let mut br = Vector::<tf32, Const<2>>::empty();
br[0usize] = tf32::cast_from(b_words[index][0usize]);
br[1usize] = tf32::cast_from(b_words[index + 4 * bn][0usize]);
#[unroll]
for i in 0usize..mt {
def.execute_vectors_inplace(&ar[i], &br, acc.index_mut(i * ntile + j));
}
}
} else {
#[unroll]
for q in 0usize..quads {
let index = b_read + s * (8 * b_row) + q * 8;
let first = sb[index];
let second = sb[index + comptime!(if matrix_load { 4usize } else { 1usize }) * b_row];
#[unroll]
for jj in 0usize..4 {
let mut br = Vector::<tf32, Const<2>>::empty();
br[0usize] = tf32::cast_from(first[jj]);
br[1usize] = tf32::cast_from(second[jj]);
#[unroll]
for i in 0usize..mt {
def.execute_vectors_inplace(&ar[i], &br, acc.index_mut(i * ntile + q * 4 + jj));
}
}
}
}
}
bank += 1;
if bank == stages { bank = 0; }
write_bank += 1;
if write_bank == stages { write_bank = 0; }
}
wait_copy_group(0u32);
let base = RUDA_POS_Y as usize * m * n;
let mut output_pairs = out.to_slice_mut().with_vector_size::<Const<2>>();
#[unroll]
for i in 0usize..mt {
#[unroll]
for h in 0usize..2 {
let r = row + wm + i * 16 + g + h * 8;
if comptime!(natural_n) {
#[unroll]
for j in 0usize..ntile {
let c = col + wn + j * 8 + t * 2;
let value = acc[i * ntile + j];
let mut pair = Vector::<f32, Const<2>>::empty();
pair[0usize] = value[h * 2];
pair[1usize] = value[h * 2 + 1];
output_pairs[(base + r * n + c) / 2] = pair;
}
} else {
#[unroll]
for q in 0usize..quads {
let c = col + wn + q * 32 + t * 8;
let mut low = Vector::<f32, Const<4>>::empty();
let mut high = Vector::<f32, Const<4>>::empty();
#[unroll]
for jj in 0usize..4 {
let value = acc[i * ntile + q * 4 + jj];
low[jj] = value[h * 2];
high[jj] = value[h * 2 + 1];
}
let mut valid = true;
if comptime!(m % bm != 0) { valid = valid && r < m; }
if comptime!(n % bn != 0) { valid = valid && c < n; }
if valid {
out[(base + r * n + c) / 4] = low;
out[(base + r * n + c + 4) / 4] = high;
}
}
}
}
}
}