Skip to main content

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}