1use cubecl::{
2 calculate_cube_count_elemwise,
3 client::Client,
4 ir::{FloatKind, VectorRegisters},
5 num_traits::Zero,
6 prelude::*,
7 std::tensor::layout::linear::{LinearViewMut, linear_view},
8 std::{FastDivmod, FastDivmodInt},
9 tensor_vector_size_parallel,
10};
11
12use crate::{components::ConvSetupError, launch::ConvolutionArgs};
13
14#[cube]
15fn decompose_linear<I: FastDivmodInt>(pos: I, shape: &Sequence<FastDivmod<I>>) -> (I, Sequence<I>) {
16 let rank = comptime![shape.len()];
17 let mut offs = pos;
18 let mut out = Sequence::new();
19
20 #[unroll]
21 for i in 0..rank {
22 let dim = comptime![rank - i - 1];
23 let (rem, offs_local) = shape.index(dim).div_mod(offs);
24 out.push(offs_local);
25 offs = rem;
26 }
27
28 (offs, out.reversed())
29}
30
31#[derive(CubeLaunch, CubeType, Clone)]
32pub(crate) struct ConvParam {
33 pub stride: u32,
34 pub dilation: u32,
35 pub padding: i32,
36}
37
38#[derive(CubeLaunch, CubeType)]
39struct Conv2dArgs {
40 conv_params: Sequence<ConvParam>,
41 channels_per_group: u32,
42}
43
44#[cube(launch_unchecked, address_type = "dynamic")]
45#[allow(clippy::redundant_closure)]
46fn direct_conv2d_kernel<E: Numeric, NIn: Size, NOut: Size>(
47 input: &Tensor<Vector<E, NIn>>,
48 weight: &Tensor<Vector<E, NIn>>,
49 bias: ComptimeOption<&[Vector<E, NOut>]>,
50 mut output: LinearViewMut<'_, Vector<E, NOut>>,
51 args: Conv2dArgs,
52 shape_out: Sequence<FastDivmod<u32>>,
53 shape_out_c: FastDivmod<u32>,
54 #[comptime] has_padding: bool,
55 #[comptime] accumulate_components: bool,
56 #[comptime] channel_block: usize,
57 #[define(E)] _dtype: ElemType,
58) {
59 if !output.is_in_bounds(ABSOLUTE_POS) {
60 terminate!();
61 }
62
63 let n_spatial = comptime![shape_out.len()];
64
65 let vector_size_out = output.vector_size();
66 let pos = ABSOLUTE_POS * vector_size_out;
67
68 let in_c_per_group = weight.shape(weight.rank() - 1) as u32;
69
70 let (rem, out_c) = shape_out_c.div_mod(pos as u32);
71 let (b, spatial_pos) = decompose_linear(rem, &shape_out);
72
73 let g = out_c / args.channels_per_group;
74 let ic_start = in_c_per_group * g;
75
76 let bias: ComptimeOption<Vector<E, NOut>> =
77 bias.map(|bias| bias[out_c as usize / vector_size_out]);
78 let mut sum = bias.unwrap_or_else(|| Vector::zero());
79
80 let in_offs = b as usize * input.stride(0) + ic_start as usize;
81
82 let stride_oc = weight.stride(0);
83
84 let mut in_shape = Sequence::new();
85 let mut in_strides = Sequence::new();
86 let mut kernel_shape = Sequence::new();
87 let mut kernel_strides = Sequence::new();
88
89 #[unroll]
90 for i in 0..n_spatial {
91 in_shape.push(input.shape(i + 1) as u32);
92 in_strides.push(input.stride(i + 1));
93 kernel_shape.push(weight.shape(i + 1) as u32);
94 kernel_strides.push(weight.stride(i + 1));
95 }
96
97 let weight_offs = out_c as usize * stride_oc;
98
99 let loop_params = LoopParams {
100 out_pos: spatial_pos,
101 in_shape,
102 in_strides,
103 kernel_shape,
104 kernel_strides,
105 conv_params: args.conv_params,
106 in_c_per_group,
107 stride_oc,
108 };
109
110 let vector_size_in = input.vector_size();
111
112 if accumulate_components {
113 comptime!(assert!(
114 vector_size_out.is_multiple_of(channel_block),
115 "a block of {channel_block} channels does not divide the output vector of {vector_size_out}"
116 ));
117
118 #[unroll]
119 for bi in 0..comptime![vector_size_out / channel_block] {
120 let base_v = bi * channel_block;
121 let mut partials = Array::<Vector<E, NIn>>::new(channel_block);
122
123 kernel_loop(
124 input,
125 weight,
126 &mut sum,
127 &mut partials,
128 in_offs,
129 true,
130 weight_offs,
131 &loop_params,
132 0usize,
133 has_padding,
134 accumulate_components,
135 base_v,
136 );
137
138 #[unroll]
139 for j in 0..channel_block {
140 let mut channel = sum.extract(base_v + j);
141
142 #[unroll]
143 for i in 0..vector_size_in {
144 channel += partials[j].extract(i);
145 }
146
147 sum.insert(base_v + j, channel);
148 }
149 }
150 } else {
151 let mut partials = Array::<Vector<E, NIn>>::new(1usize);
153
154 kernel_loop(
155 input,
156 weight,
157 &mut sum,
158 &mut partials,
159 in_offs,
160 true,
161 weight_offs,
162 &loop_params,
163 0usize,
164 has_padding,
165 accumulate_components,
166 0usize,
167 );
168 }
169
170 output.write(ABSOLUTE_POS, sum);
171}
172
173#[derive(CubeType, Clone)]
174struct LoopParams {
175 out_pos: Sequence<u32>,
176 in_shape: Sequence<u32>,
177 in_strides: Sequence<usize>,
178 kernel_shape: Sequence<u32>,
179 kernel_strides: Sequence<usize>,
180 conv_params: Sequence<ConvParam>,
181
182 in_c_per_group: u32,
183 stride_oc: usize,
184}
185
186#[cube]
187fn kernel_loop<E: Numeric, NIn: Size, NOut: Size>(
188 input: &Tensor<Vector<E, NIn>>,
189 weight: &Tensor<Vector<E, NIn>>,
190 sum: &mut Vector<E, NOut>,
191 partials: &mut Array<Vector<E, NIn>>,
192 in_offs: usize,
193 in_bounds: bool,
194 weight_offs: usize,
195 params: &LoopParams,
196 #[comptime] kernel_dim: usize,
197 #[comptime] has_padding: bool,
198 #[comptime] accumulate_components: bool,
199 #[comptime] base_v: usize,
200) {
201 if comptime![kernel_dim < params.kernel_shape.len()] {
202 let out_idx = *params.out_pos.index(kernel_dim);
203 let conv = params.conv_params.index(kernel_dim);
204 let shape = *params.in_shape.index(kernel_dim);
205 let stride = *params.in_strides.index(kernel_dim);
206 let k_stride = *params.kernel_strides.index(kernel_dim);
207
208 for pos in 0..*params.kernel_shape.index(kernel_dim) {
209 let in_pos = (out_idx * conv.stride + pos * conv.dilation) as i32 - conv.padding;
210 let in_offs = in_offs + in_pos as usize * stride;
211 let weight_offs = weight_offs + pos as usize * k_stride;
212 let mut in_bounds = in_bounds;
213
214 if has_padding {
215 in_bounds &= in_pos >= 0 && (in_pos as u32) < shape;
216 }
217
218 kernel_loop(
219 input,
220 weight,
221 sum,
222 partials,
223 in_offs,
224 in_bounds,
225 weight_offs,
226 params,
227 comptime![kernel_dim + 1],
228 has_padding,
229 accumulate_components,
230 base_v,
231 );
232 }
233 } else {
234 kernel_loop_inner(
235 input,
236 weight,
237 sum,
238 partials,
239 in_offs,
240 in_bounds,
241 weight_offs,
242 params.in_c_per_group,
243 params.stride_oc,
244 accumulate_components,
245 base_v,
246 );
247 }
248}
249
250#[cube]
251fn kernel_loop_inner<E: Numeric, NIn: Size, NOut: Size>(
252 input: &Tensor<Vector<E, NIn>>,
253 weight: &Tensor<Vector<E, NIn>>,
254 sum: &mut Vector<E, NOut>,
255 partials: &mut Array<Vector<E, NIn>>,
256 in_offs: usize,
257 in_bounds: bool,
258 weight_offs: usize,
259 in_c_per_group: u32,
260 stride_oc: usize,
261 #[comptime] accumulate_components: bool,
262 #[comptime] base_v: usize,
263) {
264 if in_bounds {
265 if accumulate_components {
266 accumulate_in_components(
267 input,
268 weight,
269 partials,
270 in_offs,
271 weight_offs,
272 in_c_per_group,
273 stride_oc,
274 base_v,
275 );
276 } else {
277 accumulate_per_step(
278 input,
279 weight,
280 sum,
281 in_offs,
282 weight_offs,
283 in_c_per_group,
284 stride_oc,
285 );
286 }
287 }
288}
289
290#[cube]
293fn accumulate_in_components<E: Numeric, NIn: Size>(
294 input: &Tensor<Vector<E, NIn>>,
295 weight: &Tensor<Vector<E, NIn>>,
296 partials: &mut Array<Vector<E, NIn>>,
297 in_offs: usize,
298 weight_offs: usize,
299 in_c_per_group: u32,
300 stride_oc: usize,
301 #[comptime] base_v: usize,
302) {
303 let vector_size_in = input.vector_size();
304 let block = partials.len();
305
306 for in_c in range_stepped(0, in_c_per_group, vector_size_in as u32) {
307 let val = input[(in_offs + in_c as usize) / vector_size_in];
308
309 #[unroll]
310 for j in 0..block {
311 let weight_offs = weight_offs + (base_v + j) * stride_oc + in_c as usize;
312
313 partials[j] += val * weight[weight_offs / vector_size_in];
314 }
315 }
316}
317
318#[cube]
320fn accumulate_per_step<E: Numeric, NIn: Size, NOut: Size>(
321 input: &Tensor<Vector<E, NIn>>,
322 weight: &Tensor<Vector<E, NIn>>,
323 sum: &mut Vector<E, NOut>,
324 in_offs: usize,
325 weight_offs: usize,
326 in_c_per_group: u32,
327 stride_oc: usize,
328) {
329 let vector_size_in = input.vector_size();
330 let vector_size_out = sum.vector_size();
331
332 for in_c in range_stepped(0, in_c_per_group, vector_size_in as u32) {
333 let in_pos = in_offs + in_c as usize;
334 let mut weight_pos = weight_offs + in_c as usize;
335
336 let val = input[in_pos / vector_size_in];
337
338 #[unroll]
339 for v in 0..vector_size_out {
340 let weight = weight[weight_pos / vector_size_in];
341 let val = val * weight;
342
343 #[unroll]
344 for i in 0..vector_size_in {
345 sum.insert(v, sum.extract(v) + val.extract(i));
346 }
347 weight_pos += stride_oc;
348 }
349 }
350}
351
352pub struct DirectTensors {
353 pub input: TensorBinding,
354 pub weight: TensorBinding,
355 pub bias: Option<TensorBinding>,
356 pub out: TensorBinding,
357}
358
359pub fn launch_direct<const N: usize>(
361 client: &Client,
362 tensors: DirectTensors,
363 args: ConvolutionArgs<N>,
364 groups: usize,
365 dtype: ElemType,
366) -> Result<(), ConvSetupError> {
367 let DirectTensors {
368 input,
369 weight,
370 bias,
371 out,
372 } = tensors;
373
374 let rank = input.shape.len();
375 let dim_c = rank - 1;
376
377 let in_shape = &input.shape[1..dim_c];
378 let out_channels = weight.shape[0];
379 let kernel_shape = &weight.shape[1..dim_c];
380 let out_size = &out.shape[1..dim_c];
381
382 let channels_per_group = out_channels / groups;
383 let check_spatial_bounds = should_check_spatial_bounds(in_shape, kernel_shape, out_size, &args);
384
385 let mut grouped_out_shape = out.shape.clone();
388 grouped_out_shape[dim_c] = channels_per_group;
389 let vector_size_out = tensor_vector_size_parallel(
390 client.io_optimized_vector_sizes(dtype.size()),
391 &grouped_out_shape,
392 &out.strides,
393 dim_c,
394 );
395 let vector_size_in = tensor_vector_size_parallel(
397 client.io_optimized_vector_sizes(dtype.size()),
398 &weight.shape,
399 &weight.strides,
400 weight.shape.len() - 1,
401 );
402
403 let accumulate_components =
406 client.properties().hardware.plane_size_max == 1 && vector_size_in > 1;
407 let channel_block =
408 VectorRegisters::new(&client.properties().hardware, register_elem_size(dtype))
409 .map_or(1, |registers| {
410 channel_block(registers, vector_size_in, vector_size_out)
411 });
412
413 let shape_out = out.shape[1..dim_c].iter().map(|s| *s as u32).collect();
414 let shape_out_c = out_channels as u32;
415
416 let mut conv_params = SequenceArg::new();
417
418 for i in 0..kernel_shape.len() {
419 conv_params.push(ConvParamLaunch::new(
420 args.stride[i] as u32,
421 args.dilation[i] as u32,
422 args.padding[i] as i32,
423 ));
424 }
425
426 let working_units = out.shape.iter().product::<usize>() / vector_size_out as usize;
427 let cube_dim = CubeDim::new(client, working_units);
428 let cube_count = calculate_cube_count_elemwise(client, working_units, cube_dim);
429
430 let address_type = input
431 .required_address_type(dtype.size())
432 .max(weight.required_address_type(dtype.size()))
433 .max(out.required_address_type(dtype.size()));
434
435 unsafe {
436 direct_conv2d_kernel::launch_unchecked(
437 client,
438 cube_count,
439 cube_dim,
440 address_type,
441 vector_size_in,
442 vector_size_out,
443 input.into_tensor_arg(),
444 weight.into_tensor_arg(),
445 bias.map(|b| b.into_buffer_arg()).into(),
446 linear_view(out),
447 Conv2dArgsLaunch::new(conv_params, channels_per_group as u32),
448 shape_out,
449 shape_out_c,
450 check_spatial_bounds,
451 accumulate_components,
452 channel_block,
453 dtype,
454 )
455 };
456
457 Ok(())
458}
459
460fn channel_block(
462 registers: VectorRegisters,
463 vector_size_in: usize,
464 vector_size_out: usize,
465) -> usize {
466 let operands = registers.count() / 2;
468 let block = registers.vectors_fitting(vector_size_in, operands).max(1);
469
470 (1 << block.ilog2()).min(vector_size_out)
471}
472
473fn register_elem_size(dtype: ElemType) -> usize {
475 match dtype {
476 ElemType::Float(FloatKind::F16 | FloatKind::BF16) => size_of::<f32>(),
477 dtype => dtype.size(),
478 }
479}
480
481fn should_check_spatial_bounds<const N: usize>(
482 in_shape: &[usize],
483 kernel_shape: &[usize],
484 out_shape: &[usize],
485 args: &ConvolutionArgs<N>,
486) -> bool {
487 (0..N).any(|dim| {
488 let begin = args.padding[dim] as i64;
489 let first = -begin;
490 let last = (out_shape[dim] as i64 - 1) * args.stride[dim] as i64
491 + (kernel_shape[dim] as i64 - 1) * args.dilation[dim] as i64
492 - begin;
493 first < 0 || last >= in_shape[dim] as i64
494 })
495}
496
497#[cfg(test)]
498mod tests {
499 use cubek_test_utils::hardware::{AVX2, AVX512, NEON};
500
501 use super::*;
502
503 fn avx2(elem_size: usize) -> VectorRegisters {
504 VectorRegisters::new(&AVX2, elem_size).unwrap()
505 }
506
507 fn avx512(elem_size: usize) -> VectorRegisters {
508 VectorRegisters::new(&AVX512, elem_size).unwrap()
509 }
510
511 fn neon(elem_size: usize) -> VectorRegisters {
512 VectorRegisters::new(&NEON, elem_size).unwrap()
513 }
514
515 #[test]
516 fn a_two_register_accumulator_halves_the_block() {
517 assert_eq!(channel_block(avx2(4), 16, 16), 4);
518 }
519
520 #[test]
521 fn a_four_register_accumulator_quarters_the_block() {
522 assert_eq!(channel_block(neon(4), 16, 16), 4);
523 }
524
525 #[test]
526 fn a_register_wide_accumulator_spends_one() {
527 assert_eq!(channel_block(avx2(4), 8, 16), 8);
528 assert_eq!(channel_block(avx512(4), 16, 16), 16);
529 }
530
531 #[test]
532 fn a_narrow_accumulator_still_spends_a_whole_register() {
533 assert_eq!(channel_block(avx2(4), 2, 16), 8);
534 }
535
536 #[test]
537 fn a_half_float_accumulator_spends_f32_registers() {
538 let f16 = register_elem_size(ElemType::Float(FloatKind::F16));
539 assert_eq!(channel_block(avx2(f16), 32, 32), 2);
540 assert_eq!(channel_block(neon(f16), 32, 32), 2);
541 }
542
543 #[test]
544 fn the_block_never_outgrows_the_output_vector() {
545 assert_eq!(channel_block(avx2(4), 8, 2), 2);
546 }
547
548 #[test]
549 fn an_accumulator_wider_than_the_budget_still_gets_a_block_of_one() {
550 assert_eq!(channel_block(avx2(8), 64, 16), 1);
551 }
552}