Skip to main content

logicaffeine_compile/
bringup.rs

1//! Model bring-up: a checkpoint's one program, lowered by every chip that runs it.
2//!
3//! A model is written once: [`crate::model::author`] writes its computation as a LOGOS program from the checkpoint's
4//! own spec, and [`crate::model::program`] names each cut of it. A chip is a [`Backend`]: it lowers the cuts it runs,
5//! writes its artifacts, and names the cut each artifact was lowered from — or refuses the model by name. Adding a
6//! model takes no backend code; adding a chip is one more [`Backend`] in [`backends`].
7//!
8//! [`bring_up`] runs a checkpoint through the backends asked for and reports, for each, the programs it lowered or why
9//! it refused, beside the identity of the decode step every one of them is a cut of. [`run_sequences`] then runs the
10//! model on every backend that brought it up and is on this machine, every [`Sequence`] it is given — a CPU tier as one
11//! generation program built once: the decode step's own statements in a loop over positions, the weights read once,
12//! each sequence's state declared afresh — each sequence judged by how many of its tokens lead its reference's (the
13//! model's f64 reference, or a golden's continuation). A chip that is not here, or whose run is not built yet, says
14//! which.
15
16use std::fmt;
17use std::path::{Path, PathBuf};
18use std::process::Command;
19use std::time::Instant;
20
21use crate::codegen_cuda::knobs::CudaKnobs;
22use crate::codegen_cuda::lane::{compile, LaneOptions};
23use crate::model::bind::{bind, rotary_values, values, Source};
24use crate::model::gguf::Gguf;
25use crate::model::program::{written, Cut, Identity};
26use crate::model::quant::GgmlType;
27use crate::model::spec::ModelSpec;
28use crate::ui_bridge::{interpret_for_ui_sync, set_engine, vm_listing, Engine};
29
30/// What a backend made of a model: the cut of the one program behind every artifact, and the files it wrote.
31#[derive(Clone, Debug)]
32pub struct Brought {
33    pub programs: Vec<Identity>,
34    pub files: Vec<PathBuf>,
35}
36
37/// Why a backend does not run a model, by name.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct Refused(pub String);
40
41impl fmt::Display for Refused {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str(&self.0)
44    }
45}
46
47/// A chip a model's one program is lowered to.
48pub trait Backend {
49    /// The chip, as the command line names it.
50    fn name(&self) -> &'static str;
51    /// Lower `spec`'s program — the checkpoint at `checkpoint` — over caches of `ctx` positions, writing the artifacts
52    /// under `out`.
53    fn bring_up(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused>;
54    /// Run the model on this chip, what [`Backend::bring_up`] wrote under `out` beside it: every one of `sequences`
55    /// from an empty state, its prompt (token ids from 0) and then as many tokens as it is judged against, generated
56    /// greedily through the one program — or refuse by name, a chip that is not on this machine, or whose run is not
57    /// built yet, saying which.
58    fn run(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> Result<Generated, Refused>;
59}
60
61fn io(path: &Path) -> impl Fn(std::io::Error) -> Refused + '_ {
62    move |e| Refused(format!("{}: {e}", path.display()))
63}
64
65fn write(path: PathBuf, bytes: impl AsRef<[u8]>, files: &mut Vec<PathBuf>) -> Result<(), Refused> {
66    if let Some(parent) = path.parent() {
67        std::fs::create_dir_all(parent).map_err(io(parent))?;
68    }
69    std::fs::write(&path, bytes).map_err(io(&path))?;
70    files.push(path);
71    Ok(())
72}
73
74/// One sequence a run generates: its prompt (token ids from 0), and the tokens it is judged against — as many as it
75/// generates.
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct Sequence {
78    pub prompt: Vec<u32>,
79    pub tokens: Vec<u32>,
80}
81
82/// What a chip generated for a run's sequences: their tokens, a list a sequence in their order (ids from 0); the
83/// seconds the runs took, not the building of what ran them; what ran them — the chip itself, or the model of it this
84/// machine holds; and the program it ran — the generation program a CPU tier built, or the artifact the chip runs.
85#[derive(Clone, Debug)]
86pub struct Generated {
87    pub tokens: Vec<Vec<u32>>,
88    pub seconds: f64,
89    pub on: String,
90    pub program: PathBuf,
91}
92
93impl Generated {
94    /// Generated tokens a second over every sequence, each prompt's inside the time.
95    pub fn tok_s(&self) -> f64 {
96        self.tokens.iter().map(Vec::len).sum::<usize>() as f64 / self.seconds.max(f64::MIN_POSITIVE)
97    }
98}
99
100/// A backend's run of one sequence: the tokens it generated after the prompt (ids from 0), the seconds from the prompt
101/// to the last token, and what ran them — the chip itself, or the model of it this machine holds.
102#[derive(Clone, Debug)]
103pub struct Ran {
104    pub tokens: Vec<u32>,
105    pub seconds: f64,
106    pub on: String,
107}
108
109impl Ran {
110    /// Generated tokens a second, the prompt's prefill inside the time.
111    pub fn tok_s(&self) -> f64 {
112        self.tokens.len() as f64 / self.seconds.max(f64::MIN_POSITIVE)
113    }
114}
115
116/// How many of `ran`'s tokens agree with `reference`'s before the first that does not.
117pub fn agreeing(ran: &[u32], reference: &[u32]) -> usize {
118    ran.iter().zip(reference).take_while(|(a, b)| a == b).count()
119}
120
121/// What a run is judged against: the tokens it should generate, and where they come from.
122#[derive(Clone, Debug)]
123pub struct Reference {
124    pub tokens: Vec<u32>,
125    pub what: String,
126}
127
128impl Reference {
129    /// The model's f64 reference ([`crate::model::reference`]) generating `tokens` tokens greedily after `prompt`,
130    /// reading the weights of the checkpoint at `checkpoint` — a GGUF file or a Hugging Face directory.
131    pub fn of(spec: &ModelSpec, checkpoint: &Path, prompt: &[u32], tokens: usize) -> Result<Reference, Refused> {
132        let g = Opened(crate::model::open_checkpoint(checkpoint).map_err(io(checkpoint))?);
133        let refused = |e: crate::model::reference::ReferenceError| Refused(format!("the f64 reference: {e}"));
134        let mut r = crate::model::reference::Reference::new(spec.clone(), g).map_err(refused)?;
135        let mut logits = r.step(prompt).map_err(refused)?;
136        let mut generated = Vec::with_capacity(tokens);
137        while generated.len() < tokens {
138            let next = crate::model::reference::argmax(&logits);
139            generated.push(next);
140            if generated.len() < tokens {
141                logits = r.step(&[next]).map_err(refused)?;
142            }
143        }
144        Ok(Reference { tokens: generated, what: "the f64 reference".to_string() })
145    }
146
147    /// A golden's first prompt as a run's prompt, and its continuation as the tokens the run is judged against
148    /// ([`Reference::golden_sequences`]).
149    pub fn golden(path: &Path) -> Result<(Vec<u32>, Reference), Refused> {
150        let (sequences, what) = Reference::golden_sequences(path)?;
151        let first = sequences.into_iter().next().ok_or_else(|| Refused(format!("{}: no prompt", path.display())))?;
152        Ok((first.prompt, Reference { tokens: first.tokens, what }))
153    }
154
155    /// Every prompt of a golden (`model_golden_lock`'s format: each of `prompts` with its `tokens` and its
156    /// `continuation`, ids from 0) as a sequence judged against its own continuation, and the golden, named.
157    pub fn golden_sequences(path: &Path) -> Result<(Vec<Sequence>, String), Refused> {
158        let text = std::fs::read_to_string(path).map_err(io(path))?;
159        let g: serde_json::Value = serde_json::from_str(&text).map_err(|e| Refused(format!("{}: {e}", path.display())))?;
160        let prompts = g["prompts"].as_array().filter(|p| !p.is_empty()).ok_or_else(|| Refused(format!("{}: no prompt", path.display())))?;
161        let ids = |i: usize, key: &str| -> Result<Vec<u32>, Refused> {
162            prompts[i][key]
163                .as_array()
164                .ok_or_else(|| Refused(format!("{}: prompt {} has no `{key}`", path.display(), i + 1)))?
165                .iter()
166                .map(|t| t.as_u64().map(|t| t as u32).ok_or_else(|| Refused(format!("{}: a `{key}` entry is not a token", path.display()))))
167                .collect()
168        };
169        let sequences = (0..prompts.len()).map(|i| Ok(Sequence { prompt: ids(i, "tokens")?, tokens: ids(i, "continuation")? })).collect::<Result<_, Refused>>()?;
170        Ok((sequences, format!("the golden {}", path.display())))
171    }
172}
173
174/// A checkpoint opened as either kind — a GGUF file or a Hugging Face directory — handed to what reads one source.
175struct Opened(Box<dyn crate::model::Checkpoint>);
176
177impl crate::model::reference::WeightSource for Opened {
178    fn describe(&self, name: &str) -> std::io::Result<(GgmlType, Vec<u64>)> {
179        crate::model::reference::WeightSource::describe(self.0.as_ref(), name)
180    }
181    fn row_bytes(&mut self, name: &str, first: usize, count: usize) -> std::io::Result<Vec<u8>> {
182        crate::model::reference::WeightSource::row_bytes(self.0.as_mut(), name, first, count)
183    }
184    fn has(&self, name: &str) -> bool {
185        crate::model::reference::WeightSource::has(self.0.as_ref(), name)
186    }
187}
188
189/// Whether `prompt` and `tokens` more fit caches of `ctx` positions: the prompt fills positions 1 to its length, and
190/// every generated token but the last is fed back at the next.
191fn fits(prompt: &[u32], tokens: usize, ctx: usize) -> Result<(), Refused> {
192    if prompt.is_empty() || prompt.len() + tokens > ctx + 1 {
193        return Err(Refused(format!("a prompt of {} tokens and {tokens} more do not fit {ctx} positions", prompt.len())));
194    }
195    Ok(())
196}
197
198/// The length a declaration's shape `R by C tensor of Real.` gives.
199fn declared_len(shape: &str) -> Option<usize> {
200    let (r, rest) = shape.split_once(" by ")?;
201    Some(r.trim().parse::<usize>().ok()? * rest.split_whitespace().next()?.parse::<usize>().ok()?)
202}
203
204/// `values` written to `path` as the file `readReals` reads: little-endian binary64, eight bytes a value in order.
205fn write_reals(path: &Path, values: &[f64]) -> Result<(), Refused> {
206    let bytes: Vec<u8> = values.iter().flat_map(|v| v.to_le_bytes()).collect();
207    std::fs::write(path, bytes).map_err(io(path))
208}
209
210/// The names a generation program wraps the decode step in; a step that names one of them is refused.
211const RUN_NAMES: [&str; 9] =
212    ["runTokens", "runStarts", "runLengths", "runCounts", "runSequence", "runStart", "runLength", "runCount", "runLast"];
213
214/// The names the decode step binds its two inputs to, the token and the position: the lines where the step at token 2,
215/// position 3 differs from the step at its placeholders, each `Let <name> be 1.` there.
216fn inputs(spec: &ModelSpec, ctx: usize, text: &str) -> Result<(String, String), Refused> {
217    let (other, _, _) = crate::model::program::decode_at(spec, "", ctx, 2, 3);
218    let (a, b): (Vec<&str>, Vec<&str>) = (text.lines().collect(), other.lines().collect());
219    if a.len() != b.len() {
220        return Err(Refused("the decode step's inputs change its lines".to_string()));
221    }
222    let (mut token, mut position) = (None, None);
223    for (x, y) in a.iter().zip(&b).filter(|(x, y)| x != y) {
224        let var = x.strip_prefix("Let ").and_then(|r| r.strip_suffix(" be 1.")).ok_or_else(|| Refused(format!("`{x}`: an input the step does not bind")))?;
225        match y.strip_prefix(&format!("Let {var} be ")) {
226            Some("2.") if token.is_none() => token = Some(var.to_string()),
227            Some("3.") if position.is_none() => position = Some(var.to_string()),
228            _ => return Err(Refused(format!("`{x}`: an input the step does not bind"))),
229        }
230    }
231    match (token, position) {
232        (Some(t), Some(p)) => Ok((t, p)),
233        _ => Err(Refused("the decode step binds no token or no position".to_string())),
234    }
235}
236
237/// The decode step as one generation program for `sequences`, written to `dir/generation.lg` with every weight's
238/// values beside it in the file `readReals` reads. Every weight is declared and read once, before the loops; each
239/// sequence declares the state its step carries afresh (the caches, memories and rings it binds as state), so none
240/// sees another's; and the step's own statements — every line of the decode step but the bindings of its token and
241/// position, in order — run in a loop over its positions, the prompt fed a token a step and each token after it the
242/// step's own `next`, shown from the prompt's last position on. A tensor of the step's own working stays in the loop,
243/// declared as the step declares it. Returns the program's path and text.
244fn generation(spec: &ModelSpec, checkpoint: &Path, ctx: usize, sequences: &[Sequence], dir: &Path) -> Result<(PathBuf, String), Refused> {
245    if sequences.is_empty() {
246        return Err(Refused("a run of no sequence".to_string()));
247    }
248    for s in sequences {
249        fits(&s.prompt, s.tokens.len(), ctx)?;
250    }
251    let ((text, f32w, q8w), _) = written(spec, Cut::Decode { ctx });
252    let (token, position) = inputs(spec, ctx, &text)?;
253    if let Some(name) = RUN_NAMES.iter().find(|n| text.contains(*n)) {
254        return Err(Refused(format!("the decode step names `{name}`, a name its generation program binds")));
255    }
256    std::fs::create_dir_all(dir).map_err(io(dir))?;
257    let mut opened = crate::model::open_checkpoint(checkpoint).map_err(io(checkpoint))?;
258    let source: &mut dyn crate::model::reference::WeightSource = opened.as_mut();
259    let (bound_token, bound_position) = (format!("Let {token} be 1."), format!("Let {position} be 1."));
260    let (mut weights, mut state, mut body) = (String::new(), String::new(), String::new());
261    for line in text.lines() {
262        if line == "## Main" || line.trim().is_empty() || line == bound_token || line == bound_position {
263            continue;
264        }
265        if let Some((var, shape)) = line.strip_prefix("Let ").and_then(|r| r.split_once(" be a ")) {
266            let named = f32w.contains_key(var) || q8w.contains_key(var);
267            let size = || declared_len(shape).ok_or_else(|| Refused(format!("`{line}`: a declaration a run cannot size")));
268            match bind(spec, var) {
269                Ok(Source::State) => {
270                    state.push_str(&format!("    {line}\n"));
271                    continue;
272                }
273                bound if named => {
274                    let v = match (rotary_values(spec, var, ctx, source).map_err(|e| Refused(format!("`{var}`: {e}")))?, bound) {
275                        (Some(v), _) => v,
276                        (None, Ok(Source::Weight(b))) => values(&b, source).map_err(|e| Refused(format!("`{var}`: {e}")))?,
277                        (None, _) => return Err(Refused(format!("`{var}`: the manifest names it and nothing binds it"))),
278                    };
279                    let len = size()?;
280                    if v.len() != len {
281                        return Err(Refused(format!("`{var}` declares {len} values and its source holds {}", v.len())));
282                    }
283                    let path = dir.join(format!("{var}.f64"));
284                    write_reals(&path, &v)?;
285                    weights.push_str(&format!("{line}\nSet {var} to readReals(\"{}\").\n", path.display()));
286                    continue;
287                }
288                Ok(Source::Weight(_)) => return Err(Refused(format!("`{var}` binds a weight the program's manifest does not name"))),
289                Err(_) => {}
290            }
291        }
292        body.push_str(&format!("        {line}\n"));
293    }
294    let list = |xs: Vec<usize>| xs.iter().map(usize::to_string).collect::<Vec<_>>().join(", ");
295    let starts = sequences.iter().scan(1, |at, s| {
296        let start = *at;
297        *at += s.prompt.len();
298        Some(start)
299    });
300    let mut out = String::from("## Main\n");
301    out.push_str(&weights);
302    out.push_str(&format!("Let runTokens be [{}].\n", list(sequences.iter().flat_map(|s| s.prompt.iter().map(|&t| t as usize + 1)).collect())));
303    out.push_str(&format!("Let runStarts be [{}].\n", list(starts.collect())));
304    out.push_str(&format!("Let runLengths be [{}].\n", list(sequences.iter().map(|s| s.prompt.len()).collect())));
305    out.push_str(&format!("Let runCounts be [{}].\n", list(sequences.iter().map(|s| s.tokens.len()).collect())));
306    out.push_str(&format!("Repeat for runSequence from 1 to {}:\n", sequences.len()));
307    out.push_str(&state);
308    out.push_str("    Let runStart be item runSequence of runStarts.\n");
309    out.push_str("    Let runLength be item runSequence of runLengths.\n");
310    out.push_str("    Let runCount be item runSequence of runCounts.\n");
311    out.push_str("    Let runLast be runLength + runCount - 1.\n");
312    out.push_str(&format!("    Let mutable {token} be item runStart of runTokens.\n"));
313    out.push_str(&format!("    Repeat for {position} from 1 to runLast:\n"));
314    out.push_str(&body);
315    out.push_str(&format!("        If {position} is at least runLength:\n            Show next.\n"));
316    out.push_str(&format!("        If {position} is less than runLength:\n            Set {token} to item (runStart + {position}) of runTokens.\n"));
317    out.push_str(&format!("        Otherwise:\n            Set {token} to next.\n"));
318    let path = dir.join("generation.lg");
319    std::fs::write(&path, &out).map_err(io(&path))?;
320    Ok((path, out))
321}
322
323/// A generation program's shown tokens read back: every sequence's, in order, from 1 in the program and from 0 here.
324fn read_generated(lines: &[String], sequences: &[Sequence]) -> Result<Vec<Vec<u32>>, Refused> {
325    let want: usize = sequences.iter().map(|s| s.tokens.len()).sum();
326    if lines.len() != want {
327        return Err(Refused(format!("the run showed {} tokens, {want} expected", lines.len())));
328    }
329    let mut shown = lines.iter();
330    sequences
331        .iter()
332        .map(|s| {
333            (0..s.tokens.len())
334                .map(|_| {
335                    let line = shown.next().map(|l| l.trim()).unwrap_or_default();
336                    let t: u32 = line.parse().map_err(|_| Refused(format!("the shown token `{line}` is not a token")))?;
337                    t.checked_sub(1).ok_or_else(|| Refused("the run's next token is 0, below the vocabulary".to_string()))
338                })
339                .collect()
340        })
341        .collect()
342}
343
344/// Generate every sequence on a CPU tier: the generation program written under `dir`, then built and run once by
345/// `go` (its text in, the lines it showed and the seconds its run took out).
346fn generate(
347    spec: &ModelSpec,
348    checkpoint: &Path,
349    ctx: usize,
350    sequences: &[Sequence],
351    on: &str,
352    dir: &Path,
353    go: impl FnOnce(&str) -> Result<(Vec<String>, f64), Refused>,
354) -> Result<Generated, Refused> {
355    let (program, text) = generation(spec, checkpoint, ctx, sequences, dir)?;
356    let (lines, seconds) = go(&text)?;
357    Ok(Generated { tokens: read_generated(&lines, sequences)?, seconds, on: on.to_string(), program })
358}
359
360/// One cut run in this process on `engine`: the lines it showed and the seconds the run took.
361fn in_process(engine: Engine, text: &str) -> Result<(Vec<String>, f64), Refused> {
362    crate::semantics::storage::reset();
363    set_engine(engine);
364    let start = Instant::now();
365    let result = interpret_for_ui_sync(text);
366    let seconds = start.elapsed().as_secs_f64();
367    set_engine(Engine::Auto);
368    match result.error {
369        Some(e) => Err(Refused(e)),
370        None => Ok((result.lines, seconds)),
371    }
372}
373
374/// Run a built program: the lines it wrote and the seconds it ran, or how it failed.
375fn timed(mut cmd: Command, what: &str) -> Result<(Vec<String>, f64), Refused> {
376    let start = Instant::now();
377    let o = cmd.output().map_err(|e| Refused(format!("{what}: {e}")))?;
378    let seconds = start.elapsed().as_secs_f64();
379    if !o.status.success() {
380        return Err(Refused(format!("{what} exited {:?}: {}", o.status.code(), String::from_utf8_lossy(&o.stderr).trim())));
381    }
382    Ok((String::from_utf8_lossy(&o.stdout).lines().map(str::to_string).collect(), seconds))
383}
384
385/// Build with `cmd`, or refuse with the tool's own report.
386fn built(mut cmd: Command, what: &str) -> Result<(), Refused> {
387    let o = cmd.output().map_err(|e| Refused(format!("{what}: {e}")))?;
388    if !o.status.success() {
389        return Err(Refused(format!("{what} failed: {}", String::from_utf8_lossy(&o.stderr).trim())));
390    }
391    Ok(())
392}
393
394/// Whether `tool` runs on this machine (`tool --version` succeeds).
395fn on_path(tool: &str) -> bool {
396    Command::new(tool).arg("--version").output().is_ok_and(|o| o.status.success())
397}
398
399/// A tier's compiler refusing a program, in the compiler's words.
400fn not_compiled(tier: &str, e: crate::ParseError) -> Refused {
401    Refused(format!("the {tier} backend does not compile the program: {:?}", e.kind))
402}
403
404/// The decode step's text, lowered by `lower` into `out/<file>` (each `(file, contents)` it returns): the artifacts a
405/// CPU tier's bring-up writes, every one of them the decode step's.
406fn lowered(spec: &ModelSpec, ctx: usize, out: &Path, lower: impl FnOnce(&str) -> Result<Vec<(&'static str, Vec<u8>)>, Refused>) -> Result<Brought, Refused> {
407    let ((text, _, _), decode) = written(spec, Cut::Decode { ctx });
408    let mut files = Vec::new();
409    for (file, bytes) in lower(&text)? {
410        write(out.join(file), bytes, &mut files)?;
411    }
412    Ok(Brought { programs: vec![decode], files })
413}
414
415/// The bytecode VM ([`crate::vm`]): the decode step compiled to the bytecode it runs, as `largo run --interpret`
416/// compiles it ([`vm_listing`]).
417pub struct Vm;
418
419impl Backend for Vm {
420    fn name(&self) -> &'static str {
421        "vm"
422    }
423
424    fn bring_up(&self, spec: &ModelSpec, _checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused> {
425        lowered(spec, ctx, out, |text| Ok(vec![("decode.bytecode", vm_listing(text).map_err(Refused)?.into_bytes())]))
426    }
427
428    fn run(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> Result<Generated, Refused> {
429        generate(spec, checkpoint, ctx, sequences, "the bytecode VM", &out.join("run"), |text| in_process(Engine::Vm, text))
430    }
431}
432
433/// The VM with its native tier: the same bytecode, the loops it runs hot compiled to machine code as it runs.
434pub struct Jit;
435
436impl Backend for Jit {
437    fn name(&self) -> &'static str {
438        "jit"
439    }
440
441    fn bring_up(&self, spec: &ModelSpec, _checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused> {
442        lowered(spec, ctx, out, |text| Ok(vec![("decode.bytecode", vm_listing(text).map_err(Refused)?.into_bytes())]))
443    }
444
445    fn run(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> Result<Generated, Refused> {
446        generate(spec, checkpoint, ctx, sequences, "the VM with its JIT", &out.join("run"), |text| in_process(Engine::Jit, text))
447    }
448}
449
450/// The C compiler a run builds with: `$CC`, else `cc`.
451fn c_compiler() -> String {
452    std::env::var("CC").ok().filter(|cc| !cc.trim().is_empty()).unwrap_or_else(|| "cc".to_string())
453}
454
455/// The CPU through C ([`crate::compile::compile_to_c`]): the decode step as one C program; a run builds its
456/// generation program once, with no contraction of a product and a sum into one fused multiply-add, whose single
457/// rounding the VM's arithmetic never takes, and runs it.
458pub struct C;
459
460impl Backend for C {
461    fn name(&self) -> &'static str {
462        "c"
463    }
464
465    fn bring_up(&self, spec: &ModelSpec, _checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused> {
466        lowered(spec, ctx, out, |text| {
467            Ok(vec![("decode.c", crate::compile::compile_to_c(text).map_err(|e| not_compiled("C", e))?.into_bytes())])
468        })
469    }
470
471    fn run(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> Result<Generated, Refused> {
472        let cc = c_compiler();
473        if !on_path(&cc) {
474            return Err(Refused(format!("no C compiler on this machine: `{cc}` does not run")));
475        }
476        let dir = out.join("run");
477        generate(spec, checkpoint, ctx, sequences, "the CPU, through C", &dir, |text| {
478            let (src, bin) = (dir.join("generation.c"), dir.join("generation"));
479            let c = crate::compile::compile_to_c(text).map_err(|e| not_compiled("C", e))?;
480            std::fs::write(&src, c).map_err(io(&src))?;
481            let mut build = Command::new(&cc);
482            build.args(["-O2", "-ffp-contract=off", "-o"]).arg(&bin).arg(&src).arg("-lm");
483            built(build, &cc)?;
484            timed(Command::new(&bin), "the C program")
485        })
486    }
487}
488
489/// A staged project's manifest cut to what a cut of the one program needs: the runtime's file reading (`persistence`,
490/// its weights' reader) and none of the rest of it (the network stack, the scheduler), tokio only when the program
491/// names it, and full optimization without link-time optimization — a cut builds in seconds where the whole runtime
492/// under link-time optimization takes many minutes. Every other line stays as staged.
493fn lean_manifest(staged: &str, rust: &str) -> String {
494    let mut out = String::with_capacity(staged.len());
495    for line in staged.lines() {
496        let trimmed = line.trim_start();
497        if trimmed.starts_with("lto =") || trimmed.starts_with("codegen-units =") {
498            continue;
499        }
500        if trimmed.starts_with("tokio =") && !rust.contains("tokio") {
501            continue;
502        }
503        let line = line
504            .replace("features = [\"full\", \"io-uring\"]", "features = [\"persistence\"]")
505            .replace("features = [\"full\"]", "features = [\"persistence\"]");
506        out.push_str(&line);
507        out.push('\n');
508    }
509    out
510}
511
512/// The CPU through Rust ([`crate::compile::compile_to_rust`]): the decode step as one Rust program; a run stages one
513/// project beside the artifacts ([`crate::compile::compile_to_dir`]) and builds its generation program into it in
514/// release, only its `src/main.rs` rewritten when the project is already staged, so the runtime crates build once a
515/// project.
516pub struct Rust;
517
518impl Backend for Rust {
519    fn name(&self) -> &'static str {
520        "rust"
521    }
522
523    fn bring_up(&self, spec: &ModelSpec, _checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused> {
524        lowered(spec, ctx, out, |text| {
525            Ok(vec![("decode.rs", crate::compile::compile_to_rust(text).map_err(|e| not_compiled("Rust", e))?.into_bytes())])
526        })
527    }
528
529    fn run(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> Result<Generated, Refused> {
530        if !on_path("cargo") {
531            return Err(Refused("no Rust toolchain on this machine: `cargo` does not run".to_string()));
532        }
533        let (dir, project) = (out.join("run"), out.join("project"));
534        generate(spec, checkpoint, ctx, sequences, "the CPU, through Rust", &dir, |text| {
535            let rust = crate::compile::compile_program_full(text).map_err(|e| not_compiled("Rust", e))?.rust_code;
536            if project.join("Cargo.toml").is_file() {
537                let main = project.join("src").join("main.rs");
538                std::fs::write(&main, &rust).map_err(io(&main))?;
539            } else {
540                crate::compile::compile_to_dir(text, &project).map_err(|e| Refused(format!("the Rust backend: {e}")))?;
541                let manifest = project.join("Cargo.toml");
542                let staged = std::fs::read_to_string(&manifest).map_err(io(&manifest))?;
543                std::fs::write(&manifest, lean_manifest(&staged, &rust)).map_err(io(&manifest))?;
544            }
545            let target = project.join("target");
546            let mut build = Command::new("cargo");
547            build.args(["build", "--release", "--quiet"]).current_dir(&project).env("CARGO_TARGET_DIR", &target);
548            built(build, "cargo build")?;
549            timed(Command::new(target.join("release").join("logos_output")), "the Rust program")
550        })
551    }
552}
553
554/// RISC-V through its AOT tier ([`crate::compile::compile_to_riscv`]): the decode step as RISC-V assembly; a run links
555/// its generation program with its runtime ([`crate::codegen_riscv::runtime`]) by `riscv64-linux-gnu-gcc` once and
556/// runs it under `qemu-riscv64`.
557pub struct RiscvQemu;
558
559impl Backend for RiscvQemu {
560    fn name(&self) -> &'static str {
561        "riscv-qemu"
562    }
563
564    fn bring_up(&self, spec: &ModelSpec, _checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused> {
565        lowered(spec, ctx, out, |text| {
566            Ok(vec![("decode.s", crate::compile::compile_to_riscv(text).map_err(|e| not_compiled("RISC-V", e))?.into_bytes())])
567        })
568    }
569
570    fn run(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> Result<Generated, Refused> {
571        for tool in ["riscv64-linux-gnu-gcc", "qemu-riscv64"] {
572            if !on_path(tool) {
573                return Err(Refused(format!("no RISC-V toolchain on this machine: `{tool}` does not run")));
574            }
575        }
576        let dir = out.join("run");
577        generate(spec, checkpoint, ctx, sequences, "RISC-V under qemu", &dir, |text| {
578            let asm = crate::compile::compile_to_riscv(text).map_err(|e| not_compiled("RISC-V", e))?;
579            let runtime = crate::codegen_riscv::runtime(&asm).map_err(|e| Refused(format!("the RISC-V runtime: {e}")))?;
580            let (s, rt, bin) = (dir.join("generation.s"), dir.join("logos_rt.c"), dir.join("generation"));
581            std::fs::write(&s, &asm).map_err(io(&s))?;
582            std::fs::write(&rt, &runtime.c).map_err(io(&rt))?;
583            let mut link = Command::new("riscv64-linux-gnu-gcc");
584            link.args(["-O2", "-static", "-o"]).arg(&bin).arg(&s).arg(&rt).args(&runtime.link_args);
585            built(link, "riscv64-linux-gnu-gcc")?;
586            let mut qemu = Command::new("qemu-riscv64");
587            qemu.arg(&bin);
588            timed(qemu, "qemu-riscv64")
589        })
590    }
591}
592
593/// WebAssembly ([`crate::compile::compile_to_wasm`]): the decode step as a module with the host shim `largo build
594/// --emit wasm` writes beside it ([`crate::web_host::wasm_host_shim`]); a run executes its generation program's module
595/// under node.
596pub struct Wasm;
597
598impl Backend for Wasm {
599    fn name(&self) -> &'static str {
600        "wasm"
601    }
602
603    fn bring_up(&self, spec: &ModelSpec, _checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused> {
604        lowered(spec, ctx, out, |text| {
605            let module = crate::compile::compile_to_wasm(text).map_err(|e| not_compiled("WebAssembly", e))?;
606            Ok(vec![("decode.wasm", module), ("decode.mjs", crate::web_host::wasm_host_shim("decode").into_bytes())])
607        })
608    }
609
610    fn run(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> Result<Generated, Refused> {
611        if !on_path("node") {
612            return Err(Refused("no WebAssembly engine on this machine: `node` does not run".to_string()));
613        }
614        let dir = out.join("run");
615        generate(spec, checkpoint, ctx, sequences, "WebAssembly under node", &dir, |text| {
616            let module = crate::compile::compile_to_wasm(text).map_err(|e| not_compiled("WebAssembly", e))?;
617            let (wasm, host) = (dir.join("generation.wasm"), dir.join("generation.mjs"));
618            std::fs::write(&wasm, module).map_err(io(&wasm))?;
619            std::fs::write(&host, crate::web_host::wasm_host_shim("generation")).map_err(io(&host))?;
620            let mut node = Command::new("node");
621            node.arg(&host);
622            timed(node, "node")
623        })
624    }
625}
626
627/// The CUDA lane ([`crate::codegen_cuda::lane`]): the decode step and its prefill chunks as one fused CUDA program
628/// and the sidecar of computed constants it reads.
629pub struct Cuda;
630
631impl Backend for Cuda {
632    fn name(&self) -> &'static str {
633        "cuda"
634    }
635
636    fn bring_up(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused> {
637        let prefill = (1..=16usize).rev().find(|r: &usize| r.is_power_of_two() && *r <= ctx).unwrap_or(1);
638        let opts = LaneOptions { ctx, knobs: CudaKnobs::default(), prefill };
639        let lane = compile(spec, checkpoint, &opts).map_err(|e| Refused(e.to_string()))?;
640        let mut files = Vec::new();
641        write(out.join("lane.cu"), &lane.program.cu, &mut files)?;
642        write(out.join("lane.side"), &lane.program.sidecar, &mut files)?;
643        Ok(Brought { programs: lane.forms.iter().map(|f| f.identity).collect(), files })
644    }
645
646    /// The lane built by `nvcc` for this machine's GPU once and run on the checkpoint a sequence a run: its own clocks
647    /// time each prompt's prefill and every decode step after its first token.
648    fn run(&self, _spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> Result<Generated, Refused> {
649        for s in sequences {
650            fits(&s.prompt, s.tokens.len(), ctx)?;
651        }
652        let gpu = Command::new("nvidia-smi").args(["--query-gpu=compute_cap", "--format=csv,noheader"]).output();
653        let cap = match gpu {
654            Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).lines().next().unwrap_or("").trim().replace('.', ""),
655            _ => String::new(),
656        };
657        if cap.is_empty() || !on_path("nvcc") {
658            return Err(Refused("no CUDA GPU on this machine: the lane needs nvcc and a device nvidia-smi lists".to_string()));
659        }
660        let bin = out.join("lane");
661        let mut nvcc = Command::new("nvcc");
662        nvcc.args(["-O3", "-std=c++17", &format!("-arch=sm_{cap}"), "-o"]).arg(&bin).arg(out.join("lane.cu"));
663        built(nvcc, "nvcc")?;
664        let (mut tokens, mut seconds) = (Vec::with_capacity(sequences.len()), 0.0);
665        for s in sequences {
666            let n = s.tokens.len();
667            if n == 0 {
668                tokens.push(Vec::new());
669                continue;
670            }
671            let o = Command::new(&bin)
672                .arg(checkpoint)
673                .arg(out.join("lane.side"))
674                .arg(n.to_string())
675                .args(s.prompt.iter().map(u32::to_string))
676                .output()
677                .map_err(|e| Refused(format!("the lane: {e}")))?;
678            let stdout = String::from_utf8_lossy(&o.stdout);
679            if !o.status.success() {
680                return Err(Refused(format!("the lane exited {:?}: {}", o.status.code(), String::from_utf8_lossy(&o.stderr).trim())));
681            }
682            let field = |key: &str| stdout.lines().find_map(|l| l.strip_prefix(key)).map(str::trim);
683            let generated: Result<Vec<u32>, _> = field("TOKENS:").unwrap_or("").split_whitespace().map(str::parse).collect();
684            let generated = generated.map_err(|_| Refused("the lane printed a token that is not a number".to_string()))?;
685            let number = |key: &str| field(key).and_then(|v| v.parse::<f64>().ok());
686            let prefill = number("PREFILL_MS:").ok_or_else(|| Refused("the lane printed no prefill time".to_string()))?;
687            let step = if n > 1 { number("DECODE_MS_PER_TOKEN:").ok_or_else(|| Refused("the lane printed no decode time".to_string()))? } else { 0.0 };
688            seconds += (prefill + step * (n - 1) as f64) / 1000.0;
689            tokens.push(generated);
690        }
691        Ok(Generated { tokens, seconds, on: "the GPU".to_string(), program: out.join("lane.cu") })
692    }
693}
694
695/// The D5005 decode engine ([`crate::spatial::decode_engine`]): the step's program for the engine's sequencer, the
696/// checkpoint held to what the engine's units read.
697pub struct D5005;
698
699impl Backend for D5005 {
700    fn name(&self) -> &'static str {
701        "d5005"
702    }
703
704    fn bring_up(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused> {
705        use crate::spatial::decode_engine::capability::{refusals, summarize};
706        use crate::spatial::decode_engine::model::EngineNumerics;
707        use crate::spatial::decode_engine::program::lower;
708        let g = Gguf::open(checkpoint).map_err(io(checkpoint))?;
709        let tensors: Vec<(String, GgmlType)> = g
710            .tensors()
711            .iter()
712            .filter_map(|t| GgmlType::from_id(t.ggml_type).ok().map(|ty| (t.name.clone(), ty)))
713            .collect();
714        let gaps = refusals(spec, &tensors);
715        if !gaps.is_empty() {
716            return Err(Refused(summarize(&gaps, spec.layers.len()).join("; ")));
717        }
718        let lowered = lower(spec, EngineNumerics::fastest(), ctx).map_err(|e| Refused(e.to_string()))?;
719        let mut files = Vec::new();
720        let commands: String = lowered.program.commands.iter().map(|c| format!("{c:?}\n")).collect();
721        let words: String = lowered
722            .program
723            .encode()
724            .iter()
725            .map(|w| format!("{:016x}{:016x}{:016x}{:016x}\n", w[3], w[2], w[1], w[0]))
726            .collect();
727        write(out.join("commands.txt"), commands, &mut files)?;
728        write(out.join("words.hex"), words, &mut files)?;
729        write(out.join("program.lg"), lowered.identity.cut.write(spec, "").0, &mut files)?;
730        write(out.join("program.txt"), format!("{}\n", lowered.identity.line()), &mut files)?;
731        Ok(Brought { programs: vec![lowered.identity], files })
732    }
733
734    /// The card is not on this machine, so the engine's machine model runs the program: the same jobs over the same
735    /// memory image the card executes, token for token as the card (`d5005_engine_card_lock`), at this computer's speed
736    /// — each sequence on a machine of its own, from the empty state the card starts a sequence in.
737    fn run(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> Result<Generated, Refused> {
738        use crate::spatial::decode_engine::machine::Machine;
739        use crate::spatial::decode_engine::model::EngineNumerics;
740        for s in sequences {
741            fits(&s.prompt, s.tokens.len(), ctx)?;
742        }
743        let refused = |e: crate::spatial::decode_engine::model::EngineError| Refused(format!("the engine's machine model: {e:?}"));
744        let start = Instant::now();
745        let mut tokens = Vec::with_capacity(sequences.len());
746        for sequence in sequences {
747            let g = Gguf::open(checkpoint).map_err(io(checkpoint))?;
748            let mut m = Machine::new(spec.clone(), g, EngineNumerics::fastest(), 1, ctx).map_err(refused)?;
749            let s = m.sequence().map_err(refused)?;
750            let mut feed = sequence.prompt.clone();
751            let mut generated = Vec::with_capacity(sequence.tokens.len());
752            while generated.len() < sequence.tokens.len() {
753                let logits = m.step(&[(s, feed)]).map_err(refused)?;
754                let last = logits.first().and_then(|e| e.last()).ok_or_else(|| Refused("the machine returned no logits".to_string()))?;
755                let next = (0..last.len()).fold(0, |best, i| if last[i] > last[best] { i } else { best }) as u32;
756                generated.push(next);
757                feed = vec![next];
758            }
759            tokens.push(generated);
760        }
761        Ok(Generated {
762            tokens,
763            seconds: start.elapsed().as_secs_f64(),
764            on: "the engine's machine model (no card on this machine)".to_string(),
765            program: out.join("program.lg"),
766        })
767    }
768}
769
770/// The SambaNova SN50 ([`crate::spatial::rdu_chip`]): the decode deployment as one certified program per layer kind,
771/// the embedding and the head, bound layer by layer by a manifest — SDK-gated, its placements MODELED.
772pub struct Sn50;
773
774impl Backend for Sn50 {
775    fn name(&self) -> &'static str {
776        "sn50"
777    }
778
779    fn bring_up(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused> {
780        use crate::compile::{compile_model_to_rdu_streamed, RduModel, StepShape};
781        use crate::spatial::rdu_chip::RduChip;
782        let mut source = crate::model::open_checkpoint(checkpoint).map_err(|e| Refused(format!("{}: {e}", checkpoint.display())))?;
783        let model = RduModel { spec: spec.clone(), ..RduModel::from_checkpoint(&spec.arch, source.as_mut()).map_err(Refused)? };
784        let mut files = Vec::new();
785        let mut written_files: Vec<(PathBuf, String)> = Vec::new();
786        let bundle = compile_model_to_rdu_streamed(&model, RduChip::Sn50, "weights", Some(StepShape { ctx, batch: 1, tp: 1, prefill: None }), |f| {
787            let dir = out.join(&f.dir);
788            written_files.push((dir.join("neutral.json"), f.neutral_json));
789            written_files.push((dir.join("placement.txt"), f.placement_txt));
790            written_files.push((dir.join("certificate.json"), f.certificate_json));
791            Ok(())
792        })
793        .map_err(Refused)?;
794        for (path, text) in written_files {
795            write(path, text, &mut files)?;
796        }
797        for (path, bytes) in &bundle.constants {
798            write(out.join(path), bytes, &mut files)?;
799        }
800        write(out.join("manifest.json"), &bundle.manifest, &mut files)?;
801        write(out.join("program.lg"), &bundle.program, &mut files)?;
802        let whole = written(spec, Cut::Decode { ctx }).1;
803        manifest_names(&bundle.manifest, &whole)?;
804        Ok(Brought { programs: vec![whole], files })
805    }
806
807    fn run(&self, _spec: &ModelSpec, _checkpoint: &Path, _ctx: usize, _out: &Path, _sequences: &[Sequence]) -> Result<Generated, Refused> {
808        Err(Refused("the SN50 runs its bundle under SambaNova's SDK, which is gated; no SN50 on this machine".to_string()))
809    }
810}
811
812/// The spatial fabric the compiler generates for an FPGA: it does not lower a model the author writes yet, and says so.
813pub struct Fabric;
814
815impl Fabric {
816    fn not_yet() -> Refused {
817        Refused("the fabric does not lower a model the author writes yet".to_string())
818    }
819}
820
821impl Backend for Fabric {
822    fn name(&self) -> &'static str {
823        "fabric"
824    }
825
826    fn bring_up(&self, _spec: &ModelSpec, _checkpoint: &Path, _ctx: usize, _out: &Path) -> Result<Brought, Refused> {
827        Err(Fabric::not_yet())
828    }
829
830    fn run(&self, _spec: &ModelSpec, _checkpoint: &Path, _ctx: usize, _out: &Path, _sequences: &[Sequence]) -> Result<Generated, Refused> {
831        Err(Fabric::not_yet())
832    }
833}
834
835/// Whether a bundle's manifest names `whole`, the decode step it was lowered from, as its `program`: the identity a
836/// backend reports is the one its artifact carries.
837fn manifest_names(manifest: &str, whole: &Identity) -> Result<(), Refused> {
838    let m: serde_json::Value = serde_json::from_str(manifest).map_err(|e| Refused(format!("the manifest: {e}")))?;
839    let Some(line) = m["program"].as_str() else {
840        return Err(Refused("the manifest names no program".to_string()));
841    };
842    if line != whole.line() {
843        return Err(Refused(format!("the bundle names `{line}`, not the decode step `{}`", whole.line())));
844    }
845    Ok(())
846}
847
848/// Every chip a model is brought up on, in the order the command line lists them: the CPU tiers, then the accelerators.
849pub fn backends() -> Vec<Box<dyn Backend>> {
850    vec![
851        Box::new(Vm),
852        Box::new(Jit),
853        Box::new(C),
854        Box::new(Rust),
855        Box::new(RiscvQemu),
856        Box::new(Wasm),
857        Box::new(Cuda),
858        Box::new(D5005),
859        Box::new(Sn50),
860        Box::new(Fabric),
861    ]
862}
863
864/// A checkpoint brought up: its spec, the identity of its decode step (the program every backend's cuts are cuts
865/// of), its text, and each backend asked for with what it made or why it refused.
866pub struct Bringup {
867    pub spec: ModelSpec,
868    pub decode: Identity,
869    pub program: String,
870    pub results: Vec<(&'static str, Result<Brought, Refused>)>,
871}
872
873/// Bring the checkpoint at `checkpoint` — a GGUF file, or a Hugging Face directory (`config.json` beside its
874/// safetensors) — up on the backends named in `targets` (every one when empty), over caches of `ctx` positions, each
875/// backend's artifacts under `out/<name>/` and the model's program as `out/program.lg`: the spec read from the
876/// checkpoint's own metadata ([`bring_up_spec`]).
877pub fn bring_up(checkpoint: &Path, ctx: usize, out: &Path, targets: &[String]) -> Result<Bringup, Refused> {
878    let source = crate::model::open_checkpoint(checkpoint).map_err(io(checkpoint))?;
879    let spec = ModelSpec::from_source(source.as_ref()).map_err(|e| Refused(format!("{}: {e}", checkpoint.display())))?;
880    bring_up_spec(spec, checkpoint, ctx, out, targets)
881}
882
883/// [`bring_up`] for `spec`, whose weights the checkpoint at `checkpoint` holds.
884pub fn bring_up_spec(spec: ModelSpec, checkpoint: &Path, ctx: usize, out: &Path, targets: &[String]) -> Result<Bringup, Refused> {
885    crate::model::author::check(&spec).map_err(|e| Refused(format!("the language cannot write this model yet: {e}")))?;
886    let unknown: Vec<&String> = targets.iter().filter(|t| !backends().iter().any(|b| b.name() == t.as_str())).collect();
887    if !unknown.is_empty() {
888        let known: Vec<&str> = backends().iter().map(|b| b.name()).collect();
889        return Err(Refused(format!("no backend named {unknown:?}; the backends are {known:?}")));
890    }
891    let ((program, _, _), decode) = written(&spec, Cut::Decode { ctx });
892    std::fs::create_dir_all(out).map_err(io(out))?;
893    std::fs::write(out.join("program.lg"), &program).map_err(io(out))?;
894    let results = backends()
895        .into_iter()
896        .filter(|b| targets.is_empty() || targets.iter().any(|t| t == b.name()))
897        .map(|b| (b.name(), b.bring_up(&spec, checkpoint, ctx, &out.join(b.name()))))
898        .collect();
899    Ok(Bringup { spec, decode, program, results })
900}
901
902/// The runs of a model brought up: the prompt, what they are judged against, and each backend that brought the model
903/// up with what it generated or why it did not run here.
904pub struct Runs {
905    pub prompt: Vec<u32>,
906    pub reference: Reference,
907    pub results: Vec<(&'static str, Result<Ran, Refused>)>,
908}
909
910/// Run the model `b` brought up on every backend that brought it up, `prompt` first, for as many tokens as
911/// `reference` holds, in the order the backends were asked for: [`run_sequences`] of the one sequence.
912pub fn run(b: &Bringup, checkpoint: &Path, ctx: usize, out: &Path, prompt: &[u32], reference: Reference) -> Runs {
913    let one = [Sequence { prompt: prompt.to_vec(), tokens: reference.tokens.clone() }];
914    let results = run_sequences(b, checkpoint, ctx, out, &one)
915        .results
916        .into_iter()
917        .map(|(chip, r)| (chip, r.map(|g| Ran { tokens: g.tokens.into_iter().next().unwrap_or_default(), seconds: g.seconds, on: g.on })))
918        .collect();
919    Runs { prompt: prompt.to_vec(), reference, results }
920}
921
922/// The runs of several sequences: the sequences, and each backend that brought the model up with what it generated for
923/// them or why it did not run here.
924pub struct SequenceRuns {
925    pub sequences: Vec<Sequence>,
926    pub results: Vec<(&'static str, Result<Generated, Refused>)>,
927}
928
929/// Run the model `b` brought up on every backend that brought it up, every one of `sequences` from an empty state, in
930/// the order the backends were asked for.
931pub fn run_sequences(b: &Bringup, checkpoint: &Path, ctx: usize, out: &Path, sequences: &[Sequence]) -> SequenceRuns {
932    let all = backends();
933    let results = b
934        .results
935        .iter()
936        .filter(|(_, brought)| brought.is_ok())
937        .filter_map(|(name, _)| all.iter().find(|x| x.name() == *name))
938        .map(|backend| (backend.name(), backend.run(&b.spec, checkpoint, ctx, &out.join(backend.name()), sequences)))
939        .collect();
940    SequenceRuns { sequences: sequences.to_vec(), results }
941}
942
943#[cfg(test)]
944#[path = "../../logicaffeine_tests/unit/logicaffeine_compile/bringup/tests.rs"]
945mod tests;