Skip to main content

logicaffeine_compile/spatial/
pipeline.rs

1//! The front-end bridge and the pinned pass order.
2
3use super::ir::*;
4#[allow(unused_imports)]
5use super::ir::{BlockPlan, EnginePacking};
6use super::link::{dense_tables, link, Form, Prelude};
7use super::passes::from_tensor_op::{from_matmul, MatmulNames};
8use super::passes::recognize_stages::{float_tensors, links, state_tensors, Link, TensorOpSpec};
9use super::passes::{
10    fuse_generic, fuse_readers, persist, place, size_buffers, synthesize_handshakes, tile, verify,
11    SpatialReject,
12};
13use super::target::SpatialTarget;
14use crate::codegen_sva::fabric_target::Precision;
15use crate::tensor_graph::graph::{Lifted, Op, Storage, TensorGraph};
16use crate::tensor_recognize::{
17    recognize_elementwise, recognize_fill, recognize_fused_row, recognize_matmul, recognize_swiglu,
18    Fill, MatmulSpec,
19};
20
21/// A pass: a pure function of the program and its target.
22pub type Pass = fn(&SpatialProgram, &SpatialTarget) -> Result<SpatialProgram, SpatialReject>;
23
24/// The pinned order. Every entry is idempotent on its own output and leaves `validate()` holding.
25pub const PASS_ORDER: &[(&str, Pass)] = &[
26    // ★ FUSION RUNS FIRST, ON THE TEMPLATE. Legalization split a workload into stages so the split
27    // could be CHECKED; this puts back together whatever the destination registers can carry, before
28    // any of it is tiled, placed or scheduled. Removing a stage here costs only a dense renumber.
29    ("fuse_generic", fuse_generic::fuse_generic),
30    ("tile", tile::tile),
31    ("place", place::place),
32    ("fuse_readers", fuse_readers::fuse_readers),
33    ("size_buffers", size_buffers::size_buffers),
34    (
35        "synthesize_handshakes",
36        synthesize_handshakes::synthesize_handshakes,
37    ),
38    ("persist", persist::persist),
39    ("verify", verify::verify),
40];
41
42/// ★ A LOWERING COMPUTES ONLY IN DATATYPES ITS CHIP HAS. Every buffer a `Generic` stage reads or writes, every
43/// index an indirect tile address waits on, and every tensor moved into or out of one of those buffers must
44/// be a precision the target's elementwise datapath computes in (or its fp32 accumulation width). Without
45/// this an index lowered for an RDU rode `Int64` — a type the PCU does not have — and evaluated exactly,
46/// because nothing asked the chip. It gates what the COMPILER chooses — every program [`super::link::link`] returns; a
47/// program built by hand is its author's claim, checked by the passes as it stands.
48pub fn check_datatypes(p: &SpatialProgram, target: &SpatialTarget) -> Result<(), SpatialReject> {
49    let mut gated: std::collections::BTreeSet<BufferId> = Default::default();
50    for s in &p.stages {
51        match &s.kind {
52            StageKind::Compute(ComputeOp::Generic { .. }) => {
53                gated.extend(s.ins.iter().chain(&s.outs).copied())
54            }
55            StageKind::Load { pattern, .. } | StageKind::Store { pattern, .. } => {
56                gated.extend(pattern.origin.indirect.map(|i| i.index))
57            }
58            _ => {}
59        }
60    }
61    let refuse = |needed: Precision| {
62        Err(SpatialReject::MissingPrecision {
63            needed,
64            target: target.name,
65        })
66    };
67    for b in &gated {
68        let d = p.buffer(*b).dtype;
69        if !target.generic_holds(d) {
70            return refuse(d);
71        }
72    }
73    for s in &p.stages {
74        let moved = match &s.kind {
75            StageKind::Load { tensor, .. } if s.outs.iter().any(|b| gated.contains(b)) => {
76                Some(*tensor)
77            }
78            StageKind::Store { tensor, .. } if s.ins.first().is_some_and(|b| gated.contains(b)) => {
79                Some(*tensor)
80            }
81            _ => None,
82        };
83        if let Some(t) = moved {
84            let d = p.tensor(t).dtype;
85            if !target.generic_holds(d) {
86                return refuse(d);
87            }
88        }
89    }
90    Ok(())
91}
92
93/// Run the pinned passes to a verified program.
94pub fn run_passes(
95    p: SpatialProgram,
96    target: &SpatialTarget,
97) -> Result<SpatialProgram, SpatialReject> {
98    run_pass_list(p, target, PASS_ORDER)
99}
100
101/// The passes through `tile`: a template fused and planned, nothing placed — where two planned sections are joined
102/// ([`super::passes::forward`]) before the rest run on the joined one.
103pub fn run_passes_through_tile(p: SpatialProgram, target: &SpatialTarget) -> Result<SpatialProgram, SpatialReject> {
104    let at = PASS_ORDER.iter().position(|(n, _)| *n == "tile").expect("a tiling pass");
105    run_pass_list(p, target, &PASS_ORDER[..=at])
106}
107
108/// The passes after `tile`, ending with the certifying one or before it, as [`run_passes`] and
109/// [`run_passes_uncertified`] end.
110fn run_passes_after_tile(p: SpatialProgram, target: &SpatialTarget, certify: bool) -> Result<SpatialProgram, SpatialReject> {
111    let at = PASS_ORDER.iter().position(|(n, _)| *n == "tile").expect("a tiling pass");
112    let end = if certify { PASS_ORDER.len() } else { PASS_ORDER.len() - 1 };
113    run_pass_list(p, target, &PASS_ORDER[at + 1..end])
114}
115
116/// Every pass but the last, which certifies: the program tiled, placed, sized and scheduled — a SECTION of a
117/// composition, which is certified as part of the composed program (`compose` drops a section's own certificate).
118pub fn run_passes_uncertified(
119    p: SpatialProgram,
120    target: &SpatialTarget,
121) -> Result<SpatialProgram, SpatialReject> {
122    let (last, before) = PASS_ORDER.split_last().expect("passes");
123    assert_eq!(last.0, "verify", "the certifying pass is the last one");
124    run_pass_list(p, target, before)
125}
126
127fn run_pass_list(
128    p: SpatialProgram,
129    target: &SpatialTarget,
130    passes: &[(&str, Pass)],
131) -> Result<SpatialProgram, SpatialReject> {
132    let mut cur = p;
133    let tr = std::env::var("LOGOS_PASS_TIME").is_ok() && cur.name.contains("gd_");
134    for (name, pass) in passes {
135        let t0 = std::time::Instant::now();
136        cur = pass(&cur, target)?;
137        let dt = t0.elapsed();
138        cur.validate()?;
139        if tr {
140            eprintln!(
141                "TIME {} {name}: {:?} stages={} buffers={} edges={} items={}",
142                cur.name,
143                dt,
144                cur.stages.len(),
145                cur.buffers.len(),
146                cur.edges.len(),
147                cur.work.items.len()
148            );
149        }
150    }
151    Ok(cur)
152}
153
154/// The source-level names and recognized fills of ONE lifted statement — the binder, then every operand
155/// in [`TensorOpSpec::ins`] order. A fill is [`Fill::Unknown`] for a tensor the program never declares,
156/// which is exactly the activations a composition produces and consumes.
157#[derive(Clone, Debug, PartialEq, Eq)]
158pub struct StageNames {
159    pub out: String,
160    pub ins: Vec<String>,
161    pub fills: Vec<Fill>,
162}
163
164impl StageNames {
165    /// The name and fill of operand `i`.
166    pub fn operand(&self, i: usize) -> (&str, &Fill) {
167        (self.ins[i].as_str(), &self.fills[i])
168    }
169}
170
171/// The one op a single-op source is, as the graph of that op over the operands the source declares.
172fn recognize_any(source: &str) -> Result<TensorGraph, SpatialReject> {
173    crate::ui_bridge::with_parsed_program(source, |parsed, interner| {
174        let (stmts, _, _) = parsed.map_err(|e| SpatialReject::Parse(format!("{e:?}")))?;
175        let operand = |s, len: usize| (s, len, recognize_fill(stmts, s));
176        let matmul = recognize_matmul(stmts, interner);
177        if let Ok(spec) = matmul {
178            let ins = vec![operand(spec.a, spec.m * spec.k), operand(spec.b, spec.k * spec.n)];
179            let out = spec.c;
180            return Ok(TensorGraph::of_op(TensorOpSpec::Matmul(spec), ins, out, interner));
181        }
182        let elementwise = recognize_elementwise(stmts, interner);
183        if let Ok(spec) = elementwise {
184            let ins = vec![operand(spec.a, spec.n), operand(spec.b, spec.n)];
185            let out = spec.c;
186            return Ok(TensorGraph::of_op(TensorOpSpec::Elementwise(spec), ins, out, interner));
187        }
188        if let Ok(spec) = recognize_swiglu(stmts, interner) {
189            let ins = vec![operand(spec.gate, spec.total), operand(spec.up, spec.total)];
190            let out = spec.y;
191            return Ok(TensorGraph::of_op(TensorOpSpec::SwiGlu(spec), ins, out, interner));
192        }
193        let reduction = recognize_fused_row(stmts, interner);
194        if let Ok(spec) = reduction {
195            let (rows, cols) = (spec.rows, spec.cols);
196            let ins = std::iter::once(operand(spec.x, rows * cols))
197                .chain(spec.weight.map(|s| operand(s, cols)))
198                .chain(spec.residual.map(|s| operand(s, rows * cols)))
199                .chain(spec.bias.map(|s| operand(s, cols)))
200                .collect();
201            let out = spec.y;
202            return Ok(TensorGraph::of_op(TensorOpSpec::FusedRow(spec), ins, out, interner));
203        }
204        Err(SpatialReject::Unrecognized {
205            matmul: matmul.unwrap_err(),
206            elementwise: elementwise.unwrap_err(),
207            reduction: reduction.unwrap_err(),
208        })
209    })
210}
211
212/// English → a template (phase `Template`) with the given numerics policy.
213pub fn template_from_source_with(
214    source: &str,
215    target: &SpatialTarget,
216    numerics: NumericsPolicy,
217) -> Result<SpatialProgram, SpatialReject> {
218    let mut programs = link(&recognize_any(source)?, 0, &Prelude::default(), Form::Alone, numerics, target)?;
219    if programs.len() != 1 {
220        return Err(SpatialReject::NotLowered(
221            "a multi-section op as a single template — this shape only exists inside a composition",
222        ));
223    }
224    Ok(programs.remove(0))
225}
226
227pub fn template_from_source(
228    source: &str,
229    target: &SpatialTarget,
230) -> Result<SpatialProgram, SpatialReject> {
231    template_from_source_with(source, target, NumericsPolicy::Exact)
232}
233
234/// A template from a matmul spec directly (external data with declared ranges).
235pub fn template_from_spec(
236    spec: &MatmulSpec,
237    names: &MatmulNames,
238    fills: (&Fill, &Fill),
239    ranges: (Option<(i64, i64)>, Option<(i64, i64)>),
240    numerics: NumericsPolicy,
241    target: &SpatialTarget,
242) -> Result<SpatialProgram, SpatialReject> {
243    from_matmul(spec, names, fills, ranges, numerics, target)
244}
245
246/// English → a verified spatial program.
247pub fn compile_to_spatial_with(
248    source: &str,
249    target: &SpatialTarget,
250    numerics: NumericsPolicy,
251) -> Result<SpatialProgram, SpatialReject> {
252    run_passes(template_from_source_with(source, target, numerics)?, target)
253}
254
255pub fn compile_to_spatial(
256    source: &str,
257    target: &SpatialTarget,
258) -> Result<SpatialProgram, SpatialReject> {
259    compile_to_spatial_with(source, target, NumericsPolicy::Exact)
260}
261
262// ── The whole-program front end ───────────────────────────────────────────────────────────────────
263//
264// ★ A PARTIAL LOWERING IS NEVER A SUCCESS. `template_from_source` lifts the FIRST op it recognizes and
265// lowers it AS IF IT WERE THE PROGRAM — which on a hundred-statement transformer forward means emitting
266// one q-projection, with a certificate, and exiting zero. That is the single failure this whole front end
267// exists to make impossible: [`ProgramLowering`] carries the counts ALWAYS, and hands back a program only
268// when every statement the walk saw became a section. A caller that wants an artifact must ask
269// [`ProgramLowering::whole`], and the refusal it gets otherwise says how many of how many.
270
271/// One statement the whole-program walk saw and could not turn into a section, and why.
272#[derive(Clone, Debug, PartialEq, Eq)]
273pub struct MissingStage {
274    /// Where the statement sits in the source's top-level statement list.
275    pub stmt_index: usize,
276    /// The tensor it binds (or the loop/branch it is, when no recognizer lifted it).
277    pub binder: String,
278    /// The recognized op kind, or `unrecognized`.
279    pub kind: String,
280    /// The refusal, in the front end's own words.
281    pub why: String,
282}
283
284/// What the whole-program front end made of a source.
285///
286/// `statements` is every top-level statement the walk considered a tensor op — the recognized ones plus
287/// the ones no recognizer lifted. `lowered` is how many became sections. `program` is `Some` only when
288/// those two agree AND the composer joined the sections, so `program.is_some()` IS the honest verdict.
289#[derive(Clone, Debug)]
290pub struct ProgramLowering {
291    pub program: Option<SpatialProgram>,
292    pub statements: usize,
293    pub lowered: usize,
294    pub sections: usize,
295    pub missing: Vec<MissingStage>,
296    /// The state the walk found, by tensor name (first-appearance order) — every table a section updates in place.
297    pub state: Vec<String>,
298    /// The part of [`Self::state`] a step carries to the next: without the tables the step assembles whole
299    /// ([`carried_state`](super::passes::recognize_stages::carried_state)).
300    pub carried: Vec<String>,
301}
302
303impl ProgramLowering {
304    /// The composed program, or `None` when the lowering is partial — there is deliberately no way to
305    /// reach a program without passing this check.
306    pub fn whole(&self) -> Option<&SpatialProgram> {
307        self.program
308            .as_ref()
309            .filter(|_| self.lowered == self.statements)
310    }
311    /// [`Self::whole`], by value: the composed program, or the [`Self::refusal`] a partial lowering earns.
312    pub fn into_whole(self) -> Result<SpatialProgram, String> {
313        if self.whole().is_none() {
314            return Err(self.refusal());
315        }
316        Ok(self.program.expect("a whole lowering carries its program"))
317    }
318    /// The refusal a partial lowering earns: the fraction, then what is missing, by name.
319    pub fn refusal(&self) -> String {
320        let mut why: Vec<String> = self
321            .missing
322            .iter()
323            .take(MISSING_NAMED)
324            .map(|m| {
325                format!(
326                    "statement {} (`{}`, {}): {}",
327                    m.stmt_index, m.binder, m.kind, m.why
328                )
329            })
330            .collect();
331        if self.missing.len() > MISSING_NAMED {
332            why.push(format!("… and {} more", self.missing.len() - MISSING_NAMED));
333        }
334        if self.missing.is_empty() && self.program.is_none() {
335            return format!(
336                "lifted {} of {} statements into {} sections, and the composer did not join them into one program",
337                self.lowered, self.statements, self.sections
338            );
339        }
340        format!(
341            "lowered {} of {} statements; not lowered: {}",
342            self.lowered,
343            self.statements,
344            why.join("; ")
345        )
346    }
347}
348
349/// How many missing statements a refusal names before it counts the rest.
350const MISSING_NAMED: usize = 6;
351
352/// English → the WHOLE tensor program as sections of one spatial program.
353///
354/// `Err` only when there is no tensor program to speak of (the source does not parse, or no recognizer
355/// sees an op anywhere in it). Everything else comes back as a [`ProgramLowering`] whose counts are the
356/// truth, whether or not it carries a program.
357pub fn compile_program_to_spatial(
358    source: &str,
359    target: &SpatialTarget,
360) -> Result<ProgramLowering, SpatialReject> {
361    compile_program_to_spatial_agreeing(source, target, &std::collections::BTreeSet::new())
362}
363
364/// [`compile_program_to_spatial`] for one program of a DEPLOYMENT: `dense` names the tables the deployment's other
365/// programs lay out as matrices ([`dense_tables_of`]), so a table one program writes and another reads is laid out
366/// alike in both — the agreement `dense_tables` makes inside a program, made across the programs that share it.
367pub fn compile_program_to_spatial_agreeing(
368    source: &str,
369    target: &SpatialTarget,
370    dense: &std::collections::BTreeSet<String>,
371) -> Result<ProgramLowering, SpatialReject> {
372    lower_program(
373        source,
374        target,
375        dense,
376        None,
377        &std::collections::BTreeSet::new(),
378        &Results::EveryNamed,
379    )
380}
381
382/// [`compile_program_to_spatial`] for a program whose RESULTS are declared: `results` names what outside the program
383/// reads — its host, or the next program of a deployment — besides what it shows and the state it carries, and every
384/// other tensor only the next statement reads is forwarded ([`super::passes::forward`]).
385pub fn compile_program_to_spatial_returning(
386    source: &str,
387    target: &SpatialTarget,
388    results: &std::collections::BTreeSet<String>,
389) -> Result<ProgramLowering, SpatialReject> {
390    let none = std::collections::BTreeSet::new();
391    lower_program(source, target, &none, None, &none, &Results::Only(results.clone()))
392}
393
394/// [`compile_program_to_spatial`] for a program whose weights `q8` the deployment STORES Q8_0: a one-row product over
395/// one of them reads its int8 values and per-block scales as they are stored, rather than a bf16 copy
396/// ([`super::passes::from_q8`]). A product the fold cannot tile reads the weight dense.
397pub fn compile_program_to_spatial_stored(
398    source: &str,
399    target: &SpatialTarget,
400    q8: &std::collections::BTreeSet<String>,
401) -> Result<ProgramLowering, SpatialReject> {
402    lower_program(source, target, &std::collections::BTreeSet::new(), None, q8, &Results::EveryNamed)
403}
404
405/// [`compile_program_to_spatial_agreeing`] for a deployment whose weights `q8` are stored Q8_0.
406pub fn compile_program_to_spatial_agreeing_stored(
407    source: &str,
408    target: &SpatialTarget,
409    dense: &std::collections::BTreeSet<String>,
410    q8: &std::collections::BTreeSet<String>,
411) -> Result<ProgramLowering, SpatialReject> {
412    lower_program(source, target, dense, None, q8, &Results::EveryNamed)
413}
414
415/// [`compile_program_to_spatial`] for a TENSOR-PARALLEL program (see [`super::tensor_parallel`]): every section runs
416/// on the chip `manifest` gives what it computes, and each collective's fold statements are one all-reduce section.
417pub fn compile_program_to_spatial_tp(
418    source: &str,
419    target: &SpatialTarget,
420    manifest: &super::tensor_parallel::TpManifest,
421) -> Result<ProgramLowering, SpatialReject> {
422    compile_program_to_spatial_tp_agreeing(
423        source,
424        target,
425        manifest,
426        &std::collections::BTreeSet::new(),
427    )
428}
429
430/// [`compile_program_to_spatial_tp`] for one program of a deployment, agreeing with its other programs on the tables
431/// laid out dense (see [`compile_program_to_spatial_agreeing`]).
432pub fn compile_program_to_spatial_tp_agreeing(
433    source: &str,
434    target: &SpatialTarget,
435    manifest: &super::tensor_parallel::TpManifest,
436    dense: &std::collections::BTreeSet<String>,
437) -> Result<ProgramLowering, SpatialReject> {
438    compile_program_to_spatial_tp_agreeing_stored(
439        source,
440        target,
441        manifest,
442        dense,
443        &std::collections::BTreeSet::new(),
444    )
445}
446
447/// [`compile_program_to_spatial_tp_agreeing`] for a deployment whose chip-local weight slices `q8` are stored Q8_0 — each
448/// chip's slice read as its int8 values and scales ([`super::passes::from_q8`]).
449pub fn compile_program_to_spatial_tp_agreeing_stored(
450    source: &str,
451    target: &SpatialTarget,
452    manifest: &super::tensor_parallel::TpManifest,
453    dense: &std::collections::BTreeSet<String>,
454    q8: &std::collections::BTreeSet<String>,
455) -> Result<ProgramLowering, SpatialReject> {
456    lower_program(source, target, dense, Some(manifest), q8, &Results::EveryNamed)
457}
458
459/// Where a composed program's sections are certified.
460#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub enum SectionCertification {
462    /// Once, as the composed program: each section is only scheduled, and the composition's certificate — the one that
463    /// ships — covers it.
464    Composed,
465    /// Each section on its own as it is lowered, and then the composition: the path that names every statement whose
466    /// section a certificate refuses.
467    EachSection,
468}
469
470/// ★ CERTIFY ONCE, WHERE IT SHIPS. A section's own certificate never leaves the pipeline — `compose` drops it and the
471/// composed program earns its own over the same stages — so certifying each section as it is lowered proves every
472/// section twice. The composed certification comes first, and it is the answer whenever the program certifies whole:
473/// the same program and the same certificate, since nothing reads a section's. A program that does not certify whole
474/// is lowered again certifying each section, so the answer is exactly the one that names what refused.
475fn lower_program(
476    source: &str,
477    target: &SpatialTarget,
478    dense: &std::collections::BTreeSet<String>,
479    tp: Option<&super::tensor_parallel::TpManifest>,
480    q8: &std::collections::BTreeSet<String>,
481    results: &Results,
482) -> Result<ProgramLowering, SpatialReject> {
483    lower_program_on(source, target, dense, tp, q8, None, results)
484}
485
486/// [`lower_program`] of the statements `chip` names alone, when it does: composed, and certified each section at a time
487/// where the composition does not certify.
488fn lower_program_on(
489    source: &str,
490    target: &SpatialTarget,
491    dense: &std::collections::BTreeSet<String>,
492    tp: Option<&super::tensor_parallel::TpManifest>,
493    q8: &std::collections::BTreeSet<String>,
494    chip: Option<(u16, &super::tensor_parallel::TpManifest)>,
495    results: &Results,
496) -> Result<ProgramLowering, SpatialReject> {
497    let composed = lower_program_stored(
498        source,
499        target,
500        dense,
501        tp,
502        SectionCertification::Composed,
503        q8,
504        chip,
505        results,
506    )?;
507    if composed.whole().is_some() {
508        return Ok(composed);
509    }
510    lower_program_stored(
511        source,
512        target,
513        dense,
514        tp,
515        SectionCertification::EachSection,
516        q8,
517        chip,
518        results,
519    )
520}
521
522/// A tensor-parallel program lowered for chips that share no fabric: one ordinary program a chip, and the tensors that
523/// cross between them ([`compile_program_to_chips`]).
524#[derive(Clone, Debug)]
525pub struct ChipLowering {
526    /// Chip `c`'s program: the statements `manifest` runs on chip `c`, lowered and composed on one site as any one-chip
527    /// program is. A tensor another chip writes is an input, holding the interval the whole program proves for it.
528    pub chips: Vec<ProgramLowering>,
529    pub transfers: Vec<super::tensor_parallel::ChipTransfer>,
530}
531
532/// ★ A CHIP BOUNDARY IS A TRANSFER, NOT A SHARED MEMORY. [`compile_program_to_spatial_tp`] lowers a tensor-parallel
533/// program as ONE program over its chips, every section followed by a barrier the chips all join — the shape of a
534/// machine whose chips share one fabric. Boards joined by a link share nothing: each runs its own program, and a tensor
535/// one board writes and the other reads crosses the link between them. So each chip's statements are lowered alone, in
536/// order, as an ordinary one-chip program, every tensor narrowed by the interval the WHOLE program proves for it, and
537/// what crosses is listed ([`super::tensor_parallel::transfers`]). An all-reduce is then what it is over a link: the
538/// partials cross to the chip that folds them, and the fold is that chip's own addition.
539pub fn compile_program_to_chips(
540    source: &str,
541    target: &SpatialTarget,
542    manifest: &super::tensor_parallel::TpManifest,
543) -> Result<ChipLowering, SpatialReject> {
544    lower_chips(source, target, manifest, &Results::EveryNamed)
545}
546
547/// [`compile_program_to_chips`] for a program whose results are declared (see
548/// [`compile_program_to_spatial_returning`]): on each chip, a tensor only the next statement reads — no statement of
549/// any chip, no `Show`, no carried state, nothing `results` names — is forwarded.
550pub fn compile_program_to_chips_returning(
551    source: &str,
552    target: &SpatialTarget,
553    manifest: &super::tensor_parallel::TpManifest,
554    results: &std::collections::BTreeSet<String>,
555) -> Result<ChipLowering, SpatialReject> {
556    lower_chips(source, target, manifest, &Results::Only(results.clone()))
557}
558
559fn lower_chips(
560    source: &str,
561    target: &SpatialTarget,
562    manifest: &super::tensor_parallel::TpManifest,
563    results: &Results,
564) -> Result<ChipLowering, SpatialReject> {
565    let rp = TensorGraph::lift_source(source, &|_| Storage::Values)?.program;
566    let transfers = super::tensor_parallel::transfers(&rp.stages, manifest);
567    let none = std::collections::BTreeSet::new();
568    let mut chips = (0..manifest.chips)
569        .map(|c| lower_program_on(source, target, &none, None, &none, Some((c as u16, manifest)), results))
570        .collect::<Result<Vec<_>, _>>()?;
571    for (c, pl) in chips.iter_mut().enumerate() {
572        if let Some(p) = pl.program.as_mut() {
573            match super::tensor_parallel::link_points(p, c as u16, &transfers) {
574                Ok(links) => p.links = links,
575                Err(e) => {
576                    pl.missing.push(MissingStage {
577                        stmt_index: 0,
578                        binder: format!("chip {c}"),
579                        kind: "link".into(),
580                        why: format!("{e:?}"),
581                    });
582                    pl.program = None;
583                }
584            }
585        }
586    }
587    Ok(ChipLowering { chips, transfers })
588}
589
590/// The whole tensor program lowered with its sections certified as `sections` says — `lower_program`'s two paths,
591/// public so a lock can hold them to one answer.
592pub fn lower_program_with(
593    source: &str,
594    target: &SpatialTarget,
595    dense: &std::collections::BTreeSet<String>,
596    tp: Option<&super::tensor_parallel::TpManifest>,
597    sections: SectionCertification,
598) -> Result<ProgramLowering, SpatialReject> {
599    lower_program_stored(
600        source,
601        target,
602        dense,
603        tp,
604        sections,
605        &std::collections::BTreeSet::new(),
606        None,
607        &Results::EveryNamed,
608    )
609}
610
611/// ★ ONE VIEW OF A VALUE, HOWEVER MANY PRODUCTS READ IT. A Q8_0 product reads its activation through the `K/32 × 32` view
612/// its own lowering reshapes, named for the activation's value ([`super::passes::from_q8::q8_rows_name`]); the
613/// projections of one normalized row (q, k, v, the gates) all read the same value, so the reshape is kept only the
614/// first time — `made` holds the views already made.
615fn share_q8_views(
616    sections: Vec<SpatialProgram>,
617    view: Option<&String>,
618    made: &mut std::collections::BTreeSet<String>,
619) -> Vec<SpatialProgram> {
620    let Some(view) = view else {
621        return sections;
622    };
623    let makes_view = |p: &SpatialProgram| {
624        p.tensors
625            .iter()
626            .any(|t| t.name == *view && t.role != TensorRole::Input)
627    };
628    if !sections.iter().any(makes_view) || made.insert(view.clone()) {
629        return sections;
630    }
631    sections.into_iter().filter(|p| !makes_view(p)).collect()
632}
633
634/// One op's section templates awaiting their passes, and what names it where it is refused.
635struct Held {
636    at: usize,
637    stmt_index: usize,
638    out: String,
639    kind: String,
640    templates: Vec<SpatialProgram>,
641}
642
643/// Statements whose templates joined ([`super::passes::forward`]): their templates as joined.
644struct Run {
645    held: Vec<Held>,
646    joined: Vec<SpatialProgram>,
647}
648
649/// A statement's place in the lowering, in program order: a run of joined statements, or sections already made (an
650/// all-reduce) or none (a statement that did not lower).
651enum Piece {
652    Run(Run),
653    Done(Vec<SpatialProgram>),
654}
655
656/// ★ WHAT OUTSIDE A PROGRAM READS ITS TENSORS, the program's caller says. Its host may read any tensor it is handed, and a
657/// program that is one of a deployment's has readers in the next program that its own statements never show. So a
658/// tensor only the next statement reads is forwarded ([`super::passes::forward`]) only where the caller has declared
659/// what else is read; otherwise every tensor a statement names is stored, as the program wrote it.
660#[derive(Clone, Debug, PartialEq, Eq)]
661pub enum Results {
662    /// Any tensor a statement names may be read: each is stored, and nothing is forwarded.
663    EveryNamed,
664    /// What these name is read, besides what the program shows and the state it carries: a tensor nothing else reads
665    /// is forwarded.
666    Only(std::collections::BTreeSet<String>),
667}
668
669/// [`lower_program_with`] with the weights `q8` the deployment stores Q8_0, and the results `results` its caller reads.
670#[allow(clippy::too_many_arguments)]
671fn lower_program_stored(
672    source: &str,
673    target: &SpatialTarget,
674    dense: &std::collections::BTreeSet<String>,
675    tp: Option<&super::tensor_parallel::TpManifest>,
676    sections: SectionCertification,
677    q8: &std::collections::BTreeSet<String>,
678    chip: Option<(u16, &super::tensor_parallel::TpManifest)>,
679    results: &Results,
680) -> Result<ProgramLowering, SpatialReject> {
681    use super::tensor_parallel::{allreduce_section, relocate};
682    let certification = sections;
683    let passes = |t: SpatialProgram| match certification {
684        SectionCertification::Composed => run_passes_uncertified(t, target),
685        SectionCertification::EachSection => run_passes(t, target),
686    };
687    let storage = |t: &str| {
688        if q8.contains(t) {
689            Storage::Rows { format: crate::model::quant::GgmlType::Q8_0, transposed: true }
690        } else {
691            Storage::Values
692        }
693    };
694    let Lifted { program: whole, graph } = TensorGraph::lift_source(source, &storage)?;
695    // The ops the shared optimizer leaves this chip, in order: under its shim, each a statement the front end lifted. A
696    // tensor-parallel program's fusions stay within one chip ([`super::tensor_parallel::TensorParallelShim`]).
697    let spatial = super::shim::SpatialShim { target };
698    let parallel = tp.or(chip.map(|(_, m)| m)).map(|manifest| super::tensor_parallel::TensorParallelShim { base: &spatial, manifest });
699    let shim: &dyn crate::tensor_graph::shim::Shim = match &parallel {
700        Some(s) => s,
701        None => &spatial,
702    };
703    let g = crate::tensor_graph::fuse::optimize(&graph, shim, &[]).graph;
704    // The statement each op lifts, by which a refusal names it.
705    let stmts: Vec<usize> = g.ops.iter().map(|op| whole.stages[op.covers[0]].stmt_index).collect();
706    let stmt_of = |k: usize| stmts[k];
707    // The statement each lifted op is, by which a region's member names its own.
708    let lifted_stmts: Vec<usize> = whole.stages.iter().map(|st| st.stmt_index).collect();
709    // The intervals and the float tensors are the whole program's: a tensor another chip writes holds on this chip what
710    // it holds there.
711    let float_out = float_tensors(&whole);
712    let ranges = super::value_range::value_ranges(&whole, &float_out);
713    let rp = match chip {
714        Some((c, m)) => super::passes::recognize_stages::RecognizedProgram {
715            stages: whole.stages.iter().filter(|st| m.chip_running(&st.out) == c).cloned().collect(),
716            ..whole.clone()
717        },
718        None => whole,
719    };
720    if rp.stages.is_empty() {
721        // Nothing lifted at all: hand back the precise three-way reject the single-op path computes, so
722        // a front-end regression is never reported as an empty program.
723        return Err(recognize_any(source)
724            .err()
725            .unwrap_or(SpatialReject::NotLowered(
726                "a source with no tensor statements",
727            )));
728    }
729    let statements = rp.stages.len() + rp.unrecognized.len() + rp.unlifted_writes.len();
730    let mut missing: Vec<MissingStage> = rp
731        .unrecognized
732        .iter()
733        .map(|(i, b)| MissingStage {
734            stmt_index: *i,
735            binder: b.clone(),
736            kind: "unrecognized".into(),
737            why: "no recognizer of the whole-program walk lifts this statement".into(),
738        })
739        .chain(rp.unlifted_writes.iter().map(|(i, t, why)| MissingStage {
740            stmt_index: *i,
741            binder: t.clone(),
742            kind: "write".into(),
743            why: (*why).to_string(),
744        }))
745        .collect();
746    // The intervals each section's storage is narrowed by: the front end's, then what each lowered section proves it
747    // stores into a tensor the front end never saw.
748    let mut known = ranges.clone();
749    let ours = |op: &Op| chip.is_none_or(|(c, m)| m.chip_running(&op.out) == c);
750    let linked: Vec<usize> = (0..g.ops.len()).filter(|&k| ours(&g.ops[k])).collect();
751    let prelude = Prelude::of(&g, &linked, target, dense, ranges.clone(), q8, &|out| tp.and_then(|m| m.chip_of.get(out).copied()));
752    let mut twins = std::collections::BTreeSet::new();
753    let mut q8_views = std::collections::BTreeSet::new();
754    let mut sections: Vec<SpatialProgram> = Vec::new();
755    let mut lowered = 0usize;
756    // ★ A RESULT ONLY THE NEXT STATEMENT READS IS FORWARDED ([`super::passes::forward`]), wherever the caller has
757    // declared the program's results ([`Results`]) and the tensor between two statements is read by nothing else — no
758    // later statement, no `Show`, no carried state, nothing the caller reads — and its store and load agree. Two levels:
759    // - statements whose templates join make a RUN, tiled, placed and scheduled as one section (a map folds into the
760    //   map before it);
761    // - two neighbouring runs whose planned sections join — a matmul's items meet a row map's one item only once `tile`
762    //   has grouped them — are placed and scheduled as one.
763    // Whatever the passes refuse joined is lowered as it was before it was joined: a planned join as its two runs, a run
764    // as its statements, each as it would have been. A tensor-parallel program relocates each statement's sections to
765    // its chips and forwards nothing.
766    let carried: std::collections::BTreeSet<String> = state_tensors(&rp.stages).into_iter().collect();
767    let touches = g.touches();
768    let forwards = |t: &str, writer: usize, reader: usize| {
769        let read = match results {
770            Results::EveryNamed => return false,
771            Results::Only(read) => read,
772        };
773        tp.is_none()
774            && !read.contains(t)
775            && !carried.contains(t)
776            && g.ops[reader].ins.iter().filter(|x| *x == t).count() == 1
777            && touches.read_only_by(t, writer, reader)
778    };
779    let certify = matches!(certification, SectionCertification::EachSection);
780    // A join hands the value to ONE section's compute: a tensor another section of either statement names stays stored.
781    let names = |ps: &[SpatialProgram], t: &str| ps.iter().any(|p| p.tensors.iter().any(|x| x.name == t));
782    // ★ A JOIN NEVER WIDENS WHAT A VALUE IS PROVEN TO HOLD. A load of the tensor reads it within the interval the front
783    // end proves for it; the buffer that carries it once joined holds what the producer's compute proves. Where the
784    // front end knows more than any analysis of that compute can see — an attention's context, a weighted mean of its
785    // values, computed by a division — the join would hand the next compute a wider value than its load did, and every
786    // width sized from it after.
787    let keeps = |j: &super::passes::forward::Joint, t: &str, known: &std::collections::BTreeMap<String, super::value_range::Interval>| {
788        let Some(&bound) = known.get(t) else { return true };
789        let proven = super::value_range::program_ranges(&declare_ranges(j.program.clone(), known));
790        proven.buffers[j.carrier.idx()].is_some_and(|iv| bound.0 <= iv.0 && iv.1 <= bound.1)
791    };
792    // ★ ONE DECISION JOINS TWO SECTIONS, at either level: `a`, the producer's last section (`before` its others), and
793    // `b`, the consumer's first (`after` its others), through `t` — where nothing else reads `t`, no other section of
794    // either names it, `forward` joins them, and the carrier keeps what `t` is proven to hold.
795    let join = |t: &str,
796                writer: usize,
797                reader: usize,
798                (before, a): (&[SpatialProgram], &SpatialProgram),
799                (b, after): (&SpatialProgram, &[SpatialProgram]),
800                known: &std::collections::BTreeMap<String, super::value_range::Interval>| {
801        (forwards(t, writer, reader) && !names(before, t) && !names(after, t))
802            .then(|| super::passes::forward::forward(a, b, t))
803            .flatten()
804            .filter(|j| keeps(j, t, known))
805    };
806    let mut pieces: Vec<Piece> = Vec::new();
807    for &k in &linked {
808        let op = &g.ops[k];
809        // ★ A COLLECTIVE IS ONE SECTION, NOT ITS FOLD. The source spells an all-reduce as the chain of additions it
810        // means, so the VM can check the decomposition; on the chips it is the partials folded where they live. The
811        // chain's earlier links compute nothing on their own, and its last is where the all-reduce stands.
812        if let Some(c) = tp.and_then(|m| m.collective_folding(&op.out)) {
813            if op.out == c.out {
814                let n = match op.prim() {
815                    Some(TensorOpSpec::Elementwise(e)) => e.n,
816                    _ => 0,
817                };
818                let floats = float_out.contains(&c.out) || c.partials.iter().any(|x| float_out.contains(x));
819                let mut narrow = |t: SpatialProgram| narrow_storage(t, &mut known, target);
820                match allreduce_section(c, g.shape_of(&c.out, n), floats, target, certification, &mut narrow) {
821                    Ok(p) => pieces.push(Piece::Done(vec![p])),
822                    Err(e) => {
823                        pieces.push(Piece::Done(Vec::new()));
824                        missing.push(MissingStage {
825                            stmt_index: stmt_of(k),
826                            binder: op.out.clone(),
827                            kind: "AllReduce".into(),
828                            why: format!("{e:?}"),
829                        })
830                    }
831                }
832            } else {
833                pieces.push(Piece::Done(Vec::new()));
834            }
835            lowered += 1;
836            continue;
837        }
838        // The float flag a section is lowered with: its result's kind — except an arg-max, whose result is
839        // always an index and whose datapath is its INPUT's.
840        let floats = match op.prim() {
841            Some(TensorOpSpec::ArgMax(_)) => float_out.contains(&op.ins[0]),
842            _ => float_out.contains(&op.out),
843        };
844        // A region is one section. Each member is linked alone, as a statement of its own would be, and the members
845        // are joined from the region's result back, each value handed from the member that makes it to the one that
846        // reads it ([`super::passes::forward`]) — the shim admits only a region whose every value is read once within it
847        // and none written out. A member that lowers to more than one section, or a join the passes refuse, leaves the
848        // members' sections apart, each storing what it makes.
849        let made = match &op.sym {
850            crate::tensor_graph::graph::Sym::Fused(f) if f.class == crate::tensor_graph::symbols::ClassId::ElementwiseRegions => {
851                let mut held: Vec<Held> = Vec::new();
852                let mut alone: Vec<SpatialProgram> = Vec::new();
853                let mut refused = None;
854                // One graph of the member alone, its tensors the program's, taken once for the region.
855                let mut one = TensorGraph { identity: g.identity.clone(), io: g.io.clone(), tensors: g.tensors.clone(), ops: Vec::new() };
856                for m in &f.parts {
857                    one.ops = vec![m.clone()];
858                    match link(&one, 0, &prelude, Form::Section { floats: float_out.contains(&m.out) }, NumericsPolicy::Exact, target) {
859                        Ok(ts) => {
860                            alone.extend(ts.iter().cloned());
861                            held.push(Held { at: k, stmt_index: lifted_stmts[m.covers[0]], out: m.out.clone(), kind: m.kind().to_string(), templates: ts });
862                        }
863                        Err(e) => {
864                            refused = Some(e);
865                            break;
866                        }
867                    }
868                }
869                match refused {
870                    Some(e) => Err(e),
871                    None => {
872                        let single = held.iter().all(|h| h.templates.len() == 1) && !matches!(results, Results::EveryNamed) && tp.is_none();
873                        let mut joined = single.then(|| held.last().expect("a region has members").templates[0].clone());
874                        for h in held.iter().rev().skip(1) {
875                            joined = joined.and_then(|acc| {
876                                super::passes::forward::forward(&h.templates[0], &acc, &h.out).filter(|j| keeps(j, &h.out, &known)).map(|j| j.program)
877                            });
878                        }
879                        Ok((held, joined.map_or(alone, |p| vec![p])))
880                    }
881                }
882            }
883            _ => link(&g, k, &prelude, Form::Section { floats }, NumericsPolicy::Exact, target)
884                .map(|ts| share_q8_views(ts, prelude.views.get(&k), &mut q8_views))
885                .map(|ts| (vec![Held { at: k, stmt_index: stmt_of(k), out: op.out.clone(), kind: op.kind().to_string(), templates: ts.clone() }], ts)),
886        };
887        match made {
888            Ok((held, ts)) => {
889                let joint = match pieces.last() {
890                    Some(Piece::Run(run)) => {
891                        let last = run.held.last().expect("a run holds a statement");
892                        run.joined
893                            .split_last()
894                            .zip(ts.split_first())
895                            .and_then(|((a, before), (b, after))| join(&last.out, last.at, k, (before, a), (b, after), &known))
896                    }
897                    _ => None,
898                };
899                match (joint, pieces.last_mut()) {
900                    (Some(j), Some(Piece::Run(run))) => {
901                        run.joined.pop();
902                        run.joined.push(j.program);
903                        run.joined.extend(ts.into_iter().skip(1));
904                        run.held.extend(held);
905                    }
906                    _ => pieces.push(Piece::Run(Run { held, joined: ts })),
907                }
908            }
909            Err(e) => {
910                pieces.push(Piece::Done(Vec::new()));
911                missing.push(MissingStage {
912                    stmt_index: stmt_of(k),
913                    binder: op.out.clone(),
914                    kind: op.kind().to_string(),
915                    why: format!("{e:?}"),
916                });
917            }
918        }
919    }
920    // A statement's own templates through every pass, its layouts agreed and, in a tensor-parallel program, relocated.
921    let alone = |h: &Held,
922                 known: &mut std::collections::BTreeMap<String, super::value_range::Interval>,
923                 twins: &mut std::collections::BTreeSet<String>|
924     -> Result<Vec<SpatialProgram>, String> {
925        let ps = h
926            .templates
927            .iter()
928            .cloned()
929            .map(|t| passes(narrow_storage(t, known, target)))
930            .collect::<Result<Vec<_>, _>>()
931            .map_err(|e| format!("{e:?}"))?;
932        let ps = prelude.agree_layouts(&g, ps, twins, &passes, target).map_err(|e| format!("{e:?}"))?;
933        match tp {
934            Some(m) => ps.iter().map(|p| relocate(p, &h.out, m).map_err(|e| format!("{e:?}"))).collect(),
935            None => Ok(ps),
936        }
937    };
938    // A run's joined templates planned, or `None` where the passes refuse one.
939    let plan_run = |run: &Run, known: &mut std::collections::BTreeMap<String, super::value_range::Interval>| -> Option<Vec<SpatialProgram>> {
940        if run.held.len() == 1 {
941            return None;
942        }
943        run.joined
944            .iter()
945            .cloned()
946            .map(|t| run_passes_through_tile(narrow_storage(t, known, target), target))
947            .collect::<Result<Vec<_>, _>>()
948            .ok()
949    };
950    let place = |ps: Vec<SpatialProgram>, twins: &mut std::collections::BTreeSet<String>| -> Option<Vec<SpatialProgram>> {
951        let ps = ps.into_iter().map(|p| run_passes_after_tile(p, target, certify)).collect::<Result<Vec<_>, _>>().ok()?;
952        prelude.agree_layouts(&g, ps, twins, &passes, target).ok()
953    };
954    let lower_alone = |run: &Run,
955                           sections: &mut Vec<SpatialProgram>,
956                           missing: &mut Vec<MissingStage>,
957                           lowered: &mut usize,
958                           known: &mut std::collections::BTreeMap<String, super::value_range::Interval>,
959                           twins: &mut std::collections::BTreeSet<String>| {
960        for h in &run.held {
961            match alone(h, known, twins) {
962                Ok(mut ps) => {
963                    *lowered += 1;
964                    sections.append(&mut ps);
965                }
966                Err(why) => missing.push(MissingStage { stmt_index: h.stmt_index, binder: h.out.clone(), kind: h.kind.clone(), why }),
967            }
968        }
969    };
970    // Each run planned — its own templates when it joined nothing — then joined to the run after it where their planned
971    // sections join, and placed and scheduled.
972    let mut planned: Vec<(Run, Vec<SpatialProgram>)> = Vec::new();
973    let flush_planned = |planned: &mut Vec<(Run, Vec<SpatialProgram>)>,
974                             sections: &mut Vec<SpatialProgram>,
975                             missing: &mut Vec<MissingStage>,
976                             lowered: &mut usize,
977                             known: &mut std::collections::BTreeMap<String, super::value_range::Interval>,
978                             twins: &mut std::collections::BTreeSet<String>| {
979        let chain = std::mem::take(planned);
980        // The chain cut into units: neighbouring runs whose planned sections join are one unit, its sections as joined;
981        // each unit is placed, and its layouts agreed, on its own, so a layout bridge is judged against the roles of the
982        // statements it serves, never the whole chain's.
983        let mut units: Vec<(Vec<usize>, Vec<SpatialProgram>)> = Vec::new();
984        for (k, (run, ps)) in chain.iter().enumerate() {
985            let joint = match (k.checked_sub(1), units.last()) {
986                (Some(p), Some((_, joined))) => {
987                    let prev = chain[p].0.held.last().expect("a run holds a statement");
988                    chain[p]
989                        .1
990                        .split_last()
991                        .map(|(_, before)| before)
992                        .zip(joined.last())
993                        .zip(ps.split_first())
994                        .and_then(|((before, a), (b, after))| join(&prev.out, prev.at, run.held[0].at, (before, a), (b, after), known))
995                }
996                _ => None,
997            };
998            match (joint, units.last_mut()) {
999                (Some(j), Some((runs, joined))) => {
1000                    joined.pop();
1001                    joined.push(j.program);
1002                    joined.extend(ps.iter().skip(1).cloned());
1003                    runs.push(k);
1004                }
1005                _ => units.push((vec![k], ps.clone())),
1006            }
1007        }
1008        for (runs, ps) in units {
1009            if runs.len() > 1 {
1010                if let Some(mut ps) = place(ps, twins) {
1011                    *lowered += runs.iter().map(|k| chain[*k].0.held.len()).sum::<usize>();
1012                    sections.append(&mut ps);
1013                    continue;
1014                }
1015            }
1016            for k in runs {
1017                let (run, ps) = &chain[k];
1018                match place(ps.clone(), twins) {
1019                    Some(mut ps) => {
1020                        *lowered += run.held.len();
1021                        sections.append(&mut ps);
1022                    }
1023                    None => lower_alone(run, sections, missing, lowered, known, twins),
1024                }
1025            }
1026        }
1027    };
1028    for piece in pieces {
1029        match piece {
1030            Piece::Done(mut ps) => {
1031                flush_planned(&mut planned, &mut sections, &mut missing, &mut lowered, &mut known, &mut twins);
1032                sections.append(&mut ps);
1033            }
1034            Piece::Run(run) => {
1035                let ps = if tp.is_some() {
1036                    None
1037                } else {
1038                    plan_run(&run, &mut known).or_else(|| {
1039                        run.joined
1040                            .iter()
1041                            .cloned()
1042                            .filter(|_| run.held.len() == 1)
1043                            .map(|t| run_passes_through_tile(narrow_storage(t, &mut known, target), target))
1044                            .collect::<Result<Vec<_>, _>>()
1045                            .ok()
1046                            .filter(|ps| !ps.is_empty())
1047                    })
1048                };
1049                match ps {
1050                    Some(ps) => planned.push((run, ps)),
1051                    None => {
1052                        flush_planned(&mut planned, &mut sections, &mut missing, &mut lowered, &mut known, &mut twins);
1053                        lower_alone(&run, &mut sections, &mut missing, &mut lowered, &mut known, &mut twins);
1054                    }
1055                }
1056            }
1057        }
1058    }
1059    flush_planned(&mut planned, &mut sections, &mut missing, &mut lowered, &mut known, &mut twins);
1060    // ★ NO SECTION READS A RESULT NOTHING STORES. A statement's result a section loads is one some section stores; a
1061    // load of one nothing stores reads whatever the memory held. Every join is made to keep this; the composition is
1062    // refused, by the result's name, wherever one did not.
1063    let stored: std::collections::BTreeSet<&str> = sections
1064        .iter()
1065        .flat_map(|p| {
1066            p.stages.iter().filter_map(move |s| match &s.kind {
1067                StageKind::Store { tensor, .. } => Some(p.tensor(*tensor).name.as_str()),
1068                _ => None,
1069            })
1070        })
1071        .collect();
1072    for p in &sections {
1073        for s in &p.stages {
1074            let StageKind::Load { tensor, .. } = &s.kind else { continue };
1075            let name = &p.tensor(*tensor).name;
1076            if stored.contains(name.as_str()) || missing.iter().any(|m| m.binder == *name) {
1077                continue;
1078            }
1079            if let Some(st) = rp.stages.iter().find(|st| st.out == *name) {
1080                missing.push(MissingStage {
1081                    stmt_index: st.stmt_index,
1082                    binder: name.clone(),
1083                    kind: "read".into(),
1084                    why: format!("section {} loads it and no section stores it", p.name),
1085                });
1086            }
1087        }
1088    }
1089    missing.sort_by_key(|m| m.stmt_index);
1090    let state = state_tensors(&rp.stages);
1091    let n_sections = sections.len();
1092    let mut program = None;
1093    if lowered == rp.stages.len() && missing.is_empty() {
1094        // A tensor-parallel program's chips run their slices at once: its sections compose phase by phase. On one chip,
1095        // independent sections run at once on disjoint sites (`pack`).
1096        let (sections, barriers) = match tp {
1097            Some(_) => {
1098                let (order, barriers) = super::tensor_parallel::phases(&sections);
1099                let mut slots: Vec<Option<SpatialProgram>> =
1100                    sections.into_iter().map(Some).collect();
1101                (
1102                    order
1103                        .iter()
1104                        .map(|k| slots[*k].take().expect("each section once"))
1105                        .collect(),
1106                    Some(barriers),
1107                )
1108            }
1109            None => match super::passes::pack::pack_phases(sections, target) {
1110                Ok((sections, barriers)) => (sections, Some(barriers)),
1111                Err(sections) => (sections, None),
1112            },
1113        };
1114        match super::passes::compose::compose_phased(
1115            sections,
1116            &links(&rp.stages),
1117            &state,
1118            super::passes::compose::SectionPlacement::Stacked,
1119            barriers.as_deref(),
1120            target,
1121        ) {
1122            // ★ THE COMPOSED PROGRAM EARNS ITS OWN CERTIFICATE. `compose` hands back a scheduled program
1123            // with `certificate: None` — the section certificates it was built from say nothing about the
1124            // choreography that now runs them back to back. So the whole thing goes through `verify` once
1125            // more, and a composition whose barriers, buffers or ordering do not hold is refused HERE
1126            // rather than shipped as a bundle whose certificate covers one section of many.
1127            Ok(p) => match super::passes::verify::verify(&declare_ranges(p, &ranges), target) {
1128                Ok(v) => program = Some(v),
1129                Err(e) => missing.push(MissingStage {
1130                    stmt_index: rp.stages.last().map(|s| s.stmt_index).unwrap_or(0),
1131                    binder: rp.stages.last().map(|s| s.out.clone()).unwrap_or_default(),
1132                    kind: "verify".into(),
1133                    why: format!("the composed program does not certify: {e:?}"),
1134                }),
1135            },
1136            Err(e) => missing.push(MissingStage {
1137                stmt_index: rp.stages.last().map(|s| s.stmt_index).unwrap_or(0),
1138                binder: rp.stages.last().map(|s| s.out.clone()).unwrap_or_default(),
1139                kind: "compose".into(),
1140                why: format!("{e:?}"),
1141            }),
1142        }
1143    }
1144    let carried = super::passes::recognize_stages::carried_state(&rp);
1145    Ok(ProgramLowering {
1146        program,
1147        statements,
1148        lowered,
1149        sections: n_sections,
1150        missing,
1151        state,
1152        carried,
1153    })
1154}
1155
1156/// ★ AN INTEGER BUFFER IS STORED AT THE NARROWEST PRECISION THAT HOLDS WHAT IT HOLDS. On a target whose datapath
1157/// stores narrow integers, a section's buffers take the narrowest of them that holds their proven interval
1158/// ([`super::value_range::program_ranges`] over the section, its tensors declaring the front end's intervals) — an int8
1159/// cache is an int8 ring, not an int64 one, which is what lets an attention's cache and its tables fit a GPU block's
1160/// shared memory. Set before tiling, so the plan sizes the narrower pages. A section with a matmul narrows only the
1161/// buffers its loads fill: before tiling its contraction is not yet blocked, so what the product holds is not yet
1162/// known; a load's buffer holds its tensor and, where the tensor ends inside a tile, the padding zeros. A matmul's
1163/// operand buffer keeps the operand precision its numerics chose.
1164fn narrow_storage(
1165    mut t: SpatialProgram,
1166    ranges: &mut std::collections::BTreeMap<String, super::value_range::Interval>,
1167    target: &SpatialTarget,
1168) -> SpatialProgram {
1169    use super::target::precision_int_bits;
1170    let storable = target.storable_ints();
1171    let matmul = |s: &Stage| matches!(s.kind, StageKind::Compute(ComputeOp::MatmulTiles { .. }));
1172    let operands: std::collections::BTreeSet<BufferId> =
1173        t.stages.iter().filter(|s| matmul(s)).flat_map(|s| s.ins.iter().copied()).collect();
1174    let proven = super::value_range::program_ranges(&declare_ranges(t.clone(), ranges));
1175    let whole = !t.stages.iter().any(matmul);
1176    // A tensor a whole section stores and the front end did not range — the inside of a decomposed op, a later
1177    // section's input — holds what the section proves it stores.
1178    if whole {
1179        for st in &t.stages {
1180            if let StageKind::Store { tensor, .. } = &st.kind {
1181                let name = &t.tensor(*tensor).name;
1182                if let (false, Some(iv)) = (ranges.contains_key(name), proven.tensors[tensor.idx()]) {
1183                    if iv != super::value_range::UNBOUNDED {
1184                        ranges.insert(name.clone(), iv);
1185                    }
1186                }
1187            }
1188        }
1189    }
1190    if storable.iter().all(|p| *p == Precision::Int64) {
1191        return t;
1192    }
1193    let loaded: std::collections::BTreeSet<BufferId> = t
1194        .stages
1195        .iter()
1196        .filter(|s| matches!(s.kind, StageKind::Load { .. }))
1197        .flat_map(|s| s.outs.iter().copied())
1198        .collect();
1199    for b in &mut t.buffers {
1200        let Some(now) = precision_int_bits(b.dtype) else { continue };
1201        if operands.contains(&b.id) || !(whole || loaded.contains(&b.id)) {
1202            continue;
1203        }
1204        let Some(iv) = proven.buffers[b.id.idx()] else { continue };
1205        let fits = |p: &Precision| {
1206            let bits = precision_int_bits(*p).unwrap_or(64);
1207            bits >= 64 || (iv.0 >= -(1i64 << (bits - 1)) && iv.1 < (1i64 << (bits - 1)))
1208        };
1209        if let Some(&p) = storable.iter().find(|p| fits(p)) {
1210            if precision_int_bits(p).unwrap_or(64) < now {
1211                b.dtype = p;
1212            }
1213        }
1214    }
1215    t
1216}
1217
1218/// ★ THE FRONT END'S INTERVALS ARE THE PROGRAM'S DECLARED RANGES. Every integer tensor the walk proved an interval for
1219/// declares it — met with a range it already declares, both holding every value it takes — so a backend reads, from the
1220/// program alone, what each tensor can hold ([`super::value_range::program_ranges`]). Set before the composed program
1221/// is certified, so its certificate covers them.
1222fn declare_ranges(
1223    mut p: SpatialProgram,
1224    ranges: &std::collections::BTreeMap<String, super::value_range::Interval>,
1225) -> SpatialProgram {
1226    for t in &mut p.tensors {
1227        if super::target::precision_int_bits(t.dtype).is_none() {
1228            continue;
1229        }
1230        let Some(&r) = ranges.get(&t.name) else { continue };
1231        if r == super::value_range::UNBOUNDED {
1232            continue;
1233        }
1234        t.range = Some(match t.range {
1235            Some(d) if d.0.max(r.0) <= d.1.min(r.1) => (d.0.max(r.0), d.1.min(r.1)),
1236            _ => r,
1237        });
1238    }
1239    p
1240}
1241
1242/// The tables `source` lays out DENSE ([`super::link::dense_tables`]) — what a deployment's other programs must agree with.
1243pub fn dense_tables_of(
1244    source: &str,
1245    target: &SpatialTarget,
1246) -> Result<std::collections::BTreeSet<String>, SpatialReject> {
1247    let g = TensorGraph::lift_source(source, &|_| Storage::Values)?.graph;
1248    let every: Vec<usize> = (0..g.ops.len()).collect();
1249    Ok(dense_tables(&g, &every, target, &std::collections::BTreeSet::new()))
1250}
1251
1252/// The proven interval of every integer tensor of `source` ([`super::value_range::value_ranges`]), by name — the ranges
1253/// each matmul's operand precision is chosen from.
1254pub fn value_ranges_of(
1255    source: &str,
1256) -> Result<std::collections::BTreeMap<String, super::value_range::Interval>, SpatialReject> {
1257    let rp = TensorGraph::lift_source(source, &|_| Storage::Values)?.program;
1258    Ok(super::value_range::value_ranges(&rp, &float_tensors(&rp)))
1259}
1260
1261/// The dataflow the walk found, for a caller that wants the links without re-walking.
1262pub fn program_links(source: &str) -> Result<Vec<Link>, SpatialReject> {
1263    TensorGraph::lift_source(source, &|_| Storage::Values).map(|l| links(&l.program.stages))
1264}
1265
1266/// A calibration candidate: the compiler's own choice, or the best plan of one reader arrangement.
1267#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1268pub enum CandidateKind {
1269    Best,
1270    Streamed,
1271    Fused,
1272    Unfused,
1273}
1274
1275impl CandidateKind {
1276    /// The arrangement label a program of this kind prints (the ledger keys on it).
1277    pub fn label(self) -> &'static str {
1278        match self {
1279            CandidateKind::Best => "best",
1280            CandidateKind::Streamed => "streamed",
1281            CandidateKind::Fused => "multicast",
1282            CandidateKind::Unfused => "blocked",
1283        }
1284    }
1285}
1286
1287impl PlanSummary {
1288    /// The emergent arrangement, as a label.
1289    /// The arrangement a measurement names. The movement packing is part of it: two programs that differ
1290    /// only in which engine carries the store are DIFFERENT arrangements, and a ledger must not let one
1291    /// supersede the other.
1292    pub fn arrangement_label(&self) -> &'static str {
1293        let split = self.engines == EnginePacking::OperandWithStore;
1294        match (self.streaming, self.multicast_groups > 0, split) {
1295            (true, _, _) => "streamed",
1296            (_, true, false) => "multicast",
1297            (_, true, true) => "multicast-split",
1298            (_, false, false) => "blocked",
1299            (_, false, true) => "blocked-split",
1300        }
1301    }
1302}
1303
1304/// Every calibration candidate of a source on a target — the compiler's own choice plus the best plan of
1305/// each reader arrangement, each run through the SAME passes. A measurement harness's facility, never a
1306/// compiler decision.
1307/// Restrict the plans a candidate search may pick from — the knobs a measurement campaign varies one at a
1308/// time. A knob left `None` is the compiler's own choice.
1309#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1310pub struct PlanFilter {
1311    pub pack_accumulate: Option<bool>,
1312    pub engines: Option<EnginePacking>,
1313    /// The per-site output block in tiles.
1314    pub per_site: Option<(usize, usize)>,
1315    /// The sub-block and reduction depth the compute engine works in.
1316    pub block: Option<BlockShape>,
1317}
1318
1319impl PlanFilter {
1320    fn allows(&self, plan: &BlockPlan) -> bool {
1321        self.pack_accumulate
1322            .map(|w| plan.pack_accumulate == w)
1323            .unwrap_or(true)
1324            && self.engines.map(|w| plan.engines == w).unwrap_or(true)
1325            && self
1326                .per_site
1327                .map(|w| (plan.per_site_m, plan.per_site_n) == w)
1328                .unwrap_or(true)
1329            && self.block.map(|w| plan.block == w).unwrap_or(true)
1330    }
1331    fn is_empty(&self) -> bool {
1332        *self == PlanFilter::default()
1333    }
1334}
1335
1336pub fn calibration_candidates(
1337    source: &str,
1338    target: &SpatialTarget,
1339    numerics: NumericsPolicy,
1340) -> Result<Vec<(CandidateKind, SpatialProgram)>, SpatialReject> {
1341    calibration_candidates_matching(source, target, numerics, PlanFilter::default())
1342}
1343
1344/// The best candidate of each arrangement AMONG the plans the filter allows.
1345pub fn calibration_candidates_matching(
1346    source: &str,
1347    target: &SpatialTarget,
1348    numerics: NumericsPolicy,
1349    want: PlanFilter,
1350) -> Result<Vec<(CandidateKind, SpatialProgram)>, SpatialReject> {
1351    let template = template_from_source_with(source, target, numerics)?;
1352    let mut out = Vec::new();
1353    if want.is_empty() {
1354        out.push((CandidateKind::Best, run_passes(template.clone(), target)?));
1355    }
1356    let cands: Vec<tile::Candidate> =
1357        tile::candidates_where(&template, target, &|plan| want.allows(plan))
1358            .into_iter()
1359            .filter(|c| want.allows(&c.plan))
1360            .collect();
1361    // ★ THE PLAN SEARCH'S OWN CHOICE, restricted to each arrangement: `tile::best_among` admits and ranks
1362    // exactly as the compiler does, so every ledger row measures a plan the compiler would emit given that
1363    // arrangement, and one ranking serves both.
1364    let objective = crate::spatial::work::Objective::default();
1365    let pick = |filter: &dyn Fn(&tile::Candidate) -> bool| {
1366        let arrangement: Vec<tile::Candidate> =
1367            cands.iter().filter(|c| filter(c)).cloned().collect();
1368        tile::best_among(&arrangement, objective).cloned()
1369    };
1370    let variants: [(CandidateKind, Option<tile::Candidate>); 4] = [
1371        (
1372            CandidateKind::Best,
1373            if want.is_empty() {
1374                None
1375            } else {
1376                tile::best_among(&cands, objective).cloned()
1377            },
1378        ),
1379        (
1380            CandidateKind::Streamed,
1381            pick(&|c| super::cost::is_streaming(&c.plan)),
1382        ),
1383        (CandidateKind::Fused, pick(&|c| c.fused)),
1384        (
1385            CandidateKind::Unfused,
1386            pick(&|c| !c.fused && !super::cost::is_streaming(&c.plan)),
1387        ),
1388    ];
1389    for (kind, cand) in variants {
1390        let Some(c) = cand else { continue };
1391        out.push((kind, build_candidate(&template, &c, target)?));
1392    }
1393    Ok(out)
1394}
1395
1396/// The program candidate `c` of `template` builds: its plan applied, placed, fused as the candidate says,
1397/// sized, handshaken, persisted and verified. What the plan search priced in closed form is what this emits.
1398pub fn build_candidate(
1399    template: &SpatialProgram,
1400    c: &tile::Candidate,
1401    target: &SpatialTarget,
1402) -> Result<SpatialProgram, SpatialReject> {
1403    verify::verify(&build_candidate_uncertified(template, c, target)?, target)
1404}
1405
1406/// [`build_candidate`] short of its certificate: the candidate tiled, placed, sized and scheduled — what a cycle
1407/// twin times.
1408pub fn build_candidate_uncertified(
1409    template: &SpatialProgram,
1410    c: &tile::Candidate,
1411    target: &SpatialTarget,
1412) -> Result<SpatialProgram, SpatialReject> {
1413    let tiled = tile::apply_plan(template, c.plan, target);
1414    let placed = place::place(&tiled, target)?;
1415    let fused = fuse_readers::fuse_readers_forced(&placed, target, c.fused)?;
1416    let sized = size_buffers::size_buffers(&fused, target)?;
1417    let sched = synthesize_handshakes::synthesize_handshakes(&sized, target)?;
1418    persist::persist(&sched, target)
1419}