Skip to main content

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}