use std::collections::HashMap;
use std::sync::Arc;
use super::super::buffers::{DtypedBuf, GpuBuffer};
use super::super::dtype::WeightDtype;
use super::super::kernel_identity::ResolvedGemmOp;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct TileFootprint {
pub(crate) tile: (u32, u32),
pub(crate) resident: u32,
}
pub(crate) fn tile_waves(
rows: usize,
columns: usize,
footprint: TileFootprint,
multiprocessors: u32,
) -> Option<u64> {
let (tile_rows, tile_columns) = footprint.tile;
if tile_rows == 0 || tile_columns == 0 || multiprocessors == 0 {
return None;
}
let ctas = u64::try_from(rows)
.ok()?
.div_ceil(u64::from(tile_rows))
.checked_mul(
u64::try_from(columns)
.ok()?
.div_ceil(u64::from(tile_columns)),
)?;
let resident = u64::from(multiprocessors).checked_mul(u64::from(footprint.resident.max(1)))?;
Some(ctas.div_ceil(resident))
}
pub(crate) fn wave_guard_admits(
rows: usize,
columns: usize,
candidate: TileFootprint,
reference: TileFootprint,
multiprocessors: u32,
) -> bool {
let (Some(candidate_waves), Some(reference_waves)) = (
tile_waves(rows, columns, candidate, multiprocessors),
tile_waves(rows, columns, reference, multiprocessors),
) else {
return false;
};
let candidate_area = u64::from(candidate.tile.0) * u64::from(candidate.tile.1);
let reference_area = u64::from(reference.tile.0) * u64::from(reference.tile.1);
!(candidate_waves > reference_waves && candidate_area >= reference_area)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct RouteProofKey {
pub(crate) candidate: &'static str,
pub(crate) op: ResolvedGemmOp,
pub(crate) dtype: WeightDtype,
pub(crate) dims: (usize, usize, usize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RouteProofVerdict {
Admitted,
Declined,
}
#[derive(Default)]
pub(crate) struct RouteProofLedger {
verdicts: HashMap<RouteProofKey, RouteProofVerdict>,
}
impl RouteProofLedger {
pub(crate) fn verdict(&self, key: RouteProofKey) -> Option<RouteProofVerdict> {
self.verdicts.get(&key).copied()
}
pub(crate) fn record(&mut self, key: RouteProofKey, verdict: RouteProofVerdict) {
self.verdicts.insert(key, verdict);
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.verdicts.len()
}
}
pub(crate) enum ProofOutput {
F32(GpuBuffer),
Typed(DtypedBuf),
}
impl ProofOutput {
pub(crate) fn seeded_from(
stream: &Arc<cudarc::driver::CudaStream>,
source: cudarc::driver::sys::CUdeviceptr,
elements: usize,
dtype: WeightDtype,
) -> Result<Self, String> {
let output = match dtype {
WeightDtype::F32 | WeightDtype::Tf32 => Self::F32(GpuBuffer::zeros(stream, elements)?),
WeightDtype::Bf16 | WeightDtype::F16 => {
Self::Typed(DtypedBuf::zeros(stream, elements, dtype)?)
}
};
let bytes = elements
.checked_mul(dtype.size_bytes())
.ok_or_else(|| "proof output byte count overflows usize".to_string())?;
if bytes > 0 {
let result = unsafe {
cudarc::driver::sys::cuMemcpyDtoDAsync_v2(
output.ptr(),
source,
bytes,
stream.cu_stream(),
)
};
if result != cudarc::driver::sys::CUresult::CUDA_SUCCESS {
return Err(format!("seed the proof output: {result:?}"));
}
}
Ok(output)
}
pub(crate) fn ptr(&self) -> cudarc::driver::sys::CUdeviceptr {
match self {
Self::F32(buffer) => buffer.cached_ptr(),
Self::Typed(buffer) => buffer.cached_ptr(),
}
}
pub(crate) fn words(
&self,
stream: &Arc<cudarc::driver::CudaStream>,
) -> Result<Vec<u32>, String> {
let mut host = match self {
Self::F32(buffer) => {
let mut host = vec![0.0f32; buffer.len()];
buffer.download(stream, &mut host)?;
host
}
Self::Typed(buffer) => {
let mut host = vec![0.0f32; buffer.len_elems()];
buffer.download_f32(stream, &mut host)?;
host
}
};
Ok(host.drain(..).map(f32::to_bits).collect())
}
}
pub(crate) fn prove_bits<Candidate, Reference>(
stream: &Arc<cudarc::driver::CudaStream>,
output: cudarc::driver::sys::CUdeviceptr,
elements: usize,
dtype: WeightDtype,
candidate: Candidate,
reference: Reference,
) -> Result<RouteProofVerdict, String>
where
Candidate: FnOnce(cudarc::driver::sys::CUdeviceptr) -> Result<(), String>,
Reference: FnOnce(cudarc::driver::sys::CUdeviceptr) -> Result<(), String>,
{
let candidate_output = ProofOutput::seeded_from(stream, output, elements, dtype)?;
let reference_output = ProofOutput::seeded_from(stream, output, elements, dtype)?;
candidate(candidate_output.ptr())?;
reference(reference_output.ptr())?;
stream
.synchronize()
.map_err(|error| format!("synchronise the route proof: {error:?}"))?;
let candidate_words = candidate_output.words(stream)?;
let reference_words = reference_output.words(stream)?;
Ok(if candidate_words == reference_words {
RouteProofVerdict::Admitted
} else {
RouteProofVerdict::Declined
})
}
#[cfg(test)]
mod tests {
use super::*;
const ADA_MULTIPROCESSORS: u32 = 142;
fn footprint(tile: (u32, u32), resident: u32) -> TileFootprint {
TileFootprint { tile, resident }
}
#[test]
fn tile_waves_count_grids_against_the_resident_ctas_of_the_board() {
assert_eq!(
tile_waves(1536, 768, footprint((64, 64), 2), ADA_MULTIPROCESSORS),
Some(2)
);
assert_eq!(
tile_waves(4621, 384, footprint((144, 96), 1), ADA_MULTIPROCESSORS),
Some(1)
);
assert_eq!(tile_waves(0, 384, footprint((64, 64), 2), 142), Some(0));
assert_eq!(tile_waves(64, 64, footprint((0, 64), 2), 142), None);
assert_eq!(tile_waves(64, 64, footprint((64, 64), 2), 0), None);
}
#[test]
fn the_wave_guard_admits_every_measured_winner_on_its_own_board() {
for (rows, columns, candidate, reference) in [
(4621, 384, footprint((144, 96), 1), footprint((128, 64), 2)),
(
4096,
3072,
footprint((128, 192), 1),
footprint((128, 64), 2),
),
(3072, 1536, footprint((192, 192), 1), footprint((64, 64), 4)),
(768, 3072, footprint((96, 192), 1), footprint((128, 96), 1)),
(1536, 768, footprint((96, 96), 1), footprint((128, 96), 1)),
(2048, 3072, footprint((128, 128), 1), footprint((64, 64), 2)),
(128, 512, footprint((32, 16), 3), footprint((64, 64), 2)),
(1024, 128, footprint((16, 64), 2), footprint((64, 64), 2)),
] {
assert!(
wave_guard_admits(rows, columns, candidate, reference, ADA_MULTIPROCESSORS),
"{rows}x{columns} {candidate:?} against {reference:?}"
);
}
}
#[test]
fn the_wave_guard_declines_a_grid_that_spills_into_a_wave_the_reference_avoids() {
let candidate = footprint((144, 96), 1);
let reference = footprint((128, 64), 2);
assert!(wave_guard_admits(4621, 384, candidate, reference, 142));
assert!(!wave_guard_admits(4621, 384, candidate, reference, 130));
assert!(wave_guard_admits(
4621,
384,
footprint((64, 64), 1),
footprint((128, 64), 2),
130
));
}
#[test]
fn ledger_records_one_verdict_per_candidate_and_cell() {
let mut ledger = RouteProofLedger::default();
let key = RouteProofKey {
candidate: "tn_sm89_relay_m64n64_bk64_s3_bf16",
op: ResolvedGemmOp::Tn,
dtype: WeightDtype::Bf16,
dims: (2048, 1536, 768),
};
assert_eq!(ledger.verdict(key), None);
ledger.record(key, RouteProofVerdict::Admitted);
assert_eq!(ledger.verdict(key), Some(RouteProofVerdict::Admitted));
let other_dims = RouteProofKey {
dims: (2048, 768, 3072),
..key
};
assert_eq!(ledger.verdict(other_dims), None);
ledger.record(other_dims, RouteProofVerdict::Declined);
assert_eq!(ledger.len(), 2);
ledger.record(key, RouteProofVerdict::Declined);
assert_eq!(ledger.len(), 2);
assert_eq!(ledger.verdict(key), Some(RouteProofVerdict::Declined));
}
}