use cudarc::driver::{LaunchConfig, PushKernelArg};
use super::columns::{grow, DeviceColumns};
use super::error::GpuError;
use super::layer::LayerScratch;
use super::module::warp_per_bucket;
use super::scan::exclusive_scan;
use super::sum::GpuSum;
use crate::collectives::Collectives;
use crate::engine::timing::timed;
use crate::truncation::builtin::{local_boundary_quota, octave_edge, EdgeDecision, APPROX_BINS};
const HISTOGRAM_BLOCKS: usize = 1024;
fn whole_sum_launch(buckets: usize) -> LaunchConfig {
LaunchConfig {
grid_dim: (buckets.div_ceil(8).clamp(1, HISTOGRAM_BLOCKS) as u32, 1, 1),
block_dim: (256, 1, 1),
shared_mem_bytes: 0,
}
}
fn octave_histogram_device<const W: usize>(
sum: &GpuSum<W>,
scratch: &mut LayerScratch<W>,
) -> Result<Vec<u64>, GpuError> {
let stream = sum.stream.clone();
let buckets = sum.hash.num_buckets();
stream.memset_zeros(&mut scratch.histogram)?;
let buckets_u32 = buckets as u32;
timed!(let started = scratch.event(sum)?);
unsafe {
stream
.launch_builder(&sum.kernels.octave_histogram)
.arg(&sum.columns.coeff)
.arg(&sum.columns.start)
.arg(&sum.columns.lengths)
.arg(&buckets_u32)
.arg(&mut scratch.histogram)
.launch(whole_sum_launch(buckets))?;
}
timed!(scratch.lap(sum, started, |stats| &mut stats.truncate_ns)?);
let packed = stream.clone_dtoh(&scratch.histogram)?;
stream.synchronize()?;
debug_assert_eq!(packed[0] as usize, sum.len(), "histogram term count");
Ok(packed)
}
fn clear_device<const W: usize>(sum: &mut GpuSum<W>) -> Result<(), GpuError> {
let buckets = sum.hash.num_buckets();
sum.stream.memset_zeros(&mut sum.columns.lengths.slice_mut(0..buckets))?;
sum.columns.len = 0;
sum.debug_check();
Ok(())
}
fn retain_at_edge_device<const W: usize>(
sum: &mut GpuSum<W>,
scratch: &mut LayerScratch<W>,
edge: EdgeDecision,
local_quota: usize,
) -> Result<(), GpuError> {
match edge {
EdgeDecision::KeepAll => Ok(()),
EdgeDecision::Clear => clear_device(sum),
EdgeDecision::Boundary { bin, .. } => {
retain_device(sum, scratch, bin as u64, 52, local_quota as u64)
}
}
}
pub(crate) fn approx_top_n_device<const W: usize>(
sum: Option<(&mut GpuSum<W>, &mut LayerScratch<W>)>,
n: usize,
collectives: &dyn Collectives,
) -> Result<(), GpuError> {
let Some((sum, scratch)) = sum else {
let mut packed = [0u64; 1 + APPROX_BINS];
collectives.allreduce_sum_u64(&mut packed);
if let EdgeDecision::Boundary { quota, .. } =
octave_edge(&packed[1..], packed[0] as usize, n)
{
if collectives.size() > 1 {
local_boundary_quota(collectives, 0, quota);
}
}
return Ok(());
};
let local = octave_histogram_device(sum, scratch);
let mut packed = match &local {
Ok(histogram) => histogram.clone(),
Err(_) => vec![0u64; 1 + APPROX_BINS],
};
collectives.allreduce_sum_u64(&mut packed);
let edge = octave_edge(&packed[1..], packed[0] as usize, n);
let local_quota = match edge {
EdgeDecision::Boundary { bin, quota, .. } if collectives.size() > 1 => {
let local_count = local.as_ref().map_or(0, |histogram| histogram[1 + bin]);
local_boundary_quota(collectives, local_count as usize, quota)
}
EdgeDecision::Boundary { quota, .. } => quota,
EdgeDecision::KeepAll | EdgeDecision::Clear => 0,
};
local?;
retain_at_edge_device(sum, scratch, edge, local_quota)
}
fn radix_select_bits<const W: usize>(
sum: &GpuSum<W>,
scratch: &mut LayerScratch<W>,
n: usize,
) -> Result<(u64, u64), GpuError> {
let stream = sum.stream.clone();
let buckets = sum.hash.num_buckets() as u32;
let mut prefix: u64 = 0;
let mut remaining: u64 = n as u64;
for pass in 0u32..8 {
let shift = 56 - 8 * pass;
let fixed_mask: u64 = if pass == 0 { 0 } else { u64::MAX << (64 - 8 * pass) };
stream.memset_zeros(&mut scratch.radix_histogram)?;
unsafe {
stream
.launch_builder(&sum.kernels.radix_histogram)
.arg(&sum.columns.coeff)
.arg(&sum.columns.start)
.arg(&sum.columns.lengths)
.arg(&buckets)
.arg(&fixed_mask)
.arg(&prefix)
.arg(&shift)
.arg(&mut scratch.radix_histogram)
.launch(whole_sum_launch(buckets as usize))?;
}
let histogram = stream.clone_dtoh(&scratch.radix_histogram)?;
stream.synchronize()?;
let mut cumulative = 0u64;
let mut chosen: Option<u32> = None;
for digit in (0..256u32).rev() {
let count = histogram[digit as usize];
if cumulative + count >= remaining {
chosen = Some(digit);
break;
}
cumulative += count;
}
let chosen = chosen.expect("radix select: remaining exceeds the population");
let group = histogram[chosen as usize];
prefix |= (u64::from(chosen)) << shift;
remaining -= cumulative;
if pass == 7 {
return Ok((prefix, remaining));
}
if group == 1 {
let mask = fixed_mask | (0xFFu64 << shift);
stream.memset_zeros(&mut scratch.radix_out.slice_mut(0..1))?;
unsafe {
stream
.launch_builder(&sum.kernels.radix_extract)
.arg(&sum.columns.coeff)
.arg(&sum.columns.start)
.arg(&sum.columns.lengths)
.arg(&buckets)
.arg(&mask)
.arg(&prefix)
.arg(&mut scratch.radix_out)
.launch(whole_sum_launch(buckets as usize))?;
}
let out = stream.clone_dtoh(&scratch.radix_out.slice(0..1))?;
stream.synchronize()?;
return Ok((out[0], remaining));
}
}
unreachable!("the loop returns by pass 7")
}
fn retain_device<const W: usize>(
sum: &mut GpuSum<W>,
scratch: &mut LayerScratch<W>,
boundary: u64,
shift: u32,
quota: u64,
) -> Result<(), GpuError> {
let stream = sum.stream.clone();
let kernels = sum.kernels.clone();
let ordinal = sum.device();
let buckets = sum.hash.num_buckets();
let extent = scratch.extent.max(sum.len());
let mut out = match sum.spare.take() {
Some(spare) => spare,
None => DeviceColumns::<W>::with_capacity(&stream, ordinal, extent, buckets)?,
};
out.len = 0;
out.buckets = 0;
out.reserve(extent, buckets)?;
grow(&stream, &mut scratch.counts, buckets, ordinal)?;
grow(&stream, &mut scratch.destination_offsets, buckets + 1, ordinal)?;
let buckets_u32 = buckets as u32;
timed!(let started = scratch.event(sum)?);
unsafe {
stream
.launch_builder(&kernels.boundary_counts)
.arg(&sum.columns.coeff)
.arg(&sum.columns.start)
.arg(&sum.columns.lengths)
.arg(&buckets_u32)
.arg(&boundary)
.arg(&shift)
.arg(&mut scratch.counts)
.launch(warp_per_bucket(buckets))?;
}
exclusive_scan(
&stream,
&kernels,
&scratch.counts.slice(0..buckets),
&mut scratch.destination_offsets.slice_mut(0..buckets + 1),
buckets,
&mut scratch.scan,
&mut scratch.totals,
)?;
unsafe {
stream
.launch_builder(&kernels.retain)
.arg(&sum.columns.x)
.arg(&sum.columns.z)
.arg(&sum.columns.coeff)
.arg(&sum.columns.fingerprint)
.arg(&sum.columns.start)
.arg(&sum.columns.lengths)
.arg(&buckets_u32)
.arg(&boundary)
.arg(&shift)
.arg(&scratch.destination_offsets)
.arg("a)
.arg(&mut out.x)
.arg(&mut out.z)
.arg(&mut out.coeff)
.arg(&mut out.fingerprint)
.arg(&mut out.start)
.arg(&mut out.lengths)
.launch(warp_per_bucket(buckets))?;
}
exclusive_scan(
&stream,
&kernels,
&out.lengths.slice(0..buckets),
&mut scratch.destination_offsets.slice_mut(0..buckets + 1),
buckets,
&mut scratch.scan,
&mut scratch.totals,
)?;
timed!(scratch.lap(sum, started, |stats| &mut stats.truncate_ns)?);
let total = stream.clone_dtoh(&scratch.totals)?[0];
stream.synchronize()?;
out.len = total as usize;
out.buckets = buckets;
sum.spare = Some(std::mem::replace(&mut sum.columns, out));
sum.debug_check();
Ok(())
}
pub(crate) fn top_n_device<const W: usize>(
sum: &mut GpuSum<W>,
scratch: &mut LayerScratch<W>,
n: usize,
) -> Result<(), GpuError> {
if sum.len() <= n {
return Ok(());
}
if n == 0 {
return clear_device(sum);
}
timed!(let started = scratch.event(sum)?);
let (threshold, tied_kept) = radix_select_bits(sum, scratch, n)?;
timed!(scratch.lap(sum, started, |stats| &mut stats.truncate_ns)?);
retain_device(sum, scratch, threshold, 0, tied_kept)
}
#[cfg(test)]
mod tests;