teeny_core/model/mod.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
17use alloc::{boxed::Box, string::String, sync::Arc, vec, vec::Vec};
18
19use crate::{
20 device::program::ArgVisitor,
21 errors::Result,
22 graph::{DtypeRepr, Graph, Shape},
23 utils::dag::Dag,
24};
25
26/// A node index within a compiled model's DAG.
27pub type NodeId = usize;
28/// Raw device pointer alias used by runtime arg-packing.
29pub type RawPtr = *mut core::ffi::c_void;
30
31/// A concrete, dtype-resolved kernel produced by a `#[kernel(dtypes = [...])]`
32/// dispatcher (or a `kernel_group!`).
33///
34/// It is deliberately crate-agnostic: it carries only the pieces needed to
35/// assemble a compilable unit (`teeny-kernels`' `KernelExecutable`), while
36/// living in `teeny-core` so the `#[kernel]` macro can reference it from any
37/// consuming crate without a dependency on `teeny-kernels`.
38pub struct KernelInstance {
39 /// Forward kernel name (used to derive the entry-point symbol).
40 pub name: String,
41 /// Combined forward kernel source (`kernel_source + entry_point`).
42 pub source: String,
43 /// Runtime dispatch object for arg-packing and launch config.
44 pub runtime_op: Arc<dyn RuntimeOp>,
45 /// Backward kernel, present only when the kernel declares one.
46 pub backward: Option<KernelInstanceBackward>,
47}
48
49/// The backward half of a [`KernelInstance`], when a paired backward kernel is
50/// declared via `#[kernel(..., backward = ...)]`.
51pub struct KernelInstanceBackward {
52 /// Backward kernel name (used to derive the entry-point symbol).
53 pub name: String,
54 /// Combined backward kernel source.
55 pub source: String,
56}
57
58/// A runtime execution context (device/stream handles, etc), threaded through arg-packing.
59pub trait RuntimeContext<'a> {}
60
61/// Encapsulates the runtime-dispatch behaviour for a compiled op node:
62/// how many activation inputs it takes, what parameter buffers it needs,
63/// how to pack kernel arguments, and how to compute the launch grid.
64///
65/// Implementations live in `teeny-kernels` alongside the kernel structs so that
66/// each kernel owns its arg layout. The trait is defined here in `teeny-core`
67/// so that both `teeny-kernels` (impl) and `teeny-cuda` (consumer) can share it
68/// without a circular dependency.
69pub trait RuntimeOp: Send + Sync {
70 /// Number of activation tensors taken from predecessor DAG nodes.
71 fn n_activation_inputs(&self) -> usize;
72
73 /// Shapes of additional parameter buffers (weights, biases) needed by this
74 /// op. Called at `LoadedModel::load()` time to pre-allocate device buffers.
75 /// `input_shapes` / `output_shape` are concrete (batch dim resolved).
76 fn param_shapes(&self, input_shapes: &[&[usize]], output_shape: &[usize]) -> Vec<Vec<usize>>;
77
78 /// Names of parameter slots returned by [`param_shapes`], in the same order.
79 /// Used as the suffix in the dotted key `{node_name}.{slot_name}`.
80 /// Return an empty slice for ops that have no named parameters.
81 fn param_names(&self) -> &'static [&'static str] {
82 &[]
83 }
84
85 /// Returns the required row stride (in elements) for the output buffer of
86 /// this op's forward kernel. The default is the natural row-major stride
87 /// ([`forward_output_row_elems`]). Kernels using TMA must round up so a
88 /// full store tile never straddles the next row (e.g. `M` padded to a
89 /// multiple of `BLOCK_M` for GEMM, or `OW` to a multiple of `BLOCK_OW`
90 /// for tiled conv).
91 fn forward_output_row_stride(&self, output_shape: &[usize]) -> usize {
92 self.forward_output_row_elems(output_shape)
93 }
94
95 /// Natural (unpadded) length of one output row, in elements.
96 ///
97 /// Used with [`forward_output_row_stride`] to size the TMA padded buffer:
98 /// `n_rows = numel / row_elems`, allocate `n_rows * row_stride`.
99 /// Default: last dimension of `output_shape`. Ops that view the tensor as
100 /// a different 2-D layout (e.g. NCHW → `[C, OH*OW]`) should override.
101 fn forward_output_row_elems(&self, output_shape: &[usize]) -> usize {
102 output_shape.last().copied().unwrap_or(1)
103 }
104
105 /// Returns raw (little-endian) bytes to pre-populate parameter slot `param_idx`
106 /// immediately after device buffer allocation. Return `None` to leave the
107 /// slot zero-initialised (the default for trained parameters).
108 /// Byte count must equal `param_shapes()[param_idx].iter().product() * dtype_bytes`.
109 fn param_init_data(&self, _param_idx: usize) -> Option<Vec<u8>> {
110 None
111 }
112
113 /// Override to compute the true concrete output shape from concrete input shapes.
114 ///
115 /// Called during both `load()` (for param allocation) and `forward()` (for buffer
116 /// allocation). The default just returns the `resolved` shape (from `resolve_shape`).
117 ///
118 /// Override this when the output's first dimension is a multiple of the batch size
119 /// (e.g. `B * H` for attention pack/unpack ops) so that `resolve_shape`'s simple
120 /// `None → batch_size` substitution would under-allocate the buffer.
121 fn compute_concrete_output_shape(
122 &self,
123 _input_shapes: &[&[usize]],
124 resolved: &[usize],
125 ) -> Vec<usize> {
126 resolved.to_vec()
127 }
128
129 /// Pack all kernel arguments into `visitor` in the correct order.
130 /// - `inputs` — (ptr, concrete_shape) per activation input
131 /// - `params` — raw pointers to pre-allocated param buffers
132 /// - `output` — raw pointer to the output buffer for this node
133 /// - `output_shape` — concrete output shape (batch dim resolved)
134 /// - `output_row_stride` — actual memory row stride (elements) of the
135 /// output buffer (may be padded for TMA alignment)
136 fn pack_args(
137 &self,
138 inputs: &[(RawPtr, &[usize])],
139 params: &[RawPtr],
140 output: RawPtr,
141 output_shape: &[usize],
142 output_row_stride: i32,
143 visitor: &mut dyn ArgVisitor,
144 );
145
146 /// Threads-per-CTA for this kernel (x, y, z).
147 fn block(&self) -> [u32; 3];
148
149 /// Number of CTAs to launch (x, y, z), given the concrete output shape.
150 fn grid(&self, output_shape: &[usize]) -> [u32; 3];
151
152 /// Number of sequential kernel launches this op requires.
153 ///
154 /// Ops like channel-cat scatter N input chunks into one output buffer and
155 /// need one kernel call per chunk. The executor loops `n_launches()` times,
156 /// calling `pack_args_for_launch` and `grid_for_launch` on each iteration.
157 /// The default is 1, which delegates to `pack_args` / `grid`.
158 fn n_launches(&self) -> usize {
159 1
160 }
161
162 /// Pack kernel arguments for launch `i` (0-indexed).
163 ///
164 /// Only called by the executor when `n_launches() > 1`. The default
165 /// delegates to `pack_args`, ignoring `launch_idx`.
166 // Kernel-launch argument list; each parameter corresponds to a distinct piece of the
167 // launch ABI (input/param/output buffers, shape, stride, launch index) and bundling them
168 // into a struct wouldn't be clearer at call sites.
169 #[allow(clippy::too_many_arguments)]
170 fn pack_args_for_launch(
171 &self,
172 launch_idx: usize,
173 inputs: &[(RawPtr, &[usize])],
174 params: &[RawPtr],
175 output: RawPtr,
176 output_shape: &[usize],
177 output_row_stride: i32,
178 visitor: &mut dyn ArgVisitor,
179 ) {
180 let _ = launch_idx;
181 self.pack_args(
182 inputs,
183 params,
184 output,
185 output_shape,
186 output_row_stride,
187 visitor,
188 );
189 }
190
191 /// Grid for launch `i`. Receives concrete input shapes so that per-chunk
192 /// grids can be computed without storing them in the op.
193 ///
194 /// Only called when `n_launches() > 1`. The default delegates to `grid`.
195 fn grid_for_launch(
196 &self,
197 launch_idx: usize,
198 input_shapes: &[&[usize]],
199 output_shape: &[usize],
200 ) -> [u32; 3] {
201 let _ = (launch_idx, input_shapes);
202 self.grid(output_shape)
203 }
204
205 /// Returns true if this op has a backward (gradient) kernel.
206 #[cfg(feature = "training")]
207 fn has_backward(&self) -> bool {
208 false
209 }
210
211 /// Returns the required row stride (in elements) for the grad_output buffer
212 /// passed to `pack_backward_args`. The default is the natural row-major
213 /// stride (`output_shape[-1]`). Kernels using TMA must round up to satisfy
214 /// the 16-byte alignment constraint (e.g. 4 elements for f32).
215 #[cfg(feature = "training")]
216 fn backward_grad_output_row_stride(&self, output_shape: &[usize]) -> usize {
217 output_shape.last().copied().unwrap_or(1)
218 }
219
220 /// Pack backward kernel arguments.
221 ///
222 /// - `inputs` — (ptr, shape) per forward activation input (from cache)
223 /// - `params` — raw ptrs to forward param buffers (weights, biases)
224 /// - `output` — forward output buffer (activation cache)
225 /// - `output_shape` — concrete forward output shape
226 /// - `grad_output` — incoming gradient dL/dy from the consumer node
227 /// - `grad_output_row_stride` — actual memory row stride (elements) of the
228 /// grad_output buffer (may be padded for TMA alignment)
229 /// - `grad_inputs` — output gradient buffers: dL/dx per activation parent
230 /// - `grad_params` — output gradient buffers: dL/dw, dL/db, etc.
231 #[cfg(feature = "training")]
232 #[allow(clippy::too_many_arguments)]
233 fn pack_backward_args(
234 &self,
235 inputs: &[(RawPtr, &[usize])],
236 params: &[RawPtr],
237 output: RawPtr,
238 output_shape: &[usize],
239 grad_output: RawPtr,
240 grad_output_row_stride: i32,
241 grad_inputs: &[RawPtr],
242 grad_params: &[RawPtr],
243 visitor: &mut dyn ArgVisitor,
244 ) {
245 let _ = (
246 inputs,
247 params,
248 output,
249 output_shape,
250 grad_output,
251 grad_output_row_stride,
252 grad_inputs,
253 grad_params,
254 visitor,
255 );
256 }
257
258 /// Threads-per-CTA for the backward kernel.
259 #[cfg(feature = "training")]
260 fn backward_block(&self) -> [u32; 3] {
261 [128, 1, 1]
262 }
263
264 /// Number of CTAs for the backward kernel.
265 ///
266 /// `input_shapes[i]` is the concrete shape of the i-th activation input.
267 #[cfg(feature = "training")]
268 fn backward_grid(&self, input_shapes: &[&[usize]], output_shape: &[usize]) -> [u32; 3] {
269 let _ = (input_shapes, output_shape);
270 [0, 0, 0]
271 }
272
273 /// Number of sequential kernel launches for the backward pass.
274 ///
275 /// For ops like channel-cat where the backward must write separate gradient
276 /// buffers per input chunk, this should return `n_inputs`. Default is 1.
277 #[cfg(feature = "training")]
278 fn n_backward_launches(&self) -> usize {
279 1
280 }
281
282 /// Pack backward kernel arguments for launch `i` (0-indexed).
283 ///
284 /// Only called when `n_backward_launches() > 1`. The default delegates to
285 /// `pack_backward_args`, ignoring `launch_idx`.
286 #[cfg(feature = "training")]
287 #[allow(clippy::too_many_arguments)]
288 fn pack_backward_args_for_launch(
289 &self,
290 launch_idx: usize,
291 inputs: &[(RawPtr, &[usize])],
292 params: &[RawPtr],
293 output: RawPtr,
294 output_shape: &[usize],
295 grad_output: RawPtr,
296 grad_output_row_stride: i32,
297 grad_inputs: &[RawPtr],
298 grad_params: &[RawPtr],
299 visitor: &mut dyn ArgVisitor,
300 ) {
301 let _ = launch_idx;
302 self.pack_backward_args(
303 inputs,
304 params,
305 output,
306 output_shape,
307 grad_output,
308 grad_output_row_stride,
309 grad_inputs,
310 grad_params,
311 visitor,
312 );
313 }
314
315 /// Grid for backward launch `i`.
316 ///
317 /// Only called when `n_backward_launches() > 1`. The default delegates to
318 /// `backward_grid`.
319 #[cfg(feature = "training")]
320 fn backward_grid_for_launch(
321 &self,
322 launch_idx: usize,
323 input_shapes: &[&[usize]],
324 output_shape: &[usize],
325 ) -> [u32; 3] {
326 let _ = launch_idx;
327 self.backward_grid(input_shapes, output_shape)
328 }
329}
330
331/// An op that has been lowered to a compilable kernel representation.
332///
333/// Holds enough information for a caller (who has access to `teeny-compiler`)
334/// to compile the kernel for a given target. Dispatch/execution is deferred.
335pub trait ExecutableOp {
336 /// This op's name.
337 fn name(&self) -> &str;
338 /// Returns `true` for `Input` placeholder nodes, which carry no kernel.
339 fn is_input(&self) -> bool {
340 false
341 }
342 /// This op's forward kernel source.
343 fn forward_kernel_source(&self) -> &str;
344 /// This op's forward kernel entry-point symbol name.
345 fn forward_kernel_entry_point(&self) -> &str;
346 /// This op's output shape.
347 fn output_shape(&self) -> &Shape;
348 /// This op's output dtype.
349 fn output_dtype(&self) -> DtypeRepr;
350 /// Returns the runtime dispatch object for this op, or `None` for Input nodes.
351 fn runtime_op(&self) -> Option<Arc<dyn RuntimeOp>> {
352 None
353 }
354
355 /// Returns the backward kernel source, or `""` if no backward is available.
356 #[cfg(feature = "training")]
357 fn backward_kernel_source(&self) -> &str {
358 ""
359 }
360
361 /// Returns the backward kernel entry point name.
362 #[cfg(feature = "training")]
363 fn backward_kernel_entry_point(&self) -> &str {
364 "entry_point"
365 }
366}
367
368/// Whether a graph is being lowered for inference or training (training lowerings additionally
369/// wire up backward kernels).
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
371pub enum LoweringMode {
372 /// Inference only; no backward kernels.
373 #[default]
374 Inference,
375 /// Training; backward kernels are wired up.
376 Training,
377}
378
379/// Lowers a [`Graph`] into a DAG of [`ExecutableOp`]s, ready to compile/execute.
380pub trait Lowering<'a> {
381 /// Lowers `graph` for `mode`.
382 fn lower(&self, graph: &Graph, mode: LoweringMode) -> Result<Dag<Box<dyn ExecutableOp>>>;
383
384 /// Like [`lower`] but also returns a `graph_node_idx → dag_node_idx` mapping
385 /// so that graph-level metadata (e.g. names) can be propagated into the compiled DAG.
386 ///
387 /// The default implementation assumes a 1-to-1 identity mapping between graph
388 /// topological order and DAG node indices — valid for graphs that are already in
389 /// topological order (which is always true for models built by sequential recording)
390 /// and lowerings that do not reorder or split nodes. Override this method in
391 /// lowerings that reorder or split nodes to return the correct mapping.
392 #[allow(clippy::type_complexity)]
393 fn lower_with_mapping(
394 &self,
395 graph: &Graph,
396 mode: LoweringMode,
397 ) -> Result<(Dag<Box<dyn ExecutableOp>>, Vec<usize>)> {
398 let dag = self.lower(graph, mode)?;
399 let topo = graph.topological_sort();
400 // topo[dag_idx] = graph_node_idx → graph_to_dag[graph_node_idx] = dag_idx
401 let mut graph_to_dag = vec![0usize; graph.nodes.len()];
402 for (dag_idx, graph_idx) in topo.into_iter().enumerate() {
403 graph_to_dag[graph_idx] = dag_idx;
404 }
405 Ok((dag, graph_to_dag))
406 }
407
408 /// Returns extra (dag_idx, name) pairs beyond those derivable from the
409 /// 1-to-1 graph→dag mapping. Used for lowerings that split one graph node
410 /// into multiple DAG nodes (e.g. Conv2d-with-bias → Conv2d + NchwBiasAdd);
411 /// the "extra" DAG nodes would otherwise have no name and their weight
412 /// parameters would not be loaded.
413 fn extra_dag_names(&self, _graph: &Graph, _graph_to_dag: &[usize]) -> Vec<(usize, String)> {
414 Vec::new()
415 }
416
417 /// Returns the next lowering in a middleware chain, or `None` if this is
418 /// the final lowering. A custom lowering can call `self.base_lowering()`
419 /// to delegate ops it does not handle.
420 fn base_lowering(&self) -> Option<&dyn Lowering<'a>> {
421 None
422 }
423}
424
425/// A compiled, runnable model.
426pub trait Model<'a> {
427 /// This model's input type.
428 type Input;
429 /// This model's output type.
430 type Output;
431
432 /// Runs inference, producing `Output` from `input`.
433 fn forward(&self, input: Self::Input) -> Result<Self::Output>;
434}