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}