1use 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#[derive(Clone, Debug)]
32pub struct Brought {
33 pub programs: Vec<Identity>,
34 pub files: Vec<PathBuf>,
35}
36
37#[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
47pub trait Backend {
49 fn name(&self) -> &'static str;
51 fn bring_up(&self, spec: &ModelSpec, checkpoint: &Path, ctx: usize, out: &Path) -> Result<Brought, Refused>;
54 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#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct Sequence {
78 pub prompt: Vec<u32>,
79 pub tokens: Vec<u32>,
80}
81
82#[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 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#[derive(Clone, Debug)]
103pub struct Ran {
104 pub tokens: Vec<u32>,
105 pub seconds: f64,
106 pub on: String,
107}
108
109impl Ran {
110 pub fn tok_s(&self) -> f64 {
112 self.tokens.len() as f64 / self.seconds.max(f64::MIN_POSITIVE)
113 }
114}
115
116pub fn agreeing(ran: &[u32], reference: &[u32]) -> usize {
118 ran.iter().zip(reference).take_while(|(a, b)| a == b).count()
119}
120
121#[derive(Clone, Debug)]
123pub struct Reference {
124 pub tokens: Vec<u32>,
125 pub what: String,
126}
127
128impl Reference {
129 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 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 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
174struct 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
189fn 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
198fn 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
204fn 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
210const RUN_NAMES: [&str; 9] =
212 ["runTokens", "runStarts", "runLengths", "runCounts", "runSequence", "runStart", "runLength", "runCount", "runLast"];
213
214fn 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
237fn 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
323fn 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
344fn 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
360fn 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
374fn 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
385fn 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
394fn on_path(tool: &str) -> bool {
396 Command::new(tool).arg("--version").output().is_ok_and(|o| o.status.success())
397}
398
399fn not_compiled(tier: &str, e: crate::ParseError) -> Refused {
401 Refused(format!("the {tier} backend does not compile the program: {:?}", e.kind))
402}
403
404fn 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
415pub 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
433pub 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
450fn c_compiler() -> String {
452 std::env::var("CC").ok().filter(|cc| !cc.trim().is_empty()).unwrap_or_else(|| "cc".to_string())
453}
454
455pub 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
489fn 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
512pub 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
554pub 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
593pub 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
627pub 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 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
695pub 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 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
770pub 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
812pub 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
835fn 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
848pub 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
864pub 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
873pub 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
883pub 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
902pub struct Runs {
905 pub prompt: Vec<u32>,
906 pub reference: Reference,
907 pub results: Vec<(&'static str, Result<Ran, Refused>)>,
908}
909
910pub 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
922pub struct SequenceRuns {
925 pub sequences: Vec<Sequence>,
926 pub results: Vec<(&'static str, Result<Generated, Refused>)>,
927}
928
929pub 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;