teeny_kernels/nn/fused/conv2d_bn_silu_tiled.rs
1/*
2 * Copyright (c) 2026 Teenygrad.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#![allow(non_snake_case)]
18
19use core::ops::BitAnd;
20
21use teeny_macros::kernel;
22use teeny_triton::triton::{
23 types::{AddOffsets, Comparison, Tensor},
24 *,
25};
26
27/// Fused Conv2d + BatchNorm2d (inference) + SiLU — channel-tiled variant.
28///
29/// Processes BLOCK_N output channels and BLOCK_OW output-width positions
30/// per thread block using a 2-D [BLOCK_N, BLOCK_OW] accumulator built from
31/// outer-product updates: acc += w_row[:, None] * x_col[None, :]
32///
33/// This gives BLOCK_N× better x-value reuse vs the scalar direct-conv kernel
34/// and substantially higher arithmetic intensity for large-channel layers.
35///
36/// Restrictions (enforced by dispatch in graph/mod.rs):
37/// - groups == 1 (non-depthwise, non-grouped)
38/// - C_OUT is a multiple of BLOCK_N (no channel padding needed)
39/// OR the extra channels are masked by the descriptor's shape bound.
40///
41/// BN parameters must be precomputed by the caller as:
42/// bn_scale[c] = gamma[c] / sqrt(var[c] + eps)
43/// bn_shift[c] = beta[c] - bn_scale[c] * mean[c]
44///
45/// Grid: pid = ((b * OH + oh) * num_n_tiles + n_tile) * num_ow_tiles + ow_tile
46///
47/// Inference-only; no backward pass.
48#[kernel]
49pub fn conv2d_bn_silu_tiled_forward<
50 T: Triton,
51 const KH: i32,
52 const KW: i32,
53 const STRIDE_H: i32,
54 const STRIDE_W: i32,
55 const PAD_H: i32,
56 const PAD_W: i32,
57 const BLOCK_OW: i32,
58 const BLOCK_N: i32,
59>(
60 x_ptr: T::Pointer<f32>,
61 w_ptr: T::Pointer<f32>,
62 bn_scale_ptr: T::Pointer<f32>,
63 bn_shift_ptr: T::Pointer<f32>,
64 y_ptr: T::Pointer<f32>,
65 B: i32,
66 C_IN: i32,
67 C_OUT: i32,
68 H: i32,
69 W: i32,
70 OH: i32,
71 OW: i32,
72 // y_col_stride: allocated column width per oh row.
73 // Must satisfy: y_col_stride >= max(OW, BLOCK_OW) AND divisible by 4.
74 // Ensures TMA store positions oh*y_col_stride are always 16-byte aligned,
75 // and adjacent oh tiles never overlap (y_col_stride >= BLOCK_OW).
76 // Caller allocates B * C_OUT * OH * y_col_stride floats for y.
77 y_col_stride: i32,
78) where
79 T::I32Tensor: Tensor<i32, 1>,
80 T::I32Tensor: Comparison<i32, BoolTensor = T::BoolTensor>,
81 T::BoolTensor: BitAnd<Output = T::BoolTensor>,
82 T::Pointer<f32>: AddOffsets<i32, 1, T::I32Tensor, Output = T::Tensor<T::Pointer<f32>>>,
83{
84 let pid = T::program_id(Axis::X);
85 let num_ow_tiles = T::cdiv(OW, BLOCK_OW);
86 let num_n_tiles = T::cdiv(C_OUT, BLOCK_N);
87
88 // Decode flat pid → (b, oh, n_tile, ow_tile).
89 let ow_tile = pid % num_ow_tiles;
90 let tmp = pid / num_ow_tiles;
91 let n_tile = tmp % num_n_tiles;
92 let tmp2 = tmp / num_n_tiles;
93 let oh = tmp2 % OH;
94 let b = tmp2 / OH;
95
96 let ow_start = ow_tile * BLOCK_OW;
97 let c_out_start = n_tile * BLOCK_N;
98
99 // 1-D lane ranges
100 let ow_range = T::arange(0, BLOCK_OW) + ow_start; // [BLOCK_OW]
101 let c_out_range = T::arange(0, BLOCK_N) + c_out_start; // [BLOCK_N]
102
103 let ow_mask = ow_range.lt(OW); // [BLOCK_OW] bool
104 let n_mask = c_out_range.lt(C_OUT); // [BLOCK_N] bool
105
106 // 2-D accumulator [BLOCK_N, BLOCK_OW] (channel dim first — matches NCHW output).
107 let mut acc = T::zeros::<f32>(&[BLOCK_N, BLOCK_OW]);
108
109 // G=1 is enforced by dispatch — all c_out in this tile use the same c_in range.
110 let loop_bound = C_IN * KH * KW;
111 for idx in 0..loop_bound {
112 let kw = idx % KW;
113 let kh_cin = idx / KW;
114 let kh = kh_cin % KH;
115 let c_in_local = kh_cin / KH;
116
117 let ih = oh * STRIDE_H + kh - PAD_H;
118 let iw_range = ow_range * STRIDE_W + kw - PAD_W;
119
120 // Broadcast ih to [BLOCK_OW] for comparison.
121 #[allow(clippy::erasing_op)]
122 let ih_t = ow_range * 0 + ih;
123 let h_in_bounds = ih_t.ge(0) & ih_t.lt(H);
124 let w_in_bounds = iw_range.ge(0) & iw_range.lt(W);
125 let x_load_mask = ow_mask & h_in_bounds & w_in_bounds;
126
127 // ── x_col [BLOCK_OW]: one spatial slice (same for all output channels) ─
128 let x_offsets = iw_range + ((b * C_IN + c_in_local) * H * W + ih * W);
129 let x_col = T::load(
130 x_ptr.add_offsets(x_offsets),
131 Some(x_load_mask),
132 Some(T::zeros::<f32>(&[BLOCK_OW])),
133 &[],
134 None,
135 None,
136 None,
137 false,
138 );
139
140 // ── w_row [BLOCK_N]: weights for BLOCK_N output channels at this k ─────
141 // Weight layout [C_OUT, C_IN, KH, KW] (groups=1):
142 // w[c_out, c_in_local, kh, kw] = w_flat[c_out*(C_IN*KH*KW) + (c_in_local*KH+kh)*KW + kw]
143 let k_scalar = (c_in_local * KH + kh) * KW + kw;
144 let w_offsets = c_out_range * (C_IN * KH * KW) + k_scalar;
145 let w_row = T::load(
146 w_ptr.add_offsets(w_offsets),
147 Some(n_mask),
148 Some(T::zeros::<f32>(&[BLOCK_N])),
149 &[],
150 None,
151 None,
152 None,
153 false,
154 );
155
156 // ── Outer product: w_row[:,None] * x_col[None,:] → [BLOCK_N, BLOCK_OW] ─
157 let w_2d = T::broadcast_to(T::expand_dims(w_row, 1), &[BLOCK_N, BLOCK_OW]);
158 let x_2d = T::broadcast_to(T::expand_dims(x_col, 0), &[BLOCK_N, BLOCK_OW]);
159 acc = acc + w_2d * x_2d;
160 }
161
162 // ── BatchNorm epilog ──────────────────────────────────────────────────────
163 let bn_scale = T::load(
164 bn_scale_ptr.add_offsets(c_out_range),
165 Some(n_mask),
166 Some(T::zeros::<f32>(&[BLOCK_N])),
167 &[],
168 None,
169 None,
170 None,
171 false,
172 );
173 let bn_shift = T::load(
174 bn_shift_ptr.add_offsets(c_out_range),
175 Some(n_mask),
176 Some(T::zeros::<f32>(&[BLOCK_N])),
177 &[],
178 None,
179 None,
180 None,
181 false,
182 );
183 let scale_2d = T::broadcast_to(T::expand_dims(bn_scale, 1), &[BLOCK_N, BLOCK_OW]);
184 let shift_2d = T::broadcast_to(T::expand_dims(bn_shift, 1), &[BLOCK_N, BLOCK_OW]);
185 let bn_out = scale_2d * acc + shift_2d;
186
187 // ── SiLU epilog: y = x * sigmoid(x) ─────────────────────────────────────
188 let y = bn_out * T::sigmoid(bn_out);
189
190 // ── Store via TMA descriptor ──────────────────────────────────────────────
191 // Output layout [B*C_OUT, OH * y_col_stride]:
192 // y[b, c_out, oh, ow] = y_flat[(b*C_OUT + c_out) * OH * y_col_stride
193 // + oh * y_col_stride + ow]
194 //
195 // y_col_stride >= BLOCK_OW ensures adjacent oh tiles never overlap.
196 // y_col_stride divisible by 4 ensures store positions oh*y_col_stride are
197 // 16-byte (4-float) aligned for the v2.b32 store the compiler generates.
198 let oh_ycs = OH * y_col_stride;
199 let y_desc = T::make_tensor_descriptor(
200 y_ptr,
201 &[B * C_OUT, oh_ycs],
202 &[oh_ycs, 1],
203 &[BLOCK_N, BLOCK_OW],
204 Some(PaddingOption::Zero),
205 );
206 T::store_tensor_descriptor(
207 y_desc,
208 &[b * C_OUT + c_out_start, oh * y_col_stride + ow_start],
209 y,
210 );
211}
212
213// ── RuntimeOp ────────────────────────────────────────────────────────────────
214//
215// Params layout: [weight [C_OUT, C_IN, KH, KW], bn_scale [C_OUT], bn_shift [C_OUT]]
216// pack_args order: x_ptr, w_ptr, bn_scale_ptr, bn_shift_ptr, y_ptr,
217// B, C_IN, C_OUT, H, W, OH, OW, y_col_stride
218
219impl teeny_core::model::RuntimeOp for Conv2dBnSiluTiledForward {
220 fn n_activation_inputs(&self) -> usize {
221 1
222 }
223
224 fn param_shapes(&self, input_shapes: &[&[usize]], output_shape: &[usize]) -> Vec<Vec<usize>> {
225 let c_in = input_shapes[0][1];
226 let c_out = output_shape[1];
227 vec![
228 vec![c_out, c_in, self.kh as usize, self.kw as usize],
229 vec![c_out],
230 vec![c_out],
231 ]
232 }
233
234 fn param_names(&self) -> &'static [&'static str] {
235 &["weight", "bn_scale", "bn_shift"]
236 }
237
238 // y_col_stride is the padded column width of each OH row in the output buffer.
239 // Must be a multiple of BLOCK_OW to guarantee that the BLOCK_OW-wide TMA store
240 // tile for the last OW tile never crosses into the next oh row's data.
241 //
242 // Example: OW=40, BLOCK_OW=16 → 3 tiles (ow_start = 0, 16, 32).
243 // Tile 2 stores [BLOCK_OW=16] columns at oh*stride+32..oh*stride+47.
244 // With stride=40: oh*40+40 = (oh+1)*40, so the last 8 columns overwrite
245 // (oh+1)'s first 8 positions. With stride=48 (next multiple of 16):
246 // oh*48+47 < oh*48+48 = (oh+1)*48 — fully within oh's padding. ✓
247 //
248 // The runtime row-stride contract expects the stride between adjacent
249 // last-dimension rows (each of natural_stride = OW elements), which is
250 // exactly y_col_stride — NOT OH * y_col_stride.
251 fn forward_output_row_stride(&self, output_shape: &[usize]) -> usize {
252 let ow = output_shape[3];
253 ow.next_multiple_of(self.block_ow as usize)
254 }
255
256 fn pack_args(
257 &self,
258 inputs: &[(teeny_core::model::RawPtr, &[usize])],
259 params: &[teeny_core::model::RawPtr],
260 output: teeny_core::model::RawPtr,
261 output_shape: &[usize],
262 output_row_stride: i32,
263 visitor: &mut dyn teeny_core::device::program::ArgVisitor,
264 ) {
265 let input_shape = inputs[0].1;
266 let oh = output_shape[2] as i32;
267 // output_row_stride == y_col_stride (the per-oh-row column stride).
268 let y_col_stride = output_row_stride;
269 visitor.visit_ptr(inputs[0].0); // x_ptr
270 visitor.visit_ptr(params[0]); // w_ptr
271 visitor.visit_ptr(params[1]); // bn_scale_ptr
272 visitor.visit_ptr(params[2]); // bn_shift_ptr
273 visitor.visit_ptr(output); // y_ptr
274 visitor.visit_i32(input_shape[0] as i32); // B
275 visitor.visit_i32(input_shape[1] as i32); // C_IN
276 visitor.visit_i32(output_shape[1] as i32); // C_OUT
277 visitor.visit_i32(input_shape[2] as i32); // H
278 visitor.visit_i32(input_shape[3] as i32); // W
279 visitor.visit_i32(oh); // OH
280 visitor.visit_i32(output_shape[3] as i32); // OW
281 visitor.visit_i32(y_col_stride); // y_col_stride
282 }
283
284 fn block(&self) -> [u32; 3] {
285 [128, 1, 1]
286 }
287
288 fn grid(&self, output_shape: &[usize]) -> [u32; 3] {
289 let num_ow_tiles = output_shape[3].div_ceil(self.block_ow as usize);
290 let num_n_tiles = output_shape[1].div_ceil(self.block_n as usize);
291 [
292 (output_shape[0] * output_shape[2] * num_n_tiles * num_ow_tiles) as u32,
293 1,
294 1,
295 ]
296 }
297}