use cubecl::ir::HardwareProperties;
use crate::definition::{InterpolateError, InterpolateForwardProblem, mode_properties};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum InputStage {
InPlace,
Smem,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct InterpolateBlueprint {
pub input_residence: InputStage,
pub planes_per_cube: usize,
pub rows_per_plane: usize,
pub cols_per_unit: usize,
pub channel_block: Option<usize>,
}
impl InterpolateBlueprint {
pub const fn new(
input_residence: InputStage,
planes_per_cube: usize,
rows_per_plane: usize,
cols_per_unit: usize,
) -> Self {
Self {
input_residence,
planes_per_cube,
rows_per_plane,
cols_per_unit,
channel_block: None,
}
}
pub const fn with_channel_block(self, block: usize) -> Self {
Self {
channel_block: Some(block),
..self
}
}
pub(crate) fn validate(&self) -> Result<(), InterpolateError> {
if self.planes_per_cube == 0 {
return Err(InterpolateError::ZeroPlanesPerCube);
}
if self.rows_per_plane == 0 {
return Err(InterpolateError::ZeroRowsPerPlane);
}
if self.cols_per_unit == 0 {
return Err(InterpolateError::ZeroColsPerUnit);
}
if self.channel_block == Some(0) {
return Err(InterpolateError::ZeroChannelBlock);
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InterpolateStrategy {
MaximizeThroughput,
MinimizeLatency,
Forced(InterpolateBlueprint),
}
const STREAMING_PLANES_PER_CUBE: usize = 16;
const WIDE_PLANES_PER_CUBE: usize = 8;
const CONCURRENT_PLANES_PER_CUBE: usize = 4;
const CPU_COLS_PER_UNIT: usize = 2;
const MAX_ROWS_PER_CUBE: usize = 256;
impl InterpolateStrategy {
pub fn blueprint(
&self,
hardware: &HardwareProperties,
problem: &InterpolateForwardProblem,
) -> InterpolateBlueprint {
if let Self::Forced(blueprint) = self {
return *blueprint;
}
let is_cpu = hardware.num_cpu_cores.is_some();
let intent = match is_cpu {
true => Self::MaximizeThroughput,
false => *self,
};
let taps = mode_properties(problem.options.mode).taps;
let budget = rows_per_cube(hardware, problem);
let planes = intent.planes_per_cube(hardware, taps, budget);
let rows = rows_per_plane(problem, taps, budget / planes);
let cols = match is_cpu {
true => CPU_COLS_PER_UNIT,
false => 1,
};
InterpolateBlueprint::new(intent.input_residence(taps), planes, rows, cols)
}
fn planes_per_cube(&self, hardware: &HardwareProperties, taps: usize, budget: usize) -> usize {
let wanted = match (hardware.num_cpu_cores, self) {
(Some(cores), _) => cores as usize,
(None, Self::MaximizeThroughput) if taps == 1 => STREAMING_PLANES_PER_CUBE,
(None, Self::MaximizeThroughput) => WIDE_PLANES_PER_CUBE,
(None, _) => CONCURRENT_PLANES_PER_CUBE,
};
let plane_units = (hardware.plane_size_max as usize).max(1);
let units = (hardware.max_units_per_cube as usize / plane_units).max(1);
floor_power_of_two(wanted.min(units).min(budget))
}
fn input_residence(&self, taps: usize) -> InputStage {
match self {
Self::MinimizeLatency if taps > 1 => InputStage::Smem,
_ => InputStage::InPlace,
}
}
}
fn rows_per_cube(hardware: &HardwareProperties, problem: &InterpolateForwardProblem) -> usize {
let cubes_wanted = hardware.num_streaming_multiprocessors.unwrap_or(1).max(1) as usize;
let rows_available = problem.batch.saturating_mul(problem.output_height);
(rows_available / cubes_wanted).clamp(1, MAX_ROWS_PER_CUBE)
}
fn rows_per_plane(problem: &InterpolateForwardProblem, taps: usize, budget: usize) -> usize {
let reuse = match taps {
1 => 1,
_ => (problem.output_height / problem.input_height.max(1)).max(1),
};
floor_power_of_two(reuse.min(budget))
}
fn floor_power_of_two(extent: usize) -> usize {
match extent {
0 => 1,
extent => 1 << extent.ilog2(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::definition::{InterpolateMode, InterpolateOptions, NearestMode};
use cubek_test_utils::hardware::{AVX2, GPU};
fn cpu(cores: u32) -> HardwareProperties {
HardwareProperties {
max_units_per_cube: cores,
max_cube_dim: (cores, cores, cores),
num_cpu_cores: Some(cores),
..AVX2
}
}
fn problem(
mode: InterpolateMode,
batch: usize,
input_height: usize,
output_height: usize,
) -> InterpolateForwardProblem {
InterpolateForwardProblem {
batch,
input_height,
input_width: input_height,
channels: 16,
output_height,
output_width: output_height,
options: InterpolateOptions::new(mode),
}
}
fn upsample() -> InterpolateForwardProblem {
problem(InterpolateMode::Bilinear, 4, 256, 512)
}
#[test]
fn the_two_intents_resolve_to_different_blueprints() {
let hardware = GPU;
let problem = upsample();
assert_ne!(
InterpolateStrategy::MaximizeThroughput.blueprint(&hardware, &problem),
InterpolateStrategy::MinimizeLatency.blueprint(&hardware, &problem)
);
}
#[test]
fn only_the_latency_intent_stages_the_input() {
let hardware = GPU;
let problem = upsample();
assert_eq!(
InterpolateStrategy::MaximizeThroughput
.blueprint(&hardware, &problem)
.input_residence,
InputStage::InPlace
);
assert_eq!(
InterpolateStrategy::MinimizeLatency
.blueprint(&hardware, &problem)
.input_residence,
InputStage::Smem
);
}
#[test]
fn a_single_tap_stages_nothing() {
let problem = problem(InterpolateMode::Nearest(NearestMode::Floor), 4, 256, 512);
assert_eq!(
InterpolateStrategy::MinimizeLatency
.blueprint(&GPU, &problem)
.input_residence,
InputStage::InPlace
);
}
#[test]
fn the_intents_collapse_on_a_cpu() {
let hardware = cpu(12);
assert_eq!(
InterpolateStrategy::MaximizeThroughput.blueprint(&hardware, &upsample()),
InterpolateStrategy::MinimizeLatency.blueprint(&hardware, &upsample())
);
}
#[test]
fn a_cpu_never_stages_and_takes_a_plane_per_core() {
let hardware = cpu(12);
for strategy in [
InterpolateStrategy::MaximizeThroughput,
InterpolateStrategy::MinimizeLatency,
] {
let blueprint = strategy.blueprint(&hardware, &upsample());
assert_eq!(blueprint.input_residence, InputStage::InPlace);
assert_eq!(blueprint.planes_per_cube, 8);
assert!(blueprint.cols_per_unit > 1);
}
}
#[test]
fn a_gpu_unit_takes_one_column() {
assert_eq!(
InterpolateStrategy::MaximizeThroughput
.blueprint(&GPU, &upsample())
.cols_per_unit,
1
);
}
#[test]
fn the_row_run_follows_the_resampling_ratio() {
let hardware = GPU;
let rows = |problem| {
InterpolateStrategy::MinimizeLatency
.blueprint(&hardware, &problem)
.rows_per_plane
};
assert_eq!(rows(problem(InterpolateMode::Bilinear, 4, 64, 256)), 4);
assert_eq!(rows(problem(InterpolateMode::Bilinear, 4, 64, 64)), 1);
assert_eq!(rows(problem(InterpolateMode::Bilinear, 4, 256, 64)), 1);
}
#[test]
fn a_cube_never_outruns_the_output_it_covers() {
let hardware = GPU;
for problem in [
problem(InterpolateMode::Nearest(NearestMode::Floor), 1, 4, 8),
problem(InterpolateMode::Bilinear, 1, 4, 8),
problem(InterpolateMode::Lanczos3, 1, 8, 8),
problem(InterpolateMode::Bicubic, 2, 32, 128),
] {
for strategy in [
InterpolateStrategy::MaximizeThroughput,
InterpolateStrategy::MinimizeLatency,
] {
let blueprint = strategy.blueprint(&hardware, &problem);
let rows_per_cube = blueprint.planes_per_cube * blueprint.rows_per_plane;
assert!(
rows_per_cube <= problem.batch * problem.output_height,
"{strategy:?} walks {rows_per_cube} rows of {}",
problem.batch * problem.output_height
);
}
}
}
#[test]
fn a_narrow_device_bounds_the_plane_count() {
let hardware = HardwareProperties {
max_units_per_cube: 128,
..GPU
};
let blueprint = InterpolateStrategy::MaximizeThroughput.blueprint(&hardware, &upsample());
assert_eq!(blueprint.planes_per_cube, 128 / 32);
}
#[test]
fn every_inferred_blueprint_is_valid() {
for hardware in [GPU, cpu(1), cpu(12)] {
for problem in [
problem(InterpolateMode::Nearest(NearestMode::Exact), 1, 1, 1),
problem(InterpolateMode::Bilinear, 1, 4, 8),
problem(InterpolateMode::Lanczos3, 8, 1024, 256),
] {
for strategy in [
InterpolateStrategy::MaximizeThroughput,
InterpolateStrategy::MinimizeLatency,
] {
strategy
.blueprint(&hardware, &problem)
.validate()
.unwrap_or_else(|e| panic!("{strategy:?} on {hardware:?}: {e}"));
}
}
}
}
#[test]
fn a_forced_blueprint_is_taken_as_stated() {
let blueprint = InterpolateBlueprint::new(InputStage::Smem, 2, 8, 4).with_channel_block(3);
assert_eq!(
InterpolateStrategy::Forced(blueprint).blueprint(&cpu(12), &upsample()),
blueprint
);
}
}