Skip to main content

logicaffeine_compile/spatial/lower/tensix/
mod.rs

1//! Tensix (TT-Metalium) lowering of the spatial IR. Every kernel — reader, compute, writer — is PRINTED
2//! from the schedule; no vendor kernel is wired by path, and every construct the artifact carries is the
3//! image of an IR object (the choreography census is a bijection, locked).
4//!
5//! The lowering reads only the verified program and its target:
6//!   buffers   → circular buffers (`cb.rs`: one `CircularBufferConfig` per buffer template);
7//!   programs  → kernels (`roles.rs`: equal engine signatures share one source — the vendor's four
8//!               reader variants are DERIVED, not enumerated);
9//!   site data → runtime args (`args.rs`: one layout drives both the kernel text and the host loop);
10//!   ops       → C++ (`kernel.rs`: every `ChoreoOp` is one statement kind);
11//!   the whole → host + CMake + a C oracle (`host.rs`, `oracle.rs`).
12
13mod args;
14mod cb;
15mod host;
16mod kernel;
17mod oracle;
18mod roles;
19
20pub use args::{ArgSlot, ArgSource, RuntimeArgLayout};
21pub use cb::{CbPlan, CbSlot};
22pub use host::mcast_corners;
23pub use roles::{KernelClass, Launch};
24
25use super::{
26    require_one_plan, require_verified, Census, SpatialArtifact, SpatialBackend, SpatialLowerError,
27};
28use crate::spatial::ir::*;
29use crate::spatial::target::{Fidelity, SpatialTarget};
30
31#[derive(Clone, Debug, PartialEq)]
32pub enum KernelRole {
33    Movement { engine: u8 },
34    Compute { fp32_acc: bool, fidelity: Fidelity },
35}
36
37/// One kernel SOURCE and its launches: a launch is a `CreateKernel` on one NOC over the maximal rectangles
38/// of the sites it covers (the writer of a 2-D GEMM is one source launched on both NOCs).
39#[derive(Clone, Debug, PartialEq)]
40pub struct Kernel {
41    pub name: String,
42    pub role: KernelRole,
43    pub source: String,
44    pub launches: Vec<Launch>,
45    pub compile_args: Vec<(String, u32)>,
46}
47
48impl Kernel {
49    /// Every site the source runs on.
50    pub fn sites(&self) -> Vec<Site> {
51        self.launches
52            .iter()
53            .flat_map(|l| l.sites.iter().copied())
54            .collect()
55    }
56}
57
58#[derive(Clone, Debug, PartialEq)]
59pub struct TensixArtifact {
60    pub name: String,
61    pub kernels: Vec<Kernel>,
62    pub host_cpp: String,
63    pub cmake: String,
64    pub runtime_arg_layout: RuntimeArgLayout,
65    pub oracle_c: String,
66    pub census: Census,
67    pub dprint_cores: Option<String>,
68    pub pass_marker: &'static str,
69    /// The ledger's `LOGOS_MM_CFG` line the host prints (the arrangement, grid and blocking of the plan).
70    pub cfg_line: String,
71}
72
73/// The files of a bundle as `(relative path, contents)` — the ledger hashes exactly these.
74pub fn bundle_files(art: &TensixArtifact) -> Vec<(String, String)> {
75    let mut files = vec![("host.cpp".to_string(), art.host_cpp.clone())];
76    for k in &art.kernels {
77        files.push((format!("kernels/{}", k.name), k.source.clone()));
78    }
79    files
80}
81
82/// Every file a standalone bundle directory needs (the measure harness writes these verbatim).
83pub fn bundle_dir_files(art: &TensixArtifact) -> Vec<(String, String)> {
84    let mut files = bundle_files(art);
85    files.push(("CMakeLists.txt".to_string(), art.cmake.clone()));
86    files.push(("logos_oracle.c".to_string(), art.oracle_c.clone()));
87    files
88}
89
90pub struct TensixBackend {
91    pub target: SpatialTarget,
92}
93
94impl TensixBackend {
95    pub fn new(target: SpatialTarget) -> Self {
96        TensixBackend { target }
97    }
98
99    /// The lowering proper: a pure function of the verified program and the target.
100    /// Just the kernels — everything the compute engines run, and nothing that needs the HOST.
101    ///
102    /// ★ WHY THIS IS A SEPARATE ENTRY POINT. The host program prints a golden, and a golden needs to
103    /// reproduce the input data, so `print_host` refuses a tensor whose fill is `External`. That is the
104    /// right refusal for a runnable bundle and the wrong one for a caller that only wants the kernel:
105    /// `compile_to_tenstorrent_matmul` returns a kernel string, and a GEMM over data the compiler
106    /// cannot reproduce still has a perfectly good kernel. Splitting the two means neither has to
107    /// weaken for the other.
108    pub fn printed_kernels(&self, p: &SpatialProgram) -> Result<Vec<Kernel>, SpatialLowerError> {
109        Ok(self.print_all(p)?.0)
110    }
111
112    /// The compute kernel's source alone — what the kernel-returning `compile_to_tenstorrent_*` entry
113    /// points hand back.
114    pub fn compute_kernel_source(&self, p: &SpatialProgram) -> Result<String, SpatialLowerError> {
115        self.printed_kernels(p)?
116            .into_iter()
117            .find(|k| matches!(k.role, KernelRole::Compute { .. }))
118            .map(|k| k.source)
119            .ok_or(SpatialLowerError::Unsupported(
120                "a program with no compute kernel",
121            ))
122    }
123
124    /// The shared front half of a Tensix lowering: validate, plan the circular buffers, classify the
125    /// engine programs, and PRINT THE KERNELS. Everything after this needs the host, which needs the
126    /// input fills; splitting here is what lets `printed_kernels` succeed where a full bundle cannot.
127    #[allow(clippy::type_complexity)]
128    fn print_all(
129        &self,
130        p: &SpatialProgram,
131    ) -> Result<
132        (
133            Vec<Kernel>,
134            cb::CbPlan,
135            Vec<roles::KernelClass>,
136            RuntimeArgLayout,
137            bool,
138            Fidelity,
139        ),
140        SpatialLowerError,
141    > {
142        require_verified(p)?;
143        require_one_plan(p)?;
144        let target = &self.target;
145        if target.tile.rows != 32 || target.tile.cols != 32 {
146            return Err(SpatialLowerError::Unsupported("Tensix tiles are 32×32"));
147        }
148        let plan = p
149            .work
150            .plan
151            .ok_or(SpatialLowerError::Unsupported("a tiled plan"))?;
152        let exact_f32 = p.numerics == NumericsPolicy::Exact && plan.acc == AccPolicy::F32;
153        if matches!(plan.acc, AccPolicy::I32 | AccPolicy::I64) {
154            return Err(SpatialLowerError::Unsupported(
155                "integer accumulate on a float-primary matrix engine",
156            ));
157        }
158        let fidelity = target
159            .matmul_fidelity()
160            .ok_or(SpatialLowerError::Unsupported("a matrix engine"))?;
161        let cbs = cb::CbPlan::of(p, target)?;
162        let classes = roles::classes(p)?;
163        let mut layout = RuntimeArgLayout::default();
164        let mut kernels = Vec::new();
165        for class in &classes {
166            let site_slots = args::slots_for(p, class)?;
167            let (source, slots) =
168                kernel::print_kernel(p, target, &cbs, class, &site_slots, exact_f32)?;
169            let role = match class.engine {
170                Engine::Movement(e) => KernelRole::Movement { engine: e },
171                Engine::Compute => KernelRole::Compute {
172                    fp32_acc: exact_f32,
173                    fidelity,
174                },
175            };
176            kernels.push(Kernel {
177                name: class.file_name.clone(),
178                role,
179                source,
180                launches: class.launches.clone(),
181                compile_args: Vec::new(),
182            });
183            layout.per_kernel.push((class.file_name.clone(), slots));
184        }
185        Ok((kernels, cbs, classes, layout, exact_f32, fidelity))
186    }
187
188    pub fn lower_tensix(&self, p: &SpatialProgram) -> Result<TensixArtifact, SpatialLowerError> {
189        let target = &self.target;
190        let (kernels, cbs, classes, layout, exact_f32, fidelity) = self.print_all(p)?;
191        let cfg_line = host::cfg_line(p);
192        let host_cpp = host::print_host(
193            p, target, &cbs, &classes, &layout, exact_f32, fidelity, &cfg_line,
194        )?;
195        let cmake = host::print_cmake();
196        let oracle_c = oracle::print_oracle(p)?;
197        Ok(TensixArtifact {
198            name: p.name.clone(),
199            kernels,
200            host_cpp,
201            cmake,
202            runtime_arg_layout: layout,
203            oracle_c,
204            census: Census::of(p),
205            dprint_cores: None,
206            pass_marker: "LOGOS_MM_PASS",
207            cfg_line,
208        })
209    }
210}
211
212impl SpatialBackend for TensixBackend {
213    fn name(&self) -> &str {
214        self.target.name
215    }
216    fn target(&self) -> &SpatialTarget {
217        &self.target
218    }
219    fn lower(&self, p: &SpatialProgram) -> Result<SpatialArtifact, SpatialLowerError> {
220        Ok(SpatialArtifact::Tensix(self.lower_tensix(p)?))
221    }
222}
223
224/// The name of a buffer template: the placed instance's name up to its `@site` suffix.
225pub(crate) fn template_name(p: &SpatialProgram, b: &Buffer) -> String {
226    let name = &b.name;
227    let base = name.split('@').next().unwrap_or(name);
228    let _ = p;
229    base.to_string()
230}
231
232/// The template id of a placed buffer.
233pub(crate) fn template_of(b: &Buffer) -> BufferId {
234    b.origin.unwrap_or(b.id)
235}