use super::{
compute::interpolate_tile_kernel,
coordinate::Rational,
filter::{BicubicFilter, BilinearFilter, Lanczos3Filter, NearestFilter, SeparableFilterFamily},
geometry::TileGeometry,
space::{self, CHANNEL},
};
use crate::InputStage;
use crate::{
InterpolateError, InterpolateStrategy,
definition::{InterpolateForwardProblem, InterpolateMode, InterpolateOptions, get_transform},
};
use cubecl::{client::Client, ir::ElemType, prelude::*};
use cubek_tile::{
Geometry, Launcher,
launch::{BoundaryPolicy, Grid},
};
pub(crate) fn interpolate_launch(
client: &Client,
input: TensorBinding,
output: TensorBinding,
options: InterpolateOptions,
dtype: ElemType,
strategy: InterpolateStrategy,
) -> Result<(), InterpolateError> {
let hardware = &client.properties().hardware;
let problem = InterpolateForwardProblem::from_input_output_shapes(
&input.shape,
&[output.shape[1], output.shape[2]],
options,
);
let blueprint = strategy.blueprint(hardware, &problem);
blueprint.validate()?;
if blueprint.input_residence == InputStage::Smem && hardware.num_cpu_cores.is_some() {
return Err(InterpolateError::SharedMemoryUnsupportedOnCpu);
}
let fallback = match strategy {
InterpolateStrategy::Forced(_) => None,
_ => Some(InputStage::InPlace),
};
let geometry =
TileGeometry::from_blueprint(blueprint, output.shape[3], hardware.plane_size_max as usize);
let residence = blueprint.input_residence;
match options.mode {
InterpolateMode::Nearest(_) => launch::<NearestFilter>(
client, input, output, options, dtype, geometry, residence, fallback,
),
InterpolateMode::Bilinear => launch::<BilinearFilter>(
client, input, output, options, dtype, geometry, residence, fallback,
),
InterpolateMode::Bicubic => launch::<BicubicFilter>(
client, input, output, options, dtype, geometry, residence, fallback,
),
InterpolateMode::Lanczos3 => launch::<Lanczos3Filter>(
client, input, output, options, dtype, geometry, residence, fallback,
),
}
}
#[allow(clippy::too_many_arguments)]
fn launch<F: SeparableFilterFamily>(
client: &Client,
input: TensorBinding,
output: TensorBinding,
options: InterpolateOptions,
dtype: ElemType,
geometry: TileGeometry,
residence: InputStage,
fallback: Option<InputStage>,
) -> Result<(), InterpolateError> {
let Some(fallback) = fallback.filter(|_| residence == InputStage::Smem) else {
return dispatch::<F>(client, input, output, options, dtype, geometry, residence);
};
match dispatch::<F>(
client,
input.clone(),
output.clone(),
options,
dtype,
geometry,
residence,
) {
Err(InterpolateError::SharedMemoryLimitExceeded { .. }) => {
dispatch::<F>(client, input, output, options, dtype, geometry, fallback)
}
result => result,
}
}
fn dispatch<F: SeparableFilterFamily>(
client: &Client,
input: TensorBinding,
output: TensorBinding,
options: InterpolateOptions,
dtype: ElemType,
geometry: TileGeometry,
residence: InputStage,
) -> Result<(), InterpolateError> {
let (input_h, input_w, output_h, output_w) = (
input.shape[1],
input.shape[2],
output.shape[1],
output.shape[2],
);
let row = Rational::of(get_transform(input_h, output_h, options));
let col = Rational::of(get_transform(input_w, output_w, options));
let units = client.properties().hardware.plane_size_max as usize;
let max_units = client.properties().hardware.max_units_per_cube as usize;
let units_per_cube = geometry.planes_per_cube.saturating_mul(units);
if units_per_cube > max_units {
return Err(InterpolateError::UnitsPerCubeExceeded {
requested: units_per_cube,
available: max_units,
});
}
let plan = space::InterpolateSpace {
batch: output.shape[0],
height: output_h,
width: output_w,
channels: output.shape[3],
plane_size: units,
taps: F::mode_properties().taps,
geometry,
};
let (cube_count, cube_dim) = plan.grid();
let launch = Launcher::new(
client,
plan.partitioning(),
&plan.space(),
Grid::Stated {
cube_count,
cube_dim,
},
)?;
let vector_size = launch.vector_size(
CHANNEL,
&[
(&Geometry::from(&input), &[CHANNEL]),
(&Geometry::from(&output), &[CHANNEL]),
],
dtype.size(),
);
let properties = F::mode_properties();
let in_bounds = tap_range_in_bounds(row, output_h, input_h, properties.taps, F::radius())
&& tap_range_in_bounds(col, output_w, input_w, properties.taps, F::radius());
let stage_width = (residence == InputStage::Smem
&& geometry.channel_block != vector_size
&& client
.io_optimized_vector_sizes(dtype.size())
.any(|v| v == geometry.channel_block))
.then_some(geometry.channel_block);
let checked = !in_bounds || stage_width.is_some();
if residence == InputStage::Smem {
let available = client.properties().hardware.max_shared_memory_size;
let requested = space::stage_window_bytes(
row,
col,
properties.taps,
F::radius(),
geometry,
stage_width.unwrap_or(vector_size),
dtype.size(),
);
if requested > available {
return Err(InterpolateError::SharedMemoryLimitExceeded {
requested,
available,
});
}
}
let input_arg = launch
.arg(input)
.gathered(space::input_projection(row, col, F::radius()))
.boundary(match checked {
true => BoundaryPolicy::Every(properties.boundary),
false => BoundaryPolicy::Unchecked,
})
.vectorize(vector_size)
.build()?;
let output_arg = launch
.arg(output)
.axes(&[space::BATCH, space::OUTPUT_H, space::OUTPUT_W, CHANNEL])
.vectorize(vector_size)
.build()?;
interpolate_tile_kernel::launch::<F>(
client,
launch.cube_count(),
launch.cube_dim(),
vector_size,
input_arg.arg(),
output_arg.arg(),
launch.partitioning_arg(),
row.scale as u32,
row.offset as i32,
row.divisor as u32,
col.scale as u32,
col.offset as i32,
col.divisor as u32,
F::radius(),
residence,
stage_width,
space::register_block(client),
dtype,
);
Ok(())
}
fn tap_range_in_bounds(
source: Rational,
outputs: usize,
input: usize,
taps: usize,
radius: usize,
) -> bool {
let first = source.offset.div_euclid(source.divisor as isize) - radius as isize;
let numerator = (outputs.saturating_sub(1) as isize) * source.scale as isize + source.offset;
let last = numerator.div_euclid(source.divisor as isize) + (taps - 1 - radius) as isize;
first >= 0 && last < input as isize
}