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    /// (`output_shape[-1]`).  Kernels using TMA must round up to satisfy the
88    /// 16-byte alignment constraint (e.g. 4 elements for f32).
89    fn forward_output_row_stride(&self, output_shape: &[usize]) -> usize {
90        output_shape.last().copied().unwrap_or(1)
91    }
92
93    /// Returns raw (little-endian) bytes to pre-populate parameter slot `param_idx`
94    /// immediately after device buffer allocation.  Return `None` to leave the
95    /// slot zero-initialised (the default for trained parameters).
96    /// Byte count must equal `param_shapes()[param_idx].iter().product() * dtype_bytes`.
97    fn param_init_data(&self, _param_idx: usize) -> Option<Vec<u8>> {
98        None
99    }
100
101    /// Override to compute the true concrete output shape from concrete input shapes.
102    ///
103    /// Called during both `load()` (for param allocation) and `forward()` (for buffer
104    /// allocation). The default just returns the `resolved` shape (from `resolve_shape`).
105    ///
106    /// Override this when the output's first dimension is a multiple of the batch size
107    /// (e.g. `B * H` for attention pack/unpack ops) so that `resolve_shape`'s simple
108    /// `None → batch_size` substitution would under-allocate the buffer.
109    fn compute_concrete_output_shape(
110        &self,
111        _input_shapes: &[&[usize]],
112        resolved: &[usize],
113    ) -> Vec<usize> {
114        resolved.to_vec()
115    }
116
117    /// Pack all kernel arguments into `visitor` in the correct order.
118    /// - `inputs`            — (ptr, concrete_shape) per activation input
119    /// - `params`            — raw pointers to pre-allocated param buffers
120    /// - `output`            — raw pointer to the output buffer for this node
121    /// - `output_shape`      — concrete output shape (batch dim resolved)
122    /// - `output_row_stride` — actual memory row stride (elements) of the
123    ///   output buffer (may be padded for TMA alignment)
124    fn pack_args(
125        &self,
126        inputs: &[(RawPtr, &[usize])],
127        params: &[RawPtr],
128        output: RawPtr,
129        output_shape: &[usize],
130        output_row_stride: i32,
131        visitor: &mut dyn ArgVisitor,
132    );
133
134    /// Threads-per-CTA for this kernel (x, y, z).
135    fn block(&self) -> [u32; 3];
136
137    /// Number of CTAs to launch (x, y, z), given the concrete output shape.
138    fn grid(&self, output_shape: &[usize]) -> [u32; 3];
139
140    /// Number of sequential kernel launches this op requires.
141    ///
142    /// Ops like channel-cat scatter N input chunks into one output buffer and
143    /// need one kernel call per chunk. The executor loops `n_launches()` times,
144    /// calling `pack_args_for_launch` and `grid_for_launch` on each iteration.
145    /// The default is 1, which delegates to `pack_args` / `grid`.
146    fn n_launches(&self) -> usize {
147        1
148    }
149
150    /// Pack kernel arguments for launch `i` (0-indexed).
151    ///
152    /// Only called by the executor when `n_launches() > 1`. The default
153    /// delegates to `pack_args`, ignoring `launch_idx`.
154    // Kernel-launch argument list; each parameter corresponds to a distinct piece of the
155    // launch ABI (input/param/output buffers, shape, stride, launch index) and bundling them
156    // into a struct wouldn't be clearer at call sites.
157    #[allow(clippy::too_many_arguments)]
158    fn pack_args_for_launch(
159        &self,
160        launch_idx: usize,
161        inputs: &[(RawPtr, &[usize])],
162        params: &[RawPtr],
163        output: RawPtr,
164        output_shape: &[usize],
165        output_row_stride: i32,
166        visitor: &mut dyn ArgVisitor,
167    ) {
168        let _ = launch_idx;
169        self.pack_args(
170            inputs,
171            params,
172            output,
173            output_shape,
174            output_row_stride,
175            visitor,
176        );
177    }
178
179    /// Grid for launch `i`. Receives concrete input shapes so that per-chunk
180    /// grids can be computed without storing them in the op.
181    ///
182    /// Only called when `n_launches() > 1`. The default delegates to `grid`.
183    fn grid_for_launch(
184        &self,
185        launch_idx: usize,
186        input_shapes: &[&[usize]],
187        output_shape: &[usize],
188    ) -> [u32; 3] {
189        let _ = (launch_idx, input_shapes);
190        self.grid(output_shape)
191    }
192
193    /// Returns true if this op has a backward (gradient) kernel.
194    #[cfg(feature = "training")]
195    fn has_backward(&self) -> bool {
196        false
197    }
198
199    /// Returns the required row stride (in elements) for the grad_output buffer
200    /// passed to `pack_backward_args`. The default is the natural row-major
201    /// stride (`output_shape[-1]`). Kernels using TMA must round up to satisfy
202    /// the 16-byte alignment constraint (e.g. 4 elements for f32).
203    #[cfg(feature = "training")]
204    fn backward_grad_output_row_stride(&self, output_shape: &[usize]) -> usize {
205        output_shape.last().copied().unwrap_or(1)
206    }
207
208    /// Pack backward kernel arguments.
209    ///
210    /// - `inputs`       — (ptr, shape) per forward activation input (from cache)
211    /// - `params`       — raw ptrs to forward param buffers (weights, biases)
212    /// - `output`       — forward output buffer (activation cache)
213    /// - `output_shape` — concrete forward output shape
214    /// - `grad_output`  — incoming gradient dL/dy from the consumer node
215    /// - `grad_output_row_stride` — actual memory row stride (elements) of the
216    ///   grad_output buffer (may be padded for TMA alignment)
217    /// - `grad_inputs`  — output gradient buffers: dL/dx per activation parent
218    /// - `grad_params`  — output gradient buffers: dL/dw, dL/db, etc.
219    #[cfg(feature = "training")]
220    #[allow(clippy::too_many_arguments)]
221    fn pack_backward_args(
222        &self,
223        inputs: &[(RawPtr, &[usize])],
224        params: &[RawPtr],
225        output: RawPtr,
226        output_shape: &[usize],
227        grad_output: RawPtr,
228        grad_output_row_stride: i32,
229        grad_inputs: &[RawPtr],
230        grad_params: &[RawPtr],
231        visitor: &mut dyn ArgVisitor,
232    ) {
233        let _ = (
234            inputs,
235            params,
236            output,
237            output_shape,
238            grad_output,
239            grad_output_row_stride,
240            grad_inputs,
241            grad_params,
242            visitor,
243        );
244    }
245
246    /// Threads-per-CTA for the backward kernel.
247    #[cfg(feature = "training")]
248    fn backward_block(&self) -> [u32; 3] {
249        [128, 1, 1]
250    }
251
252    /// Number of CTAs for the backward kernel.
253    ///
254    /// `input_shapes[i]` is the concrete shape of the i-th activation input.
255    #[cfg(feature = "training")]
256    fn backward_grid(&self, input_shapes: &[&[usize]], output_shape: &[usize]) -> [u32; 3] {
257        let _ = (input_shapes, output_shape);
258        [0, 0, 0]
259    }
260
261    /// Number of sequential kernel launches for the backward pass.
262    ///
263    /// For ops like channel-cat where the backward must write separate gradient
264    /// buffers per input chunk, this should return `n_inputs`. Default is 1.
265    #[cfg(feature = "training")]
266    fn n_backward_launches(&self) -> usize {
267        1
268    }
269
270    /// Pack backward kernel arguments for launch `i` (0-indexed).
271    ///
272    /// Only called when `n_backward_launches() > 1`. The default delegates to
273    /// `pack_backward_args`, ignoring `launch_idx`.
274    #[cfg(feature = "training")]
275    #[allow(clippy::too_many_arguments)]
276    fn pack_backward_args_for_launch(
277        &self,
278        launch_idx: usize,
279        inputs: &[(RawPtr, &[usize])],
280        params: &[RawPtr],
281        output: RawPtr,
282        output_shape: &[usize],
283        grad_output: RawPtr,
284        grad_output_row_stride: i32,
285        grad_inputs: &[RawPtr],
286        grad_params: &[RawPtr],
287        visitor: &mut dyn ArgVisitor,
288    ) {
289        let _ = launch_idx;
290        self.pack_backward_args(
291            inputs,
292            params,
293            output,
294            output_shape,
295            grad_output,
296            grad_output_row_stride,
297            grad_inputs,
298            grad_params,
299            visitor,
300        );
301    }
302
303    /// Grid for backward launch `i`.
304    ///
305    /// Only called when `n_backward_launches() > 1`. The default delegates to
306    /// `backward_grid`.
307    #[cfg(feature = "training")]
308    fn backward_grid_for_launch(
309        &self,
310        launch_idx: usize,
311        input_shapes: &[&[usize]],
312        output_shape: &[usize],
313    ) -> [u32; 3] {
314        let _ = launch_idx;
315        self.backward_grid(input_shapes, output_shape)
316    }
317}
318
319/// An op that has been lowered to a compilable kernel representation.
320///
321/// Holds enough information for a caller (who has access to `teeny-compiler`)
322/// to compile the kernel for a given target. Dispatch/execution is deferred.
323pub trait ExecutableOp {
324    /// This op's name.
325    fn name(&self) -> &str;
326    /// Returns `true` for `Input` placeholder nodes, which carry no kernel.
327    fn is_input(&self) -> bool {
328        false
329    }
330    /// This op's forward kernel source.
331    fn forward_kernel_source(&self) -> &str;
332    /// This op's forward kernel entry-point symbol name.
333    fn forward_kernel_entry_point(&self) -> &str;
334    /// This op's output shape.
335    fn output_shape(&self) -> &Shape;
336    /// This op's output dtype.
337    fn output_dtype(&self) -> DtypeRepr;
338    /// Returns the runtime dispatch object for this op, or `None` for Input nodes.
339    fn runtime_op(&self) -> Option<Arc<dyn RuntimeOp>> {
340        None
341    }
342
343    /// Returns the backward kernel source, or `""` if no backward is available.
344    #[cfg(feature = "training")]
345    fn backward_kernel_source(&self) -> &str {
346        ""
347    }
348
349    /// Returns the backward kernel entry point name.
350    #[cfg(feature = "training")]
351    fn backward_kernel_entry_point(&self) -> &str {
352        "entry_point"
353    }
354}
355
356/// Whether a graph is being lowered for inference or training (training lowerings additionally
357/// wire up backward kernels).
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
359pub enum LoweringMode {
360    /// Inference only; no backward kernels.
361    #[default]
362    Inference,
363    /// Training; backward kernels are wired up.
364    Training,
365}
366
367/// Lowers a [`Graph`] into a DAG of [`ExecutableOp`]s, ready to compile/execute.
368pub trait Lowering<'a> {
369    /// Lowers `graph` for `mode`.
370    fn lower(&self, graph: &Graph, mode: LoweringMode) -> Result<Dag<Box<dyn ExecutableOp>>>;
371
372    /// Like [`lower`] but also returns a `graph_node_idx → dag_node_idx` mapping
373    /// so that graph-level metadata (e.g. names) can be propagated into the compiled DAG.
374    ///
375    /// The default implementation assumes a 1-to-1 identity mapping between graph
376    /// topological order and DAG node indices — valid for graphs that are already in
377    /// topological order (which is always true for models built by sequential recording)
378    /// and lowerings that do not reorder or split nodes.  Override this method in
379    /// lowerings that reorder or split nodes to return the correct mapping.
380    #[allow(clippy::type_complexity)]
381    fn lower_with_mapping(
382        &self,
383        graph: &Graph,
384        mode: LoweringMode,
385    ) -> Result<(Dag<Box<dyn ExecutableOp>>, Vec<usize>)> {
386        let dag = self.lower(graph, mode)?;
387        let topo = graph.topological_sort();
388        // topo[dag_idx] = graph_node_idx  →  graph_to_dag[graph_node_idx] = dag_idx
389        let mut graph_to_dag = vec![0usize; graph.nodes.len()];
390        for (dag_idx, graph_idx) in topo.into_iter().enumerate() {
391            graph_to_dag[graph_idx] = dag_idx;
392        }
393        Ok((dag, graph_to_dag))
394    }
395
396    /// Returns extra (dag_idx, name) pairs beyond those derivable from the
397    /// 1-to-1 graph→dag mapping.  Used for lowerings that split one graph node
398    /// into multiple DAG nodes (e.g. Conv2d-with-bias → Conv2d + NchwBiasAdd);
399    /// the "extra" DAG nodes would otherwise have no name and their weight
400    /// parameters would not be loaded.
401    fn extra_dag_names(&self, _graph: &Graph, _graph_to_dag: &[usize]) -> Vec<(usize, String)> {
402        Vec::new()
403    }
404
405    /// Returns the next lowering in a middleware chain, or `None` if this is
406    /// the final lowering.  A custom lowering can call `self.base_lowering()`
407    /// to delegate ops it does not handle.
408    fn base_lowering(&self) -> Option<&dyn Lowering<'a>> {
409        None
410    }
411}
412
413/// A compiled, runnable model.
414pub trait Model<'a> {
415    /// This model's input type.
416    type Input;
417    /// This model's output type.
418    type Output;
419
420    /// Runs inference, producing `Output` from `input`.
421    fn forward(&self, input: Self::Input) -> Result<Self::Output>;
422}