use cubecl::{
calculate_cube_count_elemwise,
ir::EnumSet,
prelude::*,
std::{
FastDivmod,
tensor::{
TensorHandle,
launch::ViewArg,
layout::{
Layout, LayoutExpand, VirtualLayoutLaunch,
chain::{Chain, ChainLaunch},
},
},
},
};
use cubek_matmul::{
definition::{AccumulatorOperand, AvailableVectorSizes, MatmulElems},
multi_level::{
BatchMatmulRoutine,
args::{BatchedCoords, TensorArgs, TensorInputsLaunch, TensorOutputLaunch},
components::global::memory::{
BatchLayout, BatchLayoutLaunch, GlobalLayout, GlobalLayoutLaunch, NoopLayout,
NoopLayoutLaunch, Transpose, TransposeLaunch,
},
definition::Blueprint,
},
};
use cubek_std::{InputBinding, MatrixLayout};
use crate::{
ConvolutionArgs, UnitStrategy,
components::{
ConvSetupError, ConvolutionOperation, ConvolutionParams, ConvolutionProblem,
Dimensionality,
global::{
args::{RuntimeArgs, RuntimeArgsLaunch},
layout::{
Im2colLayout, Im2colLayoutLaunch, NhwcCheck, NhwcLayout, NhwcLayoutLaunch,
OutLayout, OutLayoutLaunch,
},
},
},
routines::{
Routine,
unit::{UnitConv, UnitMatmul},
},
};
const ROWS_PER_SPLIT: usize = 2048;
const MAX_SPLITS: usize = 64;
const MAX_PARTIAL_BYTES: usize = 128 * 1024 * 1024;
pub(crate) fn split_count(rows: usize) -> usize {
let by_rows = (rows / ROWS_PER_SPLIT).clamp(1, MAX_SPLITS);
1 << by_rows.ilog2()
}
#[allow(clippy::too_many_arguments, clippy::result_large_err)]
pub(crate) fn launch_split<const N: usize>(
client: &Client,
input: InputBinding,
out_grad: InputBinding,
weight_grad: TensorBinding,
args: ConvolutionArgs<N>,
strategy: UnitStrategy,
dtypes: MatmulElems,
) -> Result<(), ConvSetupError> {
let dtype = f32::elem_type_native();
if dtypes.lhs_global != dtype
|| dtypes.rhs_global != dtype
|| dtypes.acc_global != dtype
|| input.scale().is_some()
|| out_grad.scale().is_some()
{
return Err(ConvSetupError::Unknown);
}
let op = ConvolutionOperation::BackwardWeight;
let input = UnitConv::correct_layout(client, input.into_data(), dtype, op)?;
let out_grad = UnitConv::correct_layout(client, out_grad.into_data(), dtype, op)?;
let channels = input.shape[N + 1];
let out_channels = out_grad.shape[N + 1];
let rows = out_grad.shape[..N + 1].iter().product::<usize>();
let taps = weight_grad.shape[1..N + 1].iter().product::<usize>();
let padded_channels = channels.next_multiple_of(client.properties().io_width() as usize / 32);
let width = taps * padded_channels;
let mut split = split_count(rows);
let Some(partial_bytes) = out_channels
.checked_mul(width)
.and_then(|n| n.checked_mul(4))
.filter(|bytes| *bytes <= MAX_PARTIAL_BYTES)
else {
return Err(ConvSetupError::Unknown);
};
while split > 1 && partial_bytes * split > MAX_PARTIAL_BYTES {
split /= 2;
}
let per = rows.div_ceil(split).next_multiple_of(32);
if rows > u32::MAX as usize || width > u32::MAX as usize || per > u32::MAX as usize {
return Err(ConvSetupError::Unknown);
}
let partials = TensorHandle::empty(client, [split, out_channels, width], dtype);
let problem = ConvolutionProblem {
m: out_channels,
n: width,
k: rows,
lhs_strides: out_grad.strides.clone(),
rhs_strides: input.strides.clone(),
lhs_layout: MatrixLayout::ColMajor,
rhs_layout: MatrixLayout::RowMajor,
kernel_size: weight_grad.shape[1..N + 1]
.iter()
.map(|n| *n as u32)
.collect(),
stride: args.stride.map(|n| n as u32).to_vec(),
padding: args.padding.map(|n| n as i32).to_vec(),
dilation: args.dilation.map(|n| n as u32).to_vec(),
batches: input.shape[0],
channels,
out_channels,
in_shape: input.shape[1..N + 1].into(),
out_shape: out_grad.shape[1..N + 1].into(),
padded_channels,
operation: op,
dimensionality: match N {
1 => Dimensionality::Dim1,
2 => Dimensionality::Dim2,
_ => Dimensionality::Dim3,
},
global_dtypes: dtypes.as_global_elems(),
address_type: input
.required_address_type(4)
.max(out_grad.required_address_type(4))
.max(partials.clone().binding().required_address_type(4)),
};
let vector_sizes =
UnitConv::filter_vector_sizes(AvailableVectorSizes::from_type_sizes(client, 4, 4, 4))
.filter_lhs_with_tensor(&out_grad.strides, &out_grad.shape, MatrixLayout::RowMajor)
.filter_rhs_with_tensor(&input.strides, &input.shape, MatrixLayout::RowMajor)
.filter_out_with_tensor(partials.strides(), partials.shape())
.pick_max()?;
let mut matmul_problem = problem.as_matmul_problem(AccumulatorOperand::Absent);
matmul_problem.k = per;
matmul_problem.lhs_batches = vec![split].into();
matmul_problem.rhs_batches = vec![split].into();
matmul_problem.out_batches = vec![split].into();
matmul_problem.lhs_shape = vec![split, out_channels, per].into();
matmul_problem.rhs_shape = vec![split, per, width].into();
matmul_problem.out_shape = vec![split, out_channels, width].into();
matmul_problem.lhs_strides = vec![per * out_channels, 1, out_channels].into();
matmul_problem.rhs_strides = vec![per * width, width, 1].into();
matmul_problem.out_strides = partials.strides().into();
let settings =
<UnitMatmul as BatchMatmulRoutine<RuntimeArgs>>::device_settings(client, vector_sizes);
let strategy = strategy.blueprint(client, &matmul_problem, &dtypes);
let expand = <UnitMatmul as BatchMatmulRoutine<RuntimeArgs>>::expand_blueprint(
&matmul_problem,
&settings,
&strategy,
)?;
let launch = <UnitMatmul as BatchMatmulRoutine<RuntimeArgs>>::prepare(
&matmul_problem,
&settings,
expand,
)?;
let blueprint = &launch.blueprint;
type LhsLayout = Chain<Chain<NhwcLayout, Transpose<OutLayout>>, SplitKLayout>;
type RhsLayout = Chain<Chain<NhwcLayout, Im2colLayout>, SplitKLayout>;
let lhs = ChainLaunch::new(
ChainLaunch::new(
NhwcLayoutLaunch::unchecked(),
TransposeLaunch::new(OutLayoutLaunch::from_args(
&problem,
blueprint.lhs_global_layout_config(),
)),
),
SplitKLayoutLaunch::new(
split,
out_channels as u32,
per as u32,
rows as u32,
per as u32,
true,
),
);
let mut checks = EnumSet::empty();
if problem.should_check_spatial_bounds() {
checks.insert(NhwcCheck::Spatial);
}
if problem.should_check_channel() {
checks.insert(NhwcCheck::Channel);
}
let rhs = ChainLaunch::new(
ChainLaunch::new(
NhwcLayoutLaunch::checked(checks),
Im2colLayoutLaunch::from_args(
&problem,
ConvolutionParams::from_problem(&problem),
blueprint.rhs_global_layout_config(),
),
),
SplitKLayoutLaunch::new(
split,
per as u32,
width as u32,
rows as u32,
per as u32,
false,
),
);
let inputs = TensorInputsLaunch::new(
VirtualLayoutLaunch::new::<NoopLayout>(NoopLayoutLaunch::new()),
ViewArg::new_tensor::<LhsLayout>(out_grad.into_tensor_arg(), lhs),
VirtualLayoutLaunch::new::<NoopLayout>(NoopLayoutLaunch::new()),
ViewArg::new_tensor::<RhsLayout>(input.into_tensor_arg(), rhs),
ComptimeOptionArgs::None,
ComptimeOptionArgs::None,
);
let partial_binding = partials.clone().binding();
let layout = GlobalLayoutLaunch::from_handle(
&partial_binding,
vector_sizes.out,
blueprint.out_global_layout_config(),
);
let batch = BatchLayoutLaunch::from_handle(&partial_binding, &matmul_problem);
let output = TensorOutputLaunch::new(
ViewArg::new_tensor::<GlobalLayout>(partial_binding.into_tensor_arg(), layout),
VirtualLayoutLaunch::new::<BatchLayout>(batch),
);
let runtime = RuntimeArgsLaunch::new(per as u32, channels as u32, padded_channels as u32, op);
cubek_matmul::multi_level::launch_kernel::<TensorArgs<RuntimeArgs>, UnitMatmul>(
client, inputs, output, runtime, launch,
)?;
let elements = weight_grad.shape.iter().product::<usize>();
let cube_dim = CubeDim::new(client, elements);
let address_type = partials
.clone()
.binding()
.required_address_type(4)
.max(weight_grad.required_address_type(4));
finish::launch(
client,
calculate_cube_count_elemwise(client, elements, cube_dim),
cube_dim,
address_type,
partials.binding().into_tensor_arg(),
weight_grad.into_tensor_arg(),
channels,
taps,
padded_channels,
elements,
N,
);
Ok(())
}
#[derive(CubeType, CubeLaunch, Clone)]
struct SplitKLayout {
splits: usize,
rows: u32,
cols: u32,
total: u32,
per: u32,
#[cube(comptime)]
lhs: bool,
}
#[cube]
impl Layout for SplitKLayout {
type Coordinates = BatchedCoords;
type SourceCoordinates = BatchedCoords;
fn to_source_pos(&self, pos: BatchedCoords) -> BatchedCoords {
let (split, row, col) = pos;
let offset = split as u32 * self.per;
if self.lhs {
(0, row, col + offset)
} else {
(0, row + offset, col)
}
}
fn shape(&self) -> BatchedCoords {
(self.splits, self.rows, self.cols)
}
fn is_in_bounds(&self, pos: BatchedCoords) -> bool {
let (split, row, col) = pos;
let k = if self.lhs { col } else { row };
split < self.splits
&& row < self.rows
&& col < self.cols
&& split as u32 * self.per + k < self.total
}
fn to_source_pos_checked(&self, pos: BatchedCoords) -> (BatchedCoords, bool) {
(self.to_source_pos(pos), self.is_in_bounds(pos))
}
}
#[cube(launch, address_type = "dynamic")]
fn finish(
partials: &Tensor<f32>,
weight_grad: &mut Tensor<f32>,
channels: FastDivmod<usize>,
taps: FastDivmod<usize>,
padded_channels: usize,
elements: usize,
#[comptime] dims: usize,
) {
let index = ABSOLUTE_POS;
if index >= elements {
terminate!();
}
let (row, channel) = channels.div_mod(index);
let (out_channel, tap) = taps.div_mod(row);
let base =
out_channel * partials.stride(1) + (tap * padded_channels + channel) * partials.stride(2);
let mut value = 0.0f32;
for split in 0..partials.shape(0) {
value += partials[split * partials.stride(0) + base];
}
let mut offset = out_channel * weight_grad.stride(0) + channel * weight_grad.stride(dims + 1);
let mut tap = tap;
#[unroll]
for rev in 0..dims {
let axis = dims - rev;
offset += (tap % weight_grad.shape(axis)) * weight_grad.stride(axis);
tap /= weight_grad.shape(axis);
}
weight_grad[offset] = value;
}