Skip to main content

logicaffeine_compile/spatial/lower/tensix/
host.rs

1//! The host program: DRAM buffers from the tensors, circular buffers from the plan, semaphores from the IR,
2//! one `CreateKernel` per launch, one `SetRuntimeArgs` per (site, launch) from the argument layout, then the
3//! input fill, the golden, the run, the PCC and the ledger's `LOGOS_MM_CFG` / `LOGOS_MM_PASS` lines.
4
5use super::args::{ArgSource, Axis, RuntimeArgLayout};
6use super::cb::{data_format, CbPlan};
7use super::roles::{rect_decompose, KernelClass};
8use super::{template_name, template_of, SpatialLowerError};
9use crate::codegen_sva::fabric_target::Precision;
10use crate::spatial::ir::*;
11use crate::spatial::target::{Fidelity, SpatialTarget};
12
13pub(super) fn ident(s: &str) -> String {
14    let mut out: String = s
15        .chars()
16        .map(|c| {
17            if c.is_ascii_alphanumeric() || c == '_' {
18                c
19            } else {
20                '_'
21            }
22        })
23        .collect();
24    if out
25        .chars()
26        .next()
27        .map(|c| c.is_ascii_digit())
28        .unwrap_or(true)
29    {
30        out.insert(0, 't');
31    }
32    out
33}
34
35fn ranges_cpp(rects: &[Rect]) -> String {
36    let items: Vec<String> = rects
37        .iter()
38        .map(|r| format!("CoreRange({{{}, {}}}, {{{}, {}}})", r.x0, r.y0, r.x1, r.y1))
39        .collect();
40    format!(
41        "CoreRangeSet(std::vector<CoreRange>{{{}}})",
42        items.join(", ")
43    )
44}
45
46fn fidelity_cpp(f: Fidelity) -> &'static str {
47    match f {
48        Fidelity::LoFi => "MathFidelity::LoFi",
49        Fidelity::HiFi2 => "MathFidelity::HiFi2",
50        Fidelity::HiFi3 => "MathFidelity::HiFi3",
51        Fidelity::HiFi4 | Fidelity::Exact => "MathFidelity::HiFi4",
52    }
53}
54
55fn fidelity_label(f: Fidelity) -> &'static str {
56    match f {
57        Fidelity::LoFi => "LoFi",
58        Fidelity::HiFi2 => "HiFi2",
59        Fidelity::HiFi3 => "HiFi3",
60        Fidelity::HiFi4 | Fidelity::Exact => "HiFi4",
61    }
62}
63
64/// The per-circular-buffer unpack mode of the compute kernels: a Float32 buffer that the schedule reloads into
65/// the accumulator (`copy_tile` of a spilled partial) must be marked `UnpackToDestFp32` under fp32 accumulate —
66/// otherwise the reload is routed through the 19-bit SrcA register and rounded to TF32 on every block boundary
67/// (measured on ttsim: PCC 0.14 at 640³ blocked/exact). A buffer read by the FPU as an operand keeps `Default`.
68fn unpack_to_dest_cpp(p: &SpatialProgram, cbs: &CbPlan, exact_f32: bool) -> String {
69    let mut marked: Vec<u32> = Vec::new();
70    if exact_f32 {
71        if let Some(sched) = &p.schedule {
72            for prog in &sched.programs {
73                walk_ops(
74                    &prog.ops.iter().map(|t| t.op.clone()).collect::<Vec<_>>(),
75                    1,
76                    &mut |op, _| {
77                        if let ChoreoOp::Compute {
78                            acc_reload: Some(b),
79                            ..
80                        } = op
81                        {
82                            let slot = cbs.slot(*b);
83                            if slot.dtype == Precision::Fp32 && !marked.contains(&slot.index) {
84                                marked.push(slot.index);
85                            }
86                        }
87                    },
88                );
89            }
90        }
91    }
92    if marked.is_empty() {
93        return "{}".into();
94    }
95    marked.sort();
96    let sets: Vec<String> = marked
97        .iter()
98        .map(|i| format!("utd[{i}] = UnpackToDestMode::UnpackToDestFp32;"))
99        .collect();
100    format!("[]{{ std::vector<UnpackToDestMode> utd(NUM_CIRCULAR_BUFFERS, UnpackToDestMode::Default); {} return utd; }}()", sets.join(" "))
101}
102
103/// The bounding extent of the placed sites: the smallest device grid that can host the program.
104pub fn placed_extent(pl: &Placement) -> (usize, usize) {
105    let sites = pl.sites();
106    (
107        sites.iter().map(|s| s.x as usize + 1).max().unwrap_or(0),
108        sites.iter().map(|s| s.y as usize + 1).max().unwrap_or(0),
109    )
110}
111
112/// The ledger's configuration line: the emergent plan, in the vendor's vocabulary, plus the deployment mesh.
113pub fn cfg_line(p: &SpatialProgram) -> String {
114    let s = p.summary();
115    let b = s.block.unwrap_or(BlockShape {
116        osh: 1,
117        osw: 1,
118        ibw: 1,
119    });
120    let pl = p.placement.as_ref().expect("a placed program");
121    format!(
122        "LOGOS_MM_CFG strategy={} grid={}x{} per_core={}x{} out_block={}x{} in0_block_w={} subblock={}x{} origin=\"compiler-emitted, all kernels generated\" mesh={}x{} sites={}",
123        s.arrangement_label(),
124        s.grid.1,
125        s.grid.0,
126        s.per_site_block.0,
127        s.per_site_block.1,
128        s.per_site_block.0,
129        s.per_site_block.1,
130        b.ibw,
131        b.osh,
132        b.osw,
133        pl.mesh.x,
134        pl.mesh.y,
135        pl.sites().len()
136    )
137}
138
139/// The receivers of the group `operand` at `site`: the group's rectangle minus its sender (a 1-D group).
140fn receivers(
141    p: &SpatialProgram,
142    site: Site,
143    operand: Operand,
144) -> Result<(Rect, Site, usize), SpatialLowerError> {
145    let pl = p.placement.as_ref().unwrap();
146    let g = pl
147        .groups
148        .iter()
149        .find(|g| g.operand == operand && g.rect.contains(site))
150        .ok_or(SpatialLowerError::Unsupported(
151            "a site outside every group of its operand",
152        ))?;
153    let r = g.rect;
154    let rect = match g.direction {
155        Direction::Row => {
156            if r.y0 != r.y1
157                || g.sender
158                    != (Site {
159                        x: r.x0,
160                        y: r.y0,
161                        chip: 0,
162                    })
163            {
164                return Err(SpatialLowerError::GroupSpansSites { group: g.id });
165            }
166            Rect {
167                x0: r.x0 + 1,
168                y0: r.y0,
169                x1: r.x1,
170                y1: r.y1,
171            }
172        }
173        Direction::Col => {
174            if r.x0 != r.x1
175                || g.sender
176                    != (Site {
177                        x: r.x0,
178                        y: r.y0,
179                        chip: 0,
180                    })
181            {
182                return Err(SpatialLowerError::GroupSpansSites { group: g.id });
183            }
184            Rect {
185                x0: r.x0,
186                y0: r.y0 + 1,
187                x1: r.x1,
188                y1: r.y1,
189            }
190        }
191    };
192    if rect.x1 < rect.x0 || rect.y1 < rect.y0 {
193        return Err(SpatialLowerError::GroupSpansSites { group: g.id });
194    }
195    Ok((rect, g.sender, r.len() - 1))
196}
197
198/// The multicast rectangle of a site's receivers for `operand`, as `(start, end)` corners for a kernel on
199/// `noc`: NOC 0 travels toward larger coordinates and takes the near corner first; NOC 1 travels the other
200/// way and takes the far corner first (the vendor's `right → left+1`, `bottom → top+1`). The simulator refuses
201/// a rectangle given against its NOC's direction.
202pub fn mcast_corners(
203    p: &SpatialProgram,
204    site: Site,
205    operand: Operand,
206    noc: u8,
207) -> Result<((u16, u16), (u16, u16)), SpatialLowerError> {
208    let (r, _, _) = receivers(p, site, operand)?;
209    Ok(if noc == 1 {
210        ((r.x1, r.y1), (r.x0, r.y0))
211    } else {
212        ((r.x0, r.y0), (r.x1, r.y1))
213    })
214}
215
216fn arg_value(
217    p: &SpatialProgram,
218    site: Site,
219    source: &ArgSource,
220    noc: u8,
221) -> Result<String, SpatialLowerError> {
222    let pl = p.placement.as_ref().unwrap();
223    let items = pl.items_at(site);
224    Ok(match source {
225        ArgSource::TensorAddr(t) => {
226            format!("(uint32_t)buf_{}->address()", ident(&p.tensor(*t).name))
227        }
228        ArgSource::CyclesAddr => "(uint32_t)buf_cycles->address()".into(),
229        ArgSource::CyclesSlot(engine) => format!(
230            "{}u",
231            2 * pl
232                .items_of_site
233                .iter()
234                .position(|(x, _)| *x == site)
235                .unwrap_or(0)
236                + *engine as usize
237        ),
238        ArgSource::ItemStart => format!("{}u", items.first().map(|i| i.idx()).unwrap_or(0)),
239        ArgSource::ItemCount => format!("{}u", items.len()),
240        ArgSource::ItemY0 => format!(
241            "{}u",
242            items
243                .first()
244                .map(|i| p.work.items[i.idx()].coord.0)
245                .unwrap_or(0)
246        ),
247        ArgSource::ItemX0 => format!(
248            "{}u",
249            items
250                .first()
251                .map(|i| p.work.items[i.idx()].coord.1)
252                .unwrap_or(0)
253        ),
254        ArgSource::ItemCountX => {
255            let xs: std::collections::BTreeSet<usize> = items
256                .iter()
257                .map(|i| p.work.items[i.idx()].coord.1)
258                .collect();
259            format!("{}u", xs.len().max(1))
260        }
261        ArgSource::McastStart { operand, axis } => {
262            let ((x, y), _) = mcast_corners(p, site, *operand, noc)?;
263            match axis {
264                Axis::X => format!("(uint32_t)P({x}, {y}).x"),
265                Axis::Y => format!("(uint32_t)P({x}, {y}).y"),
266            }
267        }
268        ArgSource::McastEnd { operand, axis } => {
269            let (_, (x, y)) = mcast_corners(p, site, *operand, noc)?;
270            match axis {
271                Axis::X => format!("(uint32_t)P({x}, {y}).x"),
272                Axis::Y => format!("(uint32_t)P({x}, {y}).y"),
273            }
274        }
275        ArgSource::McastNumDests { operand } => format!("{}u", receivers(p, site, *operand)?.2),
276        ArgSource::McastSender { operand, axis } => {
277            let (_, s, _) = receivers(p, site, *operand)?;
278            match axis {
279                Axis::X => format!("(uint32_t)P({}, {}).x", s.x, s.y),
280                Axis::Y => format!("(uint32_t)P({}, {}).y", s.x, s.y),
281            }
282        }
283        ArgSource::SemHandle(s) => format!("(uint32_t)sem_{}", s.idx()),
284        ArgSource::TensorDim { tensor, axis } => {
285            let tiles = p.tensor(*tensor).tiles;
286            format!(
287                "{}u",
288                match axis {
289                    Axis::Y => tiles.rows_t,
290                    Axis::X => tiles.cols_t,
291                }
292            )
293        }
294    })
295}
296
297/// The C++ element type a tensor's host-side vector holds — the one its DEVICE buffer is paged for.
298///
299/// ★ THE HOST MUST SPEAK THE TENSOR'S DTYPE. A GEMM's operands are always bf16, so the printer wrote
300/// `std::vector<bfloat16>` unconditionally and nothing ever disagreed. A row op declares fp32, whose
301/// tile is 4096 bytes rather than 2048 — and a bf16 source vector is then exactly half the buffer it
302/// is uploaded into, which tt-metal catches at runtime as `Source vector is too small for mesh
303/// buffer`. The type follows the declaration rather than the one case that used to be the only case.
304fn host_elem_type(p: Precision) -> Result<&'static str, SpatialLowerError> {
305    Ok(match p {
306        Precision::Bf16 => "bfloat16",
307        Precision::Fp32 => "float",
308        other => return Err(SpatialLowerError::Precision { needed: other }),
309    })
310}
311
312/// A C++ literal for `v`: its shortest round-trip decimal with `suffix`, or the `<cmath>` macro for a value no
313/// decimal spells — `{:?}` would print `inf` and `NaN`, which C++ reads as undeclared names.
314fn cpp_float(v: f64, suffix: &str) -> String {
315    if v.is_nan() {
316        "NAN".into()
317    } else if v.is_infinite() {
318        if v < 0.0 {
319            "-INFINITY".into()
320        } else {
321            "INFINITY".into()
322        }
323    } else {
324        format!("{v:?}{suffix}")
325    }
326}
327
328fn fill_lines(
329    p: &SpatialProgram,
330    t: &TensorDecl,
331    var: &str,
332    padded_cols: usize,
333) -> Result<Vec<String>, SpatialLowerError> {
334    let (rows, cols) = (t.elems.rows, t.elems.cols);
335    let ety = host_elem_type(t.dtype)?;
336    let mut out = Vec::new();
337    let assign = |expr: &str| {
338        format!("for (uint32_t r = 0; r < {rows}u; ++r) for (uint32_t c = 0; c < {cols}u; ++c) {{ const uint32_t p = r * {cols}u + c; {var}[r * {padded_cols}u + c] = {ety}((float)({expr})); }}")
339    };
340    match &t.init {
341        TensorInit::Zero => {}
342        TensorInit::Pattern { modulus, shift } => {
343            out.push(assign(&format!("(int64_t)(p % {modulus}u) + ({shift})")))
344        }
345        // One constant, emitted as one literal rather than a table as long as the tensor.
346        TensorInit::ConstF(b) => out.push(assign(&cpp_float(f64::from_bits(*b), "f"))),
347        TensorInit::Values(v) => {
348            if v.len() > 65536 {
349                return Err(SpatialLowerError::TooBig {
350                    what: "literal tensor values",
351                    need: v.len(),
352                    have: 65536,
353                });
354            }
355            let lits: Vec<String> = v.iter().map(|x| x.to_string()).collect();
356            out.push(format!(
357                "static const int64_t {var}_values[{}] = {{{}}};",
358                v.len().max(1),
359                lits.join(", ")
360            ));
361            out.push(assign(&format!("{var}_values[p]")));
362        }
363        TensorInit::FloatValues(v) => {
364            if v.len() > 65536 {
365                return Err(SpatialLowerError::TooBig {
366                    what: "literal tensor values",
367                    need: v.len(),
368                    have: 65536,
369                });
370            }
371            let lits: Vec<String> = v
372                .iter()
373                .map(|b| cpp_float(f64::from_bits(*b), ""))
374                .collect();
375            out.push(format!(
376                "static const double {var}_values[{}] = {{{}}};",
377                v.len().max(1),
378                lits.join(", ")
379            ));
380            out.push(assign(&format!("{var}_values[p]")));
381        }
382        TensorInit::RandomBf16 { seed, shift_milli } => {
383            let copy = if ety == "bfloat16" {
384                "rnd[r * {cols}u + c]".to_string()
385            } else {
386                format!("{ety}(static_cast<float>(rnd[r * {{cols}}u + c]))")
387            };
388            let copy = copy.replace("{cols}", &cols.to_string());
389            out.push(format!("{{ auto rnd = create_random_vector_of_bfloat16_native(2u * {rows}u * {cols}u, 1, {seed}, {}f); for (uint32_t r = 0; r < {rows}u; ++r) for (uint32_t c = 0; c < {cols}u; ++c) {var}[r * {padded_cols}u + c] = {copy}; }}", *shift_milli as f64 / 1000.0));
390        }
391        TensorInit::External => {
392            return Err(SpatialLowerError::Unsupported(
393                "a tensor whose data is external to the program",
394            ))
395        }
396    }
397    let _ = p;
398    Ok(out)
399}
400
401/// The CPU golden for a non-GEMM program, composed stage by stage from the SAME chain the kernels run.
402///
403/// ★ ONE GOLDEN PER STAGE, NOT ONE PER WORKLOAD. A normalization is not one kernel — it is a square,
404/// a reduction, a transform and a rescale, each with its own register window. Printing a single fused
405/// reference for it would be a SECOND implementation of the pipeline, correct only as long as someone
406/// kept the two in step. Composing the reference the way the choreography composes the work means a
407/// stage that is added, reordered or removed changes the golden by construction; there is nothing left
408/// to keep in step. Each stage's arithmetic mirrors `spatial/eval.rs`, which is the reference
409/// semantics, so epsilons and f64 accumulation are reproduced rather than re-derived.
410fn golden_lines(
411    p: &SpatialProgram,
412    ids: &[(String, usize, usize)],
413    tile: usize,
414) -> Result<Vec<String>, SpatialLowerError> {
415    let sched = p.schedule.as_ref().ok_or(SpatialLowerError::Unsupported(
416        "a golden for an unscheduled program",
417    ))?;
418    // Which tensor fills a buffer, and the compute stages in the order the choreography runs them.
419    // `ChoreoOp::Compute` does not name its destination — the `PackAcc` that follows it does — so the
420    // two are paired here rather than assumed to be the same buffer every time.
421    let mut from_tensor: std::collections::BTreeMap<BufferId, TensorId> = Default::default();
422    let mut steps: Vec<(ComputeOp, Vec<BufferId>, BufferId)> = Vec::new();
423    let mut pending: Option<(ComputeOp, Vec<BufferId>)> = None;
424    for prog in &sched.programs {
425        let ops: Vec<ChoreoOp> = prog.ops.iter().map(|t| t.op.clone()).collect();
426        walk_ops(&ops, 1, &mut |op, _| match op {
427            ChoreoOp::ReadTiles { tensor, into, .. } => {
428                from_tensor.entry(*into).or_insert(*tensor);
429            }
430            ChoreoOp::Compute { op, ins, .. } => pending = Some((op.clone(), ins.clone())),
431            ChoreoOp::PackAcc { to, .. } => {
432                if let Some((op, ins)) = pending.take() {
433                    steps.push((op, ins, *to));
434                }
435            }
436            _ => {}
437        });
438    }
439    if steps.is_empty() {
440        return Err(SpatialLowerError::Unsupported(
441            "a golden for a program with no compute stage",
442        ));
443    }
444    // Every site runs the same chain, so keep the first instance of each distinct step.
445    let mut seen: std::collections::BTreeSet<(Vec<BufferId>, BufferId)> = Default::default();
446    steps.retain(|(_, ins, dst)| {
447        let key = (
448            ins.iter()
449                .map(|b| template_of(p.buffer(*b)))
450                .collect::<Vec<_>>(),
451            template_of(p.buffer(*dst)),
452        );
453        seen.insert(key)
454    });
455    // ★ A GOLDEN PLANE IS AS WIDE AS THE DATA, NOT AS WIDE AS A PAGE. A page is what one work item
456    // carries; a buffer's plane is what every item carries together. They coincide when the item is a
457    // whole tile-row (a normalization) and differ completely when the items sweep the columns (an
458    // elementwise map over a flat tensor is 64 one-tile items, page width 32, plane width 2048).
459    // Sizing the plane by the page silently truncated the operands and then read past the end of the
460    // result — a golden that is wrong in a way no lowering test can see. The width is seeded from the
461    // tensor each load carries and propagated through the chain: only a reduction narrows it.
462    let mut plane: std::collections::BTreeMap<BufferId, usize> = Default::default();
463    for (b, t) in &from_tensor {
464        plane.insert(template_of(p.buffer(*b)), p.tensor(*t).tiles.cols_t * tile);
465    }
466    for (op, ins, dst) in &steps {
467        let w = match op {
468            ComputeOp::Generic { iters, .. }
469                if iters
470                    .get(1)
471                    .map(|a| matches!(a.kind, IterKind::Reduce { .. }))
472                    .unwrap_or(false) =>
473            {
474                tile
475            }
476            _ => ins
477                .first()
478                .map(|b| {
479                    plane
480                        .get(&template_of(p.buffer(*b)))
481                        .copied()
482                        .unwrap_or(tile)
483                })
484                .unwrap_or(tile),
485        };
486        plane.insert(template_of(p.buffer(*dst)), w);
487    }
488    let width = |b: BufferId| {
489        plane
490            .get(&template_of(p.buffer(b)))
491            .copied()
492            .unwrap_or_else(|| p.buffer(b).kind.page_tiles() * tile)
493    };
494    // Placement gives every site its own instance of a buffer; the golden is ONE host-side reference
495    // for the whole matrix, so it is keyed on the template every instance came from.
496    let gvar = |b: BufferId| format!("g_{}", ident(&template_name(p, p.buffer(b))));
497    let host_of = |b: BufferId| -> Option<(String, usize, usize)> {
498        let t = from_tensor.get(&b)?;
499        let name = ident(&p.tensor(*t).name);
500        ids.iter()
501            .find(|(id, _, _)| *id == name)
502            .map(|(id, rp, cp)| (format!("{id}_host"), *rp, *cp))
503    };
504    let mut o = Vec::new();
505    let mut declared: std::collections::BTreeSet<String> = Default::default();
506    let declare = |o: &mut Vec<String>,
507                   declared: &mut std::collections::BTreeSet<String>,
508                   b: BufferId| {
509        let (v, w) = (gvar(b), width(b));
510        if !declared.insert(v.clone()) {
511            return;
512        }
513        o.push(format!("std::vector<double> {v}(Mp * {w}u, 0.0);"));
514        if let Some((host, rp, cp)) = host_of(b) {
515            // A per-column weight is ONE tile-row tall however many rows the output has, so the copy
516            // is bounded by the source tensor's own height, not by the output's. Running to `Mp` here
517            // reads past the end of a 1×cols operand the moment the matrix is taller than one tile.
518            o.push(format!(
519                "for (uint32_t r = 0; r < Mp && r < {rp}u; ++r) for (uint32_t c = 0; c < {w}u && c < {cp}u; ++c) {v}[r * {w}u + c] = static_cast<double>({host}[r * {cp}u + c]);"
520            ));
521        }
522    };
523    for (op, ins, dst) in &steps {
524        for b in ins {
525            declare(&mut o, &mut declared, *b);
526        }
527        declare(&mut o, &mut declared, *dst);
528        let (d, wd) = (gvar(*dst), width(*dst));
529        match op {
530            ComputeOp::Generic {
531                iters,
532                ins: gmaps,
533                body,
534                ..
535            } => {
536                if iters.len() != 2 {
537                    return Err(SpatialLowerError::Unsupported("a golden for a Generic with a contraction axis (LV_GEN_K, a third folded iteration variable)"));
538                }
539                let (row_ax, col_ax) = (iters[0], iters[1]);
540                // The golden spells the four tile-aligned broadcast forms and the column fold — the
541                // same set the compute kernel prints. A row-axis fold, or a map with an offset, a
542                // stride or an indirect term, is refused here in the same breath the kernel refuses it.
543                if matches!(row_ax.kind, IterKind::Reduce { .. }) {
544                    return Err(SpatialLowerError::Unsupported(
545                        "a golden for a Generic reduction over the row axis on Tensix",
546                    ));
547                }
548                for (i, m) in gmaps.iter().enumerate() {
549                    let width = ins
550                        .get(i)
551                        .map(|b| p.buffer(*b).kind.page_tiles() * tile)
552                        .unwrap_or(tile);
553                    if m.broadcast_class(row_ax.var, col_ax.var, width).is_none() {
554                        return Err(SpatialLowerError::Unsupported("a golden for a Generic access map with an offset, a stride or an indirect term on Tensix"));
555                    }
556                }
557                // Each operand's coordinate, with an absent axis reading as constant — the same rule
558                // `spatial/eval.rs` applies, written out in C++ so the two cannot drift.
559                let coord = |i: usize, ax: IterSpec, v: &str| -> String {
560                    match gmaps.get(i).map(|m| m.varies_along(ax.var)).unwrap_or(true) {
561                        true => v.to_string(),
562                        false => "0".to_string(),
563                    }
564                };
565                let expr = |b: &TensorBody| -> Result<String, SpatialLowerError> {
566                    fn go(
567                        b: &TensorBody,
568                        ins: &[BufferId],
569                        gv: &dyn Fn(BufferId) -> String,
570                        wd: &dyn Fn(BufferId) -> usize,
571                        rc: &dyn Fn(usize) -> (String, String),
572                    ) -> Result<String, SpatialLowerError> {
573                        Ok(match b {
574                            TensorBody::Operand(i) => {
575                                let buf = *ins.get(*i).ok_or(SpatialLowerError::Unsupported(
576                                    "a Generic body naming an absent operand",
577                                ))?;
578                                let (r, c) = rc(*i);
579                                format!("{}[({r}) * {}u + ({c})]", gv(buf), wd(buf))
580                            }
581                            TensorBody::Const(bits) => {
582                                format!("({})", cpp_float(f64::from_bits(*bits), ""))
583                            }
584                            TensorBody::Min(..)
585                            | TensorBody::Max(..)
586                            | TensorBody::FloorDiv(..)
587                            | TensorBody::Shr { .. } => {
588                                return Err(SpatialLowerError::Unsupported(
589                                    "an integer model's min, max, floor division or arithmetic shift on the Tensix host — it prints none of them",
590                                ))
591                            }
592                            TensorBody::Add(x, y) => format!(
593                                "({} + {})",
594                                go(x, ins, gv, wd, rc)?,
595                                go(y, ins, gv, wd, rc)?
596                            ),
597                            TensorBody::Sub(x, y) => format!(
598                                "({} - {})",
599                                go(x, ins, gv, wd, rc)?,
600                                go(y, ins, gv, wd, rc)?
601                            ),
602                            TensorBody::Mul(x, y) => format!(
603                                "({} * {})",
604                                go(x, ins, gv, wd, rc)?,
605                                go(y, ins, gv, wd, rc)?
606                            ),
607                            TensorBody::Unary { kind, arg } => {
608                                let a = go(arg, ins, gv, wd, rc)?;
609                                match kind {
610                                    SfpuKind::Exp => format!("std::exp({a})"),
611                                    SfpuKind::Recip => format!("(1.0 / ({a}))"),
612                                    SfpuKind::Rsqrt { eps_nano, over } => format!(
613                                        "(1.0 / std::sqrt(({a}) / {}.0 + {:e}))",
614                                        (*over).max(1),
615                                        *eps_nano as f64 * 1e-9
616                                    ),
617                                    SfpuKind::Silu => format!("(({a}) / (1.0 + std::exp(-({a}))))"),
618                                    SfpuKind::Relu => format!("std::fmax(0.0, {a})"),
619                                    SfpuKind::Ln => {
620                                        return Err(SpatialLowerError::Unsupported(
621                                            "a golden for a natural-log transform on Tensix",
622                                        ))
623                                    }
624                                }
625                            }
626                            TensorBody::Select { .. } => {
627                                return Err(SpatialLowerError::Unsupported(
628                                    "a golden for a piecewise Select body on Tensix",
629                                ))
630                            }
631                        })
632                    }
633                    let rc = |i: usize| (coord(i, row_ax, "r"), coord(i, col_ax, "c"));
634                    go(b, ins, &|b| gvar(b), &|b| width(b), &rc)
635                };
636                let e = expr(body)?;
637                if let IterKind::Reduce { pool } = col_ax.kind {
638                    let (init, step) = match pool {
639                        ReducePool::Sum => ("0.0", "acc += v;"),
640                        ReducePool::Max => ("-INFINITY", "acc = v > acc ? v : acc;"),
641                        ReducePool::ArgMax => {
642                            return Err(SpatialLowerError::Unsupported(
643                                "a golden for an index-valued ArgMax reduction on Tensix",
644                            ))
645                        }
646                    };
647                    o.push("for (uint32_t r = 0; r < Mp; ++r) {".into());
648                    o.push(format!("    double acc = {init};"));
649                    o.push(format!(
650                        "    for (uint32_t c = 0; c < {}u; ++c) {{ const double v = {e}; {step} }}",
651                        col_ax.extent
652                    ));
653                    o.push(format!(
654                        "    for (uint32_t c = 0; c < {wd}u; ++c) {d}[r * {wd}u + c] = acc;"
655                    ));
656                    o.push("}".into());
657                } else {
658                    o.push(format!(
659                        "for (uint32_t r = 0; r < Mp; ++r) for (uint32_t c = 0; c < {wd}u; ++c)"
660                    ));
661                    o.push(format!("    {d}[r * {wd}u + c] = {e};"));
662                }
663            }
664            // Refused in the same breath the compute kernel refuses them, so a program can never
665            // reach silicon with a kernel the golden cannot check.
666            ComputeOp::MatmulTiles { .. } => {
667                return Err(SpatialLowerError::Unsupported(
668                    "a matmul whose shapes are not a GEMM",
669                ))
670            }
671            ComputeOp::Requant { .. } => {
672                return Err(SpatialLowerError::Unsupported(
673                    "a golden for a requant epilogue on Tensix (P4b)",
674                ))
675            }
676            ComputeOp::Bias { .. } => {
677                return Err(SpatialLowerError::Unsupported(
678                    "a golden for a bias epilogue on Tensix (P4)",
679                ))
680            }
681            ComputeOp::Relu => {
682                return Err(SpatialLowerError::Unsupported(
683                    "a golden for relu on Tensix (P4)",
684                ))
685            }
686            ComputeOp::Scalar(_) => {
687                return Err(SpatialLowerError::Unsupported(
688                    "a golden for a scalar fabric body on Tensix (P4)",
689                ))
690            }
691        }
692    }
693    let (last, wl) = {
694        let (_, _, d) = steps.last().unwrap();
695        (gvar(*d), width(*d))
696    };
697    o.push(format!("for (uint32_t r = 0; r < M; ++r) for (uint32_t c = 0; c < N; ++c) golden[r * Np + c] = static_cast<float>({last}[r * {wl}u + c]);"));
698    Ok(o)
699}
700
701pub fn print_host(
702    p: &SpatialProgram,
703    target: &SpatialTarget,
704    cbs: &CbPlan,
705    classes: &[KernelClass],
706    layout: &RuntimeArgLayout,
707    exact_f32: bool,
708    fidelity: Fidelity,
709    cfg_line: &str,
710) -> Result<String, SpatialLowerError> {
711    let pl = p.placement.as_ref().unwrap();
712    let n_sites = pl.items_of_site.len();
713    let inputs: Vec<&TensorDecl> = p
714        .tensors
715        .iter()
716        .filter(|t| t.role != TensorRole::Output)
717        .collect();
718    let out = p
719        .output_tensor()
720        .ok_or(SpatialLowerError::Unsupported("an output tensor"))?;
721    if inputs.is_empty() {
722        return Err(SpatialLowerError::Unsupported(
723            "a host for a program with at least one input tensor",
724        ));
725    }
726    let tile = target.tile.rows;
727    // ★ THE GEMM TRIPLE KEEPS ITS EXACT VOCABULARY. Every row of the ttsim ledger is keyed on the
728    // strings this host prints — `LOGOS_MM_CFG`, the M/N/K constants, the golden's shape — and
729    // `spatial_cost_model_lock` re-derives the plan from them. So a GEMM prints byte-for-byte what
730    // it printed before P4 widened this function, and everything else takes the general path.
731    let a = inputs[0];
732    let gemm = inputs.len() == 2
733        && inputs[1].elems.rows == a.elems.cols
734        && out.elems.rows == a.elems.rows
735        && out.elems.cols == inputs[1].elems.cols;
736    let (m, k, n) = if gemm {
737        (a.elems.rows, a.elems.cols, inputs[1].elems.cols)
738    } else {
739        // Every non-GEMM op this printer lowers writes its output element-for-element, so the
740        // output's own geometry is the loop bound and `K` is only the operand width.
741        (out.elems.rows, a.elems.cols, out.elems.cols)
742    };
743    let (mp, kp, np) = if gemm {
744        (
745            a.tiles.rows_t * tile,
746            a.tiles.cols_t * tile,
747            inputs[1].tiles.cols_t * tile,
748        )
749    } else {
750        (
751            out.tiles.rows_t * tile,
752            a.tiles.cols_t * tile,
753            out.tiles.cols_t * tile,
754        )
755    };
756    let out_f32 = out.dtype == Precision::Fp32;
757    let floor = if p.claim == NumericsClaim::Exact {
758        "0.9999"
759    } else {
760        "0.99"
761    };
762    let all_sites: std::collections::BTreeSet<Site> = p.sites();
763    let all_rects = rect_decompose(&all_sites);
764    let mut h = String::new();
765    let w = |h: &mut String, s: &str| {
766        h.push_str(s);
767        h.push('\n');
768    };
769    w(&mut h, &format!("// host.cpp — printed from the spatial IR ({}). Every semaphore, circular buffer and kernel below is the", p.name));
770    w(
771        &mut h,
772        "// image of one IR object; the plan it runs is the emergent one the passes chose.",
773    );
774    for inc in [
775        "<tt-metalium/host_api.hpp>",
776        "<tt-metalium/constants.hpp>",
777        "<tt-metalium/bfloat16.hpp>",
778        "<tt-metalium/tilize_utils.hpp>",
779        "<tt-metalium/device.hpp>",
780        "<tt-metalium/tt_metal.hpp>",
781        "<tt-metalium/tensor_accessor_args.hpp>",
782        "<tt-metalium/distributed.hpp>",
783        "<fmt/core.h>",
784        "<cmath>",
785        "<cstdint>",
786        "<cstdlib>",
787        "<string>",
788        "<vector>",
789    ] {
790        w(&mut h, &format!("#include {inc}"));
791    }
792    w(&mut h, "using namespace tt;");
793    w(&mut h, "using namespace tt::tt_metal;");
794    w(&mut h, "");
795    w(
796        &mut h,
797        "static float logos_pcc(const std::vector<float>& x, const std::vector<float>& y) {",
798    );
799    w(&mut h, "    double mx = 0, my = 0;");
800    w(
801        &mut h,
802        "    for (size_t i = 0; i < x.size(); ++i) { mx += x[i]; my += y[i]; }",
803    );
804    w(
805        &mut h,
806        "    mx /= (double)x.size(); my /= (double)y.size();",
807    );
808    w(&mut h, "    double sxy = 0, sxx = 0, syy = 0;");
809    w(&mut h, "    for (size_t i = 0; i < x.size(); ++i) { const double dx = x[i] - mx, dy = y[i] - my; sxy += dx * dy; sxx += dx * dx; syy += dy * dy; }");
810    w(
811        &mut h,
812        "    if (sxx == 0 || syy == 0) return (sxx == 0 && syy == 0 && x == y) ? 1.0f : 0.0f;",
813    );
814    w(
815        &mut h,
816        "    return static_cast<float>(sxy / std::sqrt(sxx * syy));",
817    );
818    w(&mut h, "}");
819    w(&mut h, "");
820    w(&mut h, "int main() {");
821    // The contraction extent exists only for a GEMM. tt-metal builds with `-Werror -Wunused-variable`,
822    // so declaring a `K` nothing reads is not a harmless extra line — it fails the build.
823    if gemm {
824        w(
825            &mut h,
826            &format!("    constexpr uint32_t M = {m}, N = {n}, K = {k};"),
827        );
828        w(
829            &mut h,
830            &format!("    constexpr uint32_t Mp = {mp}, Np = {np}, Kp = {kp};"),
831        );
832    } else {
833        w(&mut h, &format!("    constexpr uint32_t M = {m}, N = {n};"));
834        w(
835            &mut h,
836            &format!("    constexpr uint32_t Mp = {mp}, Np = {np};"),
837        );
838    }
839    w(
840        &mut h,
841        "    auto mesh_device = distributed::MeshDevice::create_unit_mesh(0);",
842    );
843    let (px, py) = placed_extent(pl);
844    w(
845        &mut h,
846        "    const CoreCoord device_grid = mesh_device->compute_with_storage_grid_size();",
847    );
848    w(
849        &mut h,
850        &format!("    constexpr uint32_t PLACED_X = {px}, PLACED_Y = {py};"),
851    );
852    w(
853        &mut h,
854        "    if (device_grid.x < PLACED_X || device_grid.y < PLACED_Y) {",
855    );
856    w(&mut h, "        fmt::print(\"LOGOS_GRID_MISMATCH device={}x{} placed={}x{}\\n\", device_grid.x, device_grid.y, PLACED_X, PLACED_Y);");
857    w(&mut h, "        mesh_device->close();");
858    w(&mut h, "        return 2;");
859    w(&mut h, "    }");
860    w(&mut h, "    fmt::print(\"LOGOS_GRID device={}x{} placed={}x{}\\n\", device_grid.x, device_grid.y, PLACED_X, PLACED_Y);");
861    w(
862        &mut h,
863        "    distributed::MeshCommandQueue& cq = mesh_device->mesh_command_queue();",
864    );
865    w(
866        &mut h,
867        "    auto device_range = distributed::MeshCoordinateRange(mesh_device->shape());",
868    );
869    w(&mut h, "    Program program{};");
870    w(
871        &mut h,
872        &format!("    fmt::print(\"{}\\n\");", cfg_line.replace('"', "\\\"")),
873    );
874    w(&mut h, "");
875    // Tensors → DRAM buffers.
876    for t in &p.tensors {
877        let id = ident(&t.name);
878        let tb = target.tile_bytes(t.dtype);
879        w(&mut h, &format!("    distributed::DeviceLocalBufferConfig dram_{id}{{.page_size = {tb}, .buffer_type = BufferType::DRAM}};"));
880        w(
881            &mut h,
882            &format!(
883                "    distributed::ReplicatedBufferConfig cfg_{id}{{.size = {}}};",
884                tb * t.tiles.count()
885            ),
886        );
887        w(&mut h, &format!("    auto buf_{id} = distributed::MeshBuffer::create(cfg_{id}, dram_{id}, mesh_device.get());"));
888    }
889    // Device-cycle scratch: one word per site, written by each site's output writer and read back below.
890    // Sized from the placement rather than the mesh so a partially-used grid does not read uninitialised words.
891    // One 32-byte page per site rather than one word: DRAM transfers are 32-byte aligned, so a four-byte
892    // page would be a misaligned write rather than a compact one. Only the first word of each page is used.
893    w(&mut h, &format!("    distributed::DeviceLocalBufferConfig dram_cycles{{.page_size = 32, .buffer_type = BufferType::L1}};"));
894    w(
895        &mut h,
896        &format!(
897            "    distributed::ReplicatedBufferConfig cfg_cycles{{.size = {}}};",
898            64 * n_sites.max(1)
899        ),
900    );
901    w(&mut h, "    auto buf_cycles = distributed::MeshBuffer::create(cfg_cycles, dram_cycles, mesh_device.get());");
902    w(&mut h, "");
903    // Buffers → circular buffers (one per template, on the sites that instantiate it). A template that shares
904    // another's memory is an index of that template's config, not a config of its own.
905    for s in cbs.slots.iter().filter(|s| s.alias_of.is_none()) {
906        let sites: std::collections::BTreeSet<Site> = s.sites.iter().copied().collect();
907        let rects = if sites.is_empty() {
908            all_rects.clone()
909        } else {
910            rect_decompose(&sites)
911        };
912        let indices: Vec<&super::cb::CbSlot> = std::iter::once(s)
913            .chain(cbs.slots.iter().filter(|a| a.alias_of == Some(s.template)))
914            .collect();
915        let total = indices.iter().map(|x| x.total_bytes()).max().unwrap_or(0);
916        let formats = indices
917            .iter()
918            .map(|x| Ok(format!("{{{}, {}}}", x.index, data_format(x.dtype)?)))
919            .collect::<Result<Vec<_>, SpatialLowerError>>()?
920            .join(", ");
921        let pages: String = indices
922            .iter()
923            .map(|x| {
924                format!(
925                    " cb_{}.set_page_size({}, {});",
926                    s.name, x.index, x.tile_bytes
927                )
928            })
929            .collect();
930        w(&mut h, &format!("    CircularBufferConfig cb_{0}({1}, {{{2}}});{3} CreateCircularBuffer(program, {4}, cb_{0});", s.name, total, formats, pages, ranges_cpp(&rects)));
931    }
932    w(&mut h, "");
933    // Semaphores.
934    for s in &p.semaphores {
935        let sites: std::collections::BTreeSet<Site> = s.sites.iter().copied().collect();
936        let rects = if sites.is_empty() {
937            all_rects.clone()
938        } else {
939            rect_decompose(&sites)
940        };
941        w(
942            &mut h,
943            &format!(
944                "    auto sem_{} = CreateSemaphore(program, {}, {});",
945                s.id.idx(),
946                ranges_cpp(&rects),
947                s.init
948            ),
949        );
950    }
951    w(&mut h, "");
952    // Kernels: one source per class, one `CreateKernel` per NOC launch of it.
953    for (i, c) in classes.iter().enumerate() {
954        match c.engine {
955            Engine::Movement(e) => {
956                // Compile-time args: the tensor accessors of every tensor the kernel touches, in tensor-id order.
957                let mut tensors: Vec<TensorId> = Vec::new();
958                let rep = &p.schedule.as_ref().unwrap().programs[c.representative];
959                let ops: Vec<ChoreoOp> = rep.ops.iter().map(|t| t.op.clone()).collect();
960                walk_ops(&ops, 1, &mut |op, _| {
961                    if let ChoreoOp::ReadTiles { tensor, .. }
962                    | ChoreoOp::WriteTiles { tensor, .. } = op
963                    {
964                        if !tensors.contains(tensor) {
965                            tensors.push(*tensor);
966                        }
967                    }
968                });
969                tensors.sort();
970                let mut line = format!("    std::vector<uint32_t> ca_{i};");
971                for t in &tensors {
972                    line.push_str(&format!(
973                        " TensorAccessorArgs(*buf_{}).append_to(ca_{i});",
974                        ident(&p.tensor(*t).name)
975                    ));
976                }
977                // Appended AFTER every tensor, so the accessor offsets the kernels already compute are
978                // untouched and only the new one takes the final slot.
979                if super::args::stamps_cycles(&ops) {
980                    line.push_str(&format!(
981                        " TensorAccessorArgs(*buf_cycles).append_to(ca_{i});"
982                    ));
983                }
984                w(&mut h, &line);
985                for (j, l) in c.launches.iter().enumerate() {
986                    let noc = l.noc.map(|n| n.0).unwrap_or(0);
987                    w(&mut h, &format!(
988                        "    auto k_{i}_{j} = CreateKernel(program, LOGOS_KERNEL_DIR \"{}\", {}, DataMovementConfig{{.processor = DataMovementProcessor::RISCV_{e}, .noc = NOC::RISCV_{noc}_default, .compile_args = ca_{i}}});",
989                        c.file_name,
990                        ranges_cpp(&l.rects)
991                    ));
992                }
993            }
994            Engine::Compute => {
995                for (j, l) in c.launches.iter().enumerate() {
996                    w(&mut h, &format!(
997                        "    auto k_{i}_{j} = CreateKernel(program, LOGOS_KERNEL_DIR \"{}\", {}, ComputeConfig{{.math_fidelity = {}, .fp32_dest_acc_en = {}, .unpack_to_dest_mode = {}, .compile_args = {{}}}});",
998                        c.file_name,
999                        ranges_cpp(&l.rects),
1000                        fidelity_cpp(fidelity),
1001                        if exact_f32 { "true" } else { "false" },
1002                        unpack_to_dest_cpp(p, cbs, exact_f32)
1003                    ));
1004                }
1005            }
1006        }
1007    }
1008    w(&mut h, "");
1009    w(&mut h, "    auto P = [&](uint32_t x, uint32_t y) { return mesh_device->worker_core_from_logical_core(CoreCoord{x, y}); };");
1010    w(&mut h, "    (void)P;");
1011    // Runtime args: per (site, launch) from the layout.
1012    for (i, c) in classes.iter().enumerate() {
1013        let slots = &layout
1014            .per_kernel
1015            .iter()
1016            .find(|(n, _)| *n == c.file_name)
1017            .map(|(_, s)| s.clone())
1018            .unwrap_or_default();
1019        for (j, l) in c.launches.iter().enumerate() {
1020            let noc = l.noc.map(|n| n.0).unwrap_or(0);
1021            for site in &l.sites {
1022                let vals: Result<Vec<String>, _> = slots
1023                    .iter()
1024                    .map(|s| arg_value(p, *site, &s.source, noc))
1025                    .collect();
1026                w(&mut h, &format!("    SetRuntimeArgs(program, k_{i}_{j}, CoreCoord{{{}, {}}}, std::vector<uint32_t>{{{}}});", site.x, site.y, vals?.join(", ")));
1027            }
1028        }
1029    }
1030    let _ = pl;
1031    w(&mut h, "");
1032    // Inputs (padded to whole tiles), golden, tilize, upload.
1033    let idc = ident(&out.name);
1034    if gemm {
1035        let (ida, idb) = (ident(&a.name), ident(&inputs[1].name));
1036        w(
1037            &mut h,
1038            &format!("    std::vector<bfloat16> {ida}_host(Mp * Kp, bfloat16(0.0f));"),
1039        );
1040        w(
1041            &mut h,
1042            &format!("    std::vector<bfloat16> {idb}_host(Kp * Np, bfloat16(0.0f));"),
1043        );
1044        for l in fill_lines(p, a, &format!("{ida}_host"), kp)? {
1045            w(&mut h, &format!("    {l}"));
1046        }
1047        for l in fill_lines(p, inputs[1], &format!("{idb}_host"), np)? {
1048            w(&mut h, &format!("    {l}"));
1049        }
1050        w(&mut h, "    std::vector<float> golden(Mp * Np, 0.0f);");
1051        w(
1052            &mut h,
1053            "    for (uint32_t i = 0; i < M; ++i) for (uint32_t kk = 0; kk < K; ++kk) {",
1054        );
1055        w(
1056            &mut h,
1057            &format!("        const float av = static_cast<float>({ida}_host[i * Kp + kk]);"),
1058        );
1059        w(&mut h, &format!("        for (uint32_t j = 0; j < N; ++j) golden[i * Np + j] += av * static_cast<float>({idb}_host[kk * Np + j]);"));
1060        w(&mut h, "    }");
1061        w(
1062            &mut h,
1063            &format!("    auto {ida}_tiled = tilize_nfaces({ida}_host, Mp, Kp);"),
1064        );
1065        w(
1066            &mut h,
1067            &format!("    auto {idb}_tiled = tilize_nfaces({idb}_host, Kp, Np);"),
1068        );
1069        w(
1070            &mut h,
1071            &format!("    distributed::EnqueueWriteMeshBuffer(cq, buf_{ida}, {ida}_tiled, false);"),
1072        );
1073        w(
1074            &mut h,
1075            &format!("    distributed::EnqueueWriteMeshBuffer(cq, buf_{idb}, {idb}_tiled, false);"),
1076        );
1077    } else {
1078        // ★ N INPUTS, EACH IN ITS OWN GEOMETRY. A weighted row op carries three tensors and its
1079        // weight is a 1×cols row, so a single padded shape cannot describe them all: every tensor
1080        // is filled, tilized and uploaded at the width its OWN tile count declares.
1081        let mut ids: Vec<(String, usize, usize)> = Vec::new();
1082        for t in &inputs {
1083            let id = ident(&t.name);
1084            let ety = host_elem_type(t.dtype)?;
1085            let (rp, cp) = (t.tiles.rows_t * tile, t.tiles.cols_t * tile);
1086            w(
1087                &mut h,
1088                &format!("    std::vector<{ety}> {id}_host({rp}u * {cp}u, {ety}(0.0f));"),
1089            );
1090            for l in fill_lines(p, t, &format!("{id}_host"), cp)? {
1091                w(&mut h, &format!("    {l}"));
1092            }
1093            ids.push((id, rp, cp));
1094        }
1095        w(&mut h, "    std::vector<float> golden(Mp * Np, 0.0f);");
1096        for l in golden_lines(p, &ids, tile)? {
1097            w(&mut h, &format!("    {l}"));
1098        }
1099        for (id, rp, cp) in &ids {
1100            w(
1101                &mut h,
1102                &format!("    auto {id}_tiled = tilize_nfaces({id}_host, {rp}u, {cp}u);"),
1103            );
1104            w(
1105                &mut h,
1106                &format!(
1107                    "    distributed::EnqueueWriteMeshBuffer(cq, buf_{id}, {id}_tiled, false);"
1108                ),
1109            );
1110        }
1111    }
1112    w(&mut h, "    distributed::MeshWorkload workload; workload.add_program(device_range, std::move(program));");
1113    w(
1114        &mut h,
1115        "    distributed::EnqueueMeshWorkload(cq, workload, false);",
1116    );
1117    if out_f32 {
1118        w(
1119            &mut h,
1120            &format!("    std::vector<float> {idc}_tiled(Mp * Np);"),
1121        );
1122        w(
1123            &mut h,
1124            &format!("    distributed::EnqueueReadMeshBuffer(cq, {idc}_tiled, buf_{idc}, true);"),
1125        );
1126        w(
1127            &mut h,
1128            &format!("    auto {idc}_host = untilize_nfaces({idc}_tiled, Mp, Np);"),
1129        );
1130        w(&mut h, &format!("    std::vector<float> got(M * N); for (uint32_t i = 0; i < M; ++i) for (uint32_t j = 0; j < N; ++j) got[i * N + j] = {idc}_host[i * Np + j];"));
1131    } else {
1132        w(
1133            &mut h,
1134            &format!("    std::vector<bfloat16> {idc}_tiled(Mp * Np, bfloat16(0.0f));"),
1135        );
1136        w(
1137            &mut h,
1138            &format!("    distributed::EnqueueReadMeshBuffer(cq, {idc}_tiled, buf_{idc}, true);"),
1139        );
1140        w(
1141            &mut h,
1142            &format!("    auto {idc}_host = untilize_nfaces({idc}_tiled, Mp, Np);"),
1143        );
1144        w(&mut h, &format!("    std::vector<float> got(M * N); for (uint32_t i = 0; i < M; ++i) for (uint32_t j = 0; j < N; ++j) got[i * N + j] = static_cast<float>({idc}_host[i * Np + j]);"));
1145    }
1146    w(&mut h, "    std::vector<float> want(M * N); for (uint32_t i = 0; i < M; ++i) for (uint32_t j = 0; j < N; ++j) want[i * N + j] = golden[i * Np + j];");
1147    w(
1148        &mut h,
1149        "    long long checksum = 0; for (float v : got) checksum += (long long)std::llround(v);",
1150    );
1151    w(&mut h, "    long long want_checksum = 0; for (float v : want) want_checksum += (long long)std::llround(v);");
1152    w(&mut h, "    const float pcc = logos_pcc(want, got);");
1153    w(
1154        &mut h,
1155        "    fmt::print(\"LOGOS_CHECKSUM {} golden {}\\n\", checksum, want_checksum);",
1156    );
1157    w(
1158        &mut h,
1159        &format!(
1160            "    fmt::print(\"LOGOS spatial-IR matmul ({}) -- PCC = {{}}\\n\", pcc);",
1161            target.name
1162        ),
1163    );
1164    let exact_claim = p.claim == NumericsClaim::Exact;
1165    let ok = if exact_claim {
1166        format!("(pcc >= {floor}f && checksum == want_checksum)")
1167    } else {
1168        format!("(pcc >= {floor}f)")
1169    };
1170    w(&mut h, &format!("    const bool ok = {ok};"));
1171    w(&mut h, &format!("    if (ok) fmt::print(\"LOGOS_MM_PASS fidelity={} fp32={} l1acc={} pcc={{}} floor={floor}\\n\", pcc);", fidelity_label(fidelity), if exact_f32 { 1 } else { 0 }, p.work.plan.map(|pl| pl.pack_accumulate).unwrap_or(false) as u8));
1172    w(&mut h, "    else fmt::print(\"LOGOS_MM_FAIL pcc={} checksum={} golden={}\\n\", pcc, checksum, want_checksum);");
1173    w(
1174        &mut h,
1175        &format!("    std::vector<uint32_t> cyc({});", 16 * n_sites.max(1)),
1176    );
1177    w(
1178        &mut h,
1179        "    distributed::EnqueueReadMeshBuffer(cq, cyc, buf_cycles, true);",
1180    );
1181    // The MAX, not the mean: the slowest core is the program's length, and an average would hide exactly the
1182    // imbalance between engines that the whole plan search is trying to remove.
1183    w(&mut h, "    uint32_t device_cycles = 0; for (size_t i = 0; i < cyc.size(); i += 8) device_cycles = std::max(device_cycles, cyc[i]);");
1184    w(
1185        &mut h,
1186        "    fmt::print(\"LOGOS_MM_DEVICE_CYCLES={}\\n\", device_cycles);",
1187    );
1188    w(&mut h, "    mesh_device->close();");
1189    w(&mut h, "    return ok ? 0 : 1;");
1190    w(&mut h, "}");
1191    Ok(h)
1192}
1193
1194pub fn print_cmake() -> String {
1195    [
1196        "cmake_minimum_required(VERSION 3.22...4.2)",
1197        "project(metal_example_logos_spatial)",
1198        "add_executable(metal_example_logos_spatial host.cpp)",
1199        "if(NOT TARGET TT::Metalium)",
1200        "    find_package(TT-Metalium REQUIRED)",
1201        "endif()",
1202        "target_link_libraries(metal_example_logos_spatial PUBLIC TT::Metalium)",
1203        "target_compile_definitions(metal_example_logos_spatial PRIVATE LOGOS_KERNEL_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/kernels/\")",
1204        "if(TARGET TT::CommonPCH)",
1205        "    tt_reuse_precompile_headers(metal_example_logos_spatial TT::CommonPCH)",
1206        "endif()",
1207        "",
1208    ]
1209    .join("\n")
1210}