Skip to main content

teeny_cuda/compiler/
graph.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 std::collections::HashMap;
18
19use teeny_compiler::compiler::backend::llvm::compiler::LlvmCompiler;
20use teeny_core::{
21    compiler::{Compiler, Target},
22    device::program::Kernel,
23    graph::{Graph, compiler::GraphCompiler},
24    model::{ExecutableOp, Lowering, LoweringMode, Model},
25    utils::dag::Dag,
26};
27
28use crate::{
29    errors::Result,
30    model::{CompiledNode, CudaModel},
31};
32
33/// Adapts a `&dyn ExecutableOp` to the `Kernel` trait so that `LlvmCompiler`
34/// can compile the forward kernel without knowing the concrete argument types.
35struct ForwardKernelAdapter<'a>(&'a dyn ExecutableOp);
36
37/// Adapts a `&dyn ExecutableOp` backward kernel source to the `Kernel` trait.
38#[cfg(feature = "training")]
39struct BackwardKernelAdapter<'a>(&'a dyn ExecutableOp);
40
41impl<'a> Kernel for ForwardKernelAdapter<'a> {
42    /// Argument types are not needed at compile time; `()` satisfies the bound.
43    type Args<'b> = ();
44
45    fn name(&self) -> &str {
46        self.0.name()
47    }
48
49    fn source(&self) -> &str {
50        self.0.forward_kernel_source()
51    }
52
53    fn kernel_source(&self) -> &str {
54        self.0.forward_kernel_source()
55    }
56
57    fn entry_point_source(&self) -> &str {
58        ""
59    }
60}
61
62#[cfg(feature = "training")]
63impl<'a> Kernel for BackwardKernelAdapter<'a> {
64    type Args<'b> = ();
65
66    fn name(&self) -> &str {
67        self.0.name()
68    }
69
70    fn source(&self) -> &str {
71        self.0.backward_kernel_source()
72    }
73
74    fn kernel_source(&self) -> &str {
75        self.0.backward_kernel_source()
76    }
77
78    fn entry_point_source(&self) -> &str {
79        ""
80    }
81}
82
83/// Compiles a `teeny-core` [`Graph`] into a runnable CUDA model.
84#[derive(Debug, Clone)]
85pub struct CudaGraphCompiler {
86    compiler: LlvmCompiler,
87}
88
89impl CudaGraphCompiler {
90    /// Wraps an [`LlvmCompiler`] as a graph compiler.
91    pub fn new(compiler: LlvmCompiler) -> Self {
92        Self { compiler }
93    }
94
95    /// Compile a graph to a `CudaModel`, returning the concrete type directly.
96    /// Use this when you need access to the compiled DAG (e.g. in tests).
97    pub fn compile_model<'a, L: Lowering<'a>, T: Target>(
98        &self,
99        graph: &Graph,
100        lowering: &L,
101        target: &T,
102        mode: LoweringMode,
103        force: bool,
104    ) -> Result<CudaModel<'a>> {
105        self.compile_inner(graph, lowering, target, mode, force)
106    }
107
108    fn compile_inner<'a, L: Lowering<'a>, T: Target>(
109        &self,
110        graph: &Graph,
111        lowering: &L,
112        target: &T,
113        mode: LoweringMode,
114        force: bool,
115    ) -> Result<CudaModel<'a>> {
116        let (op_dag, graph_to_dag) = lowering.lower_with_mapping(graph, mode)?;
117
118        let compiler = match target.target_cpu() {
119            Some(cpu) => self.compiler.clone().with_target_cpu(cpu),
120            None => self.compiler.clone(),
121        };
122
123        let mut compiled_dag: Dag<CompiledNode> = Dag::new();
124
125        for i in 0..op_dag.len() {
126            let op = op_dag.node(i).value.as_ref();
127            let ptx_path = if op.is_input() {
128                String::new()
129            } else if op.forward_kernel_source().is_empty() {
130                return Err(anyhow::anyhow!(
131                    "no forward kernel source for op {}",
132                    op.name()
133                ));
134            } else {
135                let adapter = ForwardKernelAdapter(op);
136                compiler.compile(&adapter, target, force)?
137            };
138
139            #[cfg(feature = "training")]
140            let backward_ptx_path = if op.is_input() || op.backward_kernel_source().is_empty() {
141                None
142            } else {
143                let adapter = BackwardKernelAdapter(op);
144                Some(compiler.compile(&adapter, target, force)?)
145            };
146
147            compiled_dag.add_node(CompiledNode {
148                ptx_path,
149                entry_point: op.forward_kernel_entry_point().to_string(),
150                output_shape: op.output_shape().clone(),
151                output_dtype: op.output_dtype(),
152                runtime_op: op.runtime_op(),
153                #[cfg(feature = "training")]
154                backward_ptx_path,
155                #[cfg(feature = "training")]
156                backward_entry_point: op.backward_kernel_entry_point().to_string(),
157            });
158        }
159
160        // Rebuild edges using parent lists (not children) to preserve the insertion
161        // order that the lowering recorded. If we iterated children (add_edge(i, child)
162        // for each i in 0..N), parents with smaller DAG indices would be appended first,
163        // destroying the logical input ordering required by ops like ChannelCat.
164        for i in 0..op_dag.len() {
165            for &parent in &op_dag.node(i).parents {
166                compiled_dag.add_edge(parent, i);
167            }
168        }
169
170        // Propagate graph-level node names (from name_scope annotations) into the
171        // compiled DAG using the graph_node → dag_node index mapping.
172        let mut dag_names: HashMap<usize, String> = graph
173            .names
174            .iter()
175            .filter_map(|(&graph_idx, name)| {
176                let dag_idx = *graph_to_dag.get(graph_idx)?;
177                Some((dag_idx, name.clone()))
178            })
179            .collect();
180
181        // Lowerings that split one graph node into multiple DAG nodes (e.g.
182        // Conv2d-with-bias → Conv2d + NchwBiasAdd) expose the extra mappings
183        // here so that every DAG node with parameters can resolve its name.
184        for (dag_idx, name) in lowering.extra_dag_names(graph, &graph_to_dag) {
185            dag_names.entry(dag_idx).or_insert(name);
186        }
187
188        CudaModel::with_names(compiled_dag, dag_names)
189    }
190}
191
192impl GraphCompiler for CudaGraphCompiler {
193    fn compile<'a, L: Lowering<'a>, T: Target>(
194        &self,
195        graph: &Graph,
196        lowering: &L,
197        target: &T,
198        mode: LoweringMode,
199        force: bool,
200    ) -> Result<impl Model<'a>> {
201        self.compile_inner(graph, lowering, target, mode, force)
202    }
203}