Skip to main content

teeny_kernels/nn/fused/
conv2d_bn_silu_gemm.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(1×1, stride=1, pad=0, groups=1) + BatchNorm2d + SiLU using GEMM.
28///
29/// A 1×1 stride=1 no-padding convolution is mathematically equivalent to:
30///   Y[N, M] = W[N, K] @ X[K, M]
31/// where:
32///   N = C_OUT, K = C_IN, M = OH * OW  (batch handled in grid)
33///
34/// Input X is stored NCHW = [B, K, OH, OW], viewed as 2-D [K, OH*OW] per batch
35/// (column-major spatial: stride_K = OH*OW, stride_spatial = 1).
36///
37/// Weight W is [C_OUT, C_IN] row-major.
38///
39/// Output Y is viewed as [B*C_OUT, y_row_stride] where `y_row_stride >= M` and
40/// `y_row_stride` is a multiple of `BLOCK_M`. TMA stores a full `[BLOCK_N,
41/// BLOCK_M]` tile; without that padding the last M-tile writes past `M` into
42/// the next channel (silent overwrite). Runtime allocates the padded buffer
43/// and depads back to tight NCHW.
44///
45/// T::dot uses TF32 Tensor Cores on sm_87+ (Jetson Orin) for ~8× throughput
46/// vs direct scalar accumulation.
47///
48/// Restrictions (enforced by dispatch in graph/mod.rs):
49///   - kernel_h == 1, kernel_w == 1
50///   - stride_h == 1, stride_w == 1
51///   - padding_h == 0, padding_w == 0
52///   - groups == 1
53///
54/// BN parameters must be precomputed (same convention as conv2d_bn_silu_forward).
55///
56/// Grid: pid = b * num_pid_per_batch + group_id * (GROUP_M * num_pid_n)
57///             + pid_in_group  (same L2-locality grouping as Triton's matmul tutorial)
58///
59/// Inference-only; no backward pass.
60#[kernel]
61pub fn conv2d_bn_silu_gemm_forward<
62    T: Triton,
63    const BLOCK_M: i32,
64    const BLOCK_N: i32,
65    const BLOCK_K: i32,
66    const GROUP_M: i32,
67>(
68    x_ptr: T::Pointer<f32>,
69    w_ptr: T::Pointer<f32>,
70    bn_scale_ptr: T::Pointer<f32>,
71    bn_shift_ptr: T::Pointer<f32>,
72    y_ptr: T::Pointer<f32>,
73    B: i32,
74    C_IN: i32,
75    C_OUT: i32,
76    M: i32, // OH * OW per batch (tight spatial extent)
77    // y_row_stride: allocated column width per (b, c_out) row.
78    //   Must satisfy: y_row_stride >= M AND divisible by BLOCK_M.
79    //   Prevents the last M-tile's TMA store from spilling into the next channel.
80    y_row_stride: i32,
81) where
82    T::I32Tensor: Tensor<i32, 1>,
83    T::I32Tensor: Comparison<i32, BoolTensor = T::BoolTensor>,
84    T::BoolTensor: BitAnd<Output = T::BoolTensor>,
85    T::Pointer<f32>: AddOffsets<i32, 1, T::I32Tensor, Output = T::Tensor<T::Pointer<f32>>>,
86{
87    let pid = T::program_id(Axis::X);
88
89    let num_pid_m = T::cdiv(M, BLOCK_M);
90    let num_pid_n = T::cdiv(C_OUT, BLOCK_N);
91    let pids_per_batch = num_pid_m * num_pid_n;
92
93    // Decode batch dimension from pid
94    let b = pid / pids_per_batch;
95    let pid_local = pid % pids_per_batch;
96
97    // L2-locality grouping (same as Triton matmul tutorial)
98    let num_pid_in_group = GROUP_M * num_pid_n;
99    let group_id = pid_local / num_pid_in_group;
100    let first_pid_m = group_id * GROUP_M;
101    let remaining_m = num_pid_m - first_pid_m;
102    let group_size_m = if remaining_m < GROUP_M {
103        remaining_m
104    } else {
105        GROUP_M
106    };
107    let pid_in_group = pid_local % num_pid_in_group;
108    let pid_m = first_pid_m + (pid_in_group % group_size_m);
109    let pid_n = pid_in_group / group_size_m;
110
111    // ── Input descriptor: X viewed as [B*C_IN, M] ────────────────────────────
112    // NCHW layout: x[b, c_in, oh, ow] = x_flat[b*(C_IN*M) + c_in*M + oh*OW + ow]
113    // For batch b: the C_IN rows for that batch start at row b*C_IN.
114    // stride along K (row) = M (spatial extent per channel)
115    // stride along M (col) = 1
116    let x_desc = T::make_tensor_descriptor(
117        x_ptr,
118        &[B * C_IN, M],
119        &[M, 1],
120        &[BLOCK_K, BLOCK_M],
121        Some(PaddingOption::Zero),
122    );
123
124    // ── Weight descriptor: W [C_OUT, C_IN] row-major ─────────────────────────
125    let w_desc = T::make_tensor_descriptor(
126        w_ptr,
127        &[C_OUT, C_IN],
128        &[C_IN, 1],
129        &[BLOCK_N, BLOCK_K],
130        Some(PaddingOption::Zero),
131    );
132
133    // ── GEMM: acc [BLOCK_N, BLOCK_M] = sum_k W_tile @ X_tile ─────────────────
134    let mut acc = T::zeros::<f32>(&[BLOCK_N, BLOCK_M]);
135    let k_tiles = T::cdiv(C_IN, BLOCK_K);
136    for k in 0..k_tiles {
137        // x_tile: [BLOCK_K, BLOCK_M] — rows are the C_IN channels for batch b
138        let x_tile = T::load_tensor_descriptor(x_desc, &[b * C_IN + k * BLOCK_K, pid_m * BLOCK_M]);
139
140        // w_tile: [BLOCK_N, BLOCK_K]
141        let w_tile = T::load_tensor_descriptor(w_desc, &[pid_n * BLOCK_N, k * BLOCK_K]);
142
143        // [BLOCK_N, BLOCK_K] @ [BLOCK_K, BLOCK_M] → [BLOCK_N, BLOCK_M]
144        // TF32 precision is what actually routes this dot to the tensor-core MMA
145        // path (see getMmaTypeDot in Triton's MMAv2.cpp) — IEEE forces the
146        // software FMA fallback and silently disables tensor cores entirely.
147        acc = T::dot::<f32, f32>(w_tile, x_tile, Some(acc), InputPrecision::TF32, None);
148    }
149
150    // ── BatchNorm epilog ──────────────────────────────────────────────────────
151    let bn_off = T::arange(0, BLOCK_N) + pid_n * BLOCK_N;
152    let bn_n_mask = bn_off.lt(C_OUT);
153    let bn_scale = T::load(
154        bn_scale_ptr.add_offsets(bn_off),
155        Some(bn_n_mask),
156        Some(T::zeros::<f32>(&[BLOCK_N])),
157        &[],
158        None,
159        None,
160        None,
161        false,
162    );
163    let bn_shift = T::load(
164        bn_shift_ptr.add_offsets(bn_off),
165        Some(bn_n_mask),
166        Some(T::zeros::<f32>(&[BLOCK_N])),
167        &[],
168        None,
169        None,
170        None,
171        false,
172    );
173    let scale_2d = T::broadcast_to(T::expand_dims(bn_scale, 1), &[BLOCK_N, BLOCK_M]);
174    let shift_2d = T::broadcast_to(T::expand_dims(bn_shift, 1), &[BLOCK_N, BLOCK_M]);
175    let bn_out = scale_2d * acc + shift_2d;
176
177    // ── SiLU epilog: y = x * sigmoid(x) ─────────────────────────────────────
178    let y = bn_out * T::sigmoid(bn_out);
179
180    // ── Store: y NCHW [B, C_OUT, OH, OW] viewed as [B*C_OUT, y_row_stride] ───
181    // Valid columns are [0, M); columns [M, y_row_stride) are padding so the
182    // last BLOCK_M-wide TMA store cannot spill into the next channel's row.
183    let y_desc = T::make_tensor_descriptor(
184        y_ptr,
185        &[B * C_OUT, y_row_stride],
186        &[y_row_stride, 1],
187        &[BLOCK_N, BLOCK_M],
188        Some(PaddingOption::Zero),
189    );
190    T::store_tensor_descriptor(y_desc, &[b * C_OUT + pid_n * BLOCK_N, pid_m * BLOCK_M], y);
191}
192
193// ── RuntimeOp ────────────────────────────────────────────────────────────────
194//
195// Params layout: [weight [C_OUT, C_IN], bn_scale [C_OUT], bn_shift [C_OUT]]
196// pack_args order: x_ptr, w_ptr, bn_scale_ptr, bn_shift_ptr, y_ptr,
197//                  B, C_IN, C_OUT, M (= OH * OW), y_row_stride
198
199impl teeny_core::model::RuntimeOp for Conv2dBnSiluGemmForward {
200    fn n_activation_inputs(&self) -> usize {
201        1
202    }
203
204    fn param_shapes(&self, input_shapes: &[&[usize]], output_shape: &[usize]) -> Vec<Vec<usize>> {
205        let c_in = input_shapes[0][1];
206        let c_out = output_shape[1];
207        // 1×1 weight: [C_OUT, C_IN] (no KH/KW dims needed — both are 1)
208        vec![vec![c_out, c_in], vec![c_out], vec![c_out]]
209    }
210
211    fn param_names(&self) -> &'static [&'static str] {
212        &["weight", "bn_scale", "bn_shift"]
213    }
214
215    // Row = one (b, c_out) channel flattened over OH*OW. Pad to a multiple of
216    // BLOCK_M so the last TMA store tile stays inside the channel's row.
217    fn forward_output_row_elems(&self, output_shape: &[usize]) -> usize {
218        output_shape[2] * output_shape[3]
219    }
220
221    fn forward_output_row_stride(&self, output_shape: &[usize]) -> usize {
222        let m = self.forward_output_row_elems(output_shape);
223        m.next_multiple_of(self.block_m as usize)
224    }
225
226    fn pack_args(
227        &self,
228        inputs: &[(teeny_core::model::RawPtr, &[usize])],
229        params: &[teeny_core::model::RawPtr],
230        output: teeny_core::model::RawPtr,
231        output_shape: &[usize],
232        output_row_stride: i32,
233        visitor: &mut dyn teeny_core::device::program::ArgVisitor,
234    ) {
235        let input_shape = inputs[0].1;
236        let b = input_shape[0] as i32;
237        let c_in = input_shape[1] as i32;
238        let c_out = output_shape[1] as i32;
239        let m = (output_shape[2] * output_shape[3]) as i32; // OH * OW
240        visitor.visit_ptr(inputs[0].0); // x_ptr
241        visitor.visit_ptr(params[0]); // w_ptr
242        visitor.visit_ptr(params[1]); // bn_scale_ptr
243        visitor.visit_ptr(params[2]); // bn_shift_ptr
244        visitor.visit_ptr(output); // y_ptr
245        visitor.visit_i32(b);
246        visitor.visit_i32(c_in);
247        visitor.visit_i32(c_out);
248        visitor.visit_i32(m);
249        visitor.visit_i32(output_row_stride); // y_row_stride
250    }
251
252    fn block(&self) -> [u32; 3] {
253        [128, 1, 1]
254    }
255
256    fn grid(&self, output_shape: &[usize]) -> [u32; 3] {
257        let b = output_shape[0];
258        let m = output_shape[2] * output_shape[3]; // OH * OW
259        let c_out = output_shape[1];
260        let pm = m.div_ceil(self.block_m as usize);
261        let pn = c_out.div_ceil(self.block_n as usize);
262        [(b * pm * pn) as u32, 1, 1]
263    }
264}