use cubecl::{
ir::ElemType,
throughput::{ThroughputKey, ThroughputMode},
tune::Work,
};
use crate::definition::{InterpolateMode, InterpolateProblem, mode_properties};
#[derive(Debug, Clone)]
pub struct InterpolateCost {
pub problem: InterpolateProblem,
pub dtype: ElemType,
}
impl InterpolateCost {
pub fn new(problem: InterpolateProblem, dtype: ElemType) -> Self {
Self { problem, dtype }
}
pub fn work(&self) -> Work {
let (read, written) = self.traffic();
Work {
compute_ops: self.compute_ops(),
bytes: read + written,
}
}
pub fn compute_ops(&self) -> usize {
match &self.problem {
InterpolateProblem::Forward(prob) => {
prob.batch
* prob.channels
* prob.output_height
* prob.output_width
* ops_per_output(prob.options.mode)
}
InterpolateProblem::Backward(prob) => {
let [batch, grad_height, grad_width, channels] = prob.out_grad_shape;
batch * channels * grad_height * grad_width
}
}
}
pub fn traffic(&self) -> (usize, usize) {
let size = self.dtype.size();
match &self.problem {
InterpolateProblem::Forward(prob) => {
let planes = prob.batch * prob.channels;
let outputs = planes * prob.output_height * prob.output_width;
let taps = mode_properties(prob.options.mode).taps;
let rows_read = prob.input_height.min(prob.output_height * taps);
let cols_read = prob.input_width.min(prob.output_width * taps);
(planes * rows_read * cols_read * size, outputs * size)
}
InterpolateProblem::Backward(prob) => {
let [batch, grad_height, grad_width, channels] = prob.out_grad_shape;
let planes = batch * channels;
let grads = planes * grad_height * grad_width;
let outputs = planes * prob.input_size[0] * prob.input_size[1];
(grads * size, outputs * size)
}
}
}
pub fn compute_key(&self) -> ThroughputKey {
ThroughputKey {
mode: ThroughputMode::ComputeDirect { dtype: self.dtype },
}
}
}
fn ops_per_output(mode: InterpolateMode) -> usize {
if let InterpolateMode::Nearest(_) = mode {
return 4;
}
let taps = mode_properties(mode).taps;
let weights = 2 * taps * weight_ops(mode);
let contraction = taps * taps * 2;
let rows = taps * 2;
weights + contraction + rows + bound_check_ops(mode)
}
fn bound_check_ops(mode: InterpolateMode) -> usize {
if !mode_properties(mode).renormalizes {
return 0;
}
let taps = mode_properties(mode).taps;
let per_tap = 7 + 2 + 1;
let per_row = 2;
taps * taps * per_tap + taps * per_row + 2
}
fn weight_ops(mode: InterpolateMode) -> usize {
match mode {
InterpolateMode::Nearest(_) => 0,
InterpolateMode::Bilinear => 4,
InterpolateMode::Bicubic => 2 + 2 + 3 + 4,
InterpolateMode::Lanczos3 => 3 + 3 + 2 + 2 + 4,
}
}
#[cfg(test)]
mod tests {
use super::*;
use cubecl::ir::FloatKind;
use crate::definition::{
InterpolateBackwardProblem, InterpolateForwardProblem, InterpolateOptions, NearestMode,
};
const F32: ElemType = ElemType::Float(FloatKind::F32);
fn forward(
input: [usize; 4],
output: [usize; 2],
mode: InterpolateMode,
) -> InterpolateForwardProblem {
InterpolateForwardProblem::from_input_output_shapes(
&input.into(),
&output,
InterpolateOptions::new(mode),
)
}
fn work(problem: InterpolateProblem) -> Work {
InterpolateCost::new(problem, F32).work()
}
fn nearest() -> InterpolateMode {
InterpolateMode::Nearest(NearestMode::Floor)
}
#[test]
fn nearest_downsample_reads_only_the_rows_it_lands_on() {
let problem =
InterpolateProblem::Forward(forward([1, 2048, 2048, 3], [1024, 1024], nearest()));
let reads = 3 * 1024 * 1024;
let writes = 3 * 1024 * 1024;
assert_eq!(work(problem).bytes, (reads + writes) * F32.size());
}
#[test]
fn a_two_tap_window_covers_the_whole_input_at_the_same_scale() {
let problem = InterpolateProblem::Forward(forward(
[1, 2048, 2048, 3],
[1024, 1024],
InterpolateMode::Bilinear,
));
let reads = 3 * 2048 * 2048;
let writes = 3 * 1024 * 1024;
assert_eq!(work(problem).bytes, (reads + writes) * F32.size());
}
#[test]
fn an_upsample_never_counts_the_input_more_than_once() {
let problem = InterpolateProblem::Forward(forward(
[1, 2048, 2048, 3],
[4096, 4096],
InterpolateMode::Lanczos3,
));
let reads = 3 * 2048 * 2048;
let writes = 3 * 4096 * 4096;
assert_eq!(work(problem).bytes, (reads + writes) * F32.size());
}
#[test]
fn wider_filters_cost_more_per_output() {
let ops = |mode| ops_per_output(mode);
assert!(ops(nearest()) < ops(InterpolateMode::Bilinear));
assert!(ops(InterpolateMode::Bilinear) < ops(InterpolateMode::Bicubic));
assert!(ops(InterpolateMode::Bicubic) < ops(InterpolateMode::Lanczos3));
}
#[test]
fn only_the_renormalizing_filter_pays_for_bounds() {
assert_eq!(bound_check_ops(nearest()), 0);
assert_eq!(bound_check_ops(InterpolateMode::Bilinear), 0);
assert_eq!(bound_check_ops(InterpolateMode::Bicubic), 0);
assert!(bound_check_ops(InterpolateMode::Lanczos3) > 0);
}
#[test]
fn the_backward_gather_visits_every_gradient_once() {
let grads = 4096 * 4096 * 3;
let outputs = 2048 * 2048 * 3;
let downsampling_gradient =
work(InterpolateProblem::Backward(InterpolateBackwardProblem {
input_size: [2048, 2048],
out_grad_shape: [1, 4096, 4096, 3],
options: InterpolateOptions::new(nearest()),
}));
assert_eq!(downsampling_gradient.compute_ops, grads);
assert_eq!(downsampling_gradient.bytes, (grads + outputs) * F32.size());
let upsampling_gradient = work(InterpolateProblem::Backward(InterpolateBackwardProblem {
input_size: [4096, 4096],
out_grad_shape: [1, 2048, 2048, 3],
options: InterpolateOptions::new(nearest()),
}));
assert_eq!(upsampling_gradient.compute_ops, outputs);
assert_eq!(upsampling_gradient.bytes, (grads + outputs) * F32.size());
}
}