Skip to main content

logicaffeine_compile/codegen_cuda/lane/
mod.rs

1//! The compiled generation lane: a model's decode step, written in LOGOS from its [`ModelSpec`] and compiled to one
2//! CUDA program that loads the checkpoint, runs the prompt and generates.
3//!
4//! The path is the compiler's own, end to end, and nothing on it is written for one model:
5//!
6//!   1. [`author::decode_step`] writes the step as a LOGOS program — the token and position in, every layer's KV
7//!      cache, convolution ring and delta-rule memory carried, the next token out — a cut of the model's one program
8//!      ([`program`]), whose identity the emitted program prints above the text it compiled;
9//!   2. the shared tensor front end lifts it into a [`StepGraph`](graph::StepGraph): every statement an op, or the
10//!      compile refused by name;
11//!   3. [`weights`] places every constant — quantized projections as rows read in place from the checkpoint, the
12//!      rest computed once into a sidecar;
13//!   4. [`plan`] gives each op a kernel from the [`kernels`] library, applies the fusion rules and lays out the
14//!      buffers;
15//!   5. [`arena`] places every array a step computes and reads within the step in one arena, by its lifetime;
16//!   6. [`emit`] prints the program, each step captured as one CUDA graph and replayed per token or per chunk.
17//!
18//! A lane has several forms of the step, compiled the same way: the decode step, and [`author::prefill`]'s chunks of
19//! 2, 4, … rows — the same layers over a block of prompt tokens, every projection reading its weights once for the
20//! block. They share the model's constants and its carried state; the prompt runs as the largest chunks that fit.
21//!
22//! The lane's knobs are [`CudaKnobs`]': `activations` picks the projections' numerics — `f32` activations with `f64`
23//! sums, a two-word ~14-bit int8 activation (the default), or llama.cpp's one int8 word — `cuda_graphs` captures each
24//! step, `fuse_quant` quantizes a projection's activation in its producer.
25
26pub mod arena;
27pub mod branches;
28pub mod emit;
29pub mod graph;
30pub mod kernels;
31pub mod plan;
32pub mod residence;
33pub mod weights;
34
35use std::collections::BTreeMap;
36use std::fmt;
37use std::path::Path;
38
39use crate::codegen_cuda::knobs::CudaKnobs;
40use crate::model::author::{self, NotYet};
41use crate::model::program::{self, Cut, Identity};
42use crate::model::gguf::Gguf;
43use crate::model::spec::{Mixer, ModelSpec};
44use crate::spatial::passes::recognize_stages::recognize_stages;
45
46use emit::{Checkpoint, Program};
47use graph::{GraphError, Role, StepGraph};
48use plan::{Schedule, SeqIo, StepIo};
49
50/// Why a model's step does not compile to the lane, by name.
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum LaneError {
53    /// The language cannot write the model yet.
54    NotYet(NotYet),
55    /// The authored step does not parse.
56    Parse(String),
57    /// The front end does not lift the step whole.
58    Graph(GraphError),
59    /// A constant the step declares that nothing binds.
60    Unbound(String),
61    /// A constant whose bound values are not the size the step declares.
62    Shape { var: String, declared: usize, bound: usize },
63    /// A checkpoint tensor in a format the lane reads no way.
64    Format { tensor: String, ty: &'static str, why: &'static str },
65    /// An op the library has no kernel for, in the form the step writes it.
66    NoKernel { op: usize, kind: &'static str, out: String, why: String },
67    /// A prefill chunk that is not a power of two up to [`plan::MAX_CHUNK`] rows and the caches' positions.
68    Chunk { rows: usize, ctx: usize },
69    /// A batch of fewer than two sequences: the decode step is the batch of one.
70    Batch { batch: usize },
71    /// The checkpoint could not be read.
72    Io(String),
73    /// Arrays the device must hold — its caches, arena and buffers, every matrix moved to host memory — past its memory.
74    Memory { needed: usize, budget: usize },
75}
76
77impl fmt::Display for LaneError {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        match self {
80            LaneError::NotYet(n) => write!(f, "{n}"),
81            LaneError::Parse(e) => write!(f, "the authored step does not parse: {e}"),
82            LaneError::Graph(e) => write!(f, "{e}"),
83            LaneError::Unbound(v) => write!(f, "nothing binds `{v}`"),
84            LaneError::Shape { var, declared, bound } => {
85                write!(f, "`{var}` is declared with {declared} values and bound to {bound}")
86            }
87            LaneError::Format { tensor, ty, why } => write!(f, "`{tensor}` is {ty}: {why}"),
88            LaneError::NoKernel { op, kind, out, why } => write!(f, "op {op} ({kind} into `{out}`) has no kernel: {why}"),
89            LaneError::Chunk { rows, ctx } => write!(
90                f,
91                "a prefill chunk of {rows} rows: a chunk is a power of two up to {} rows and the caches' {ctx} positions",
92                plan::MAX_CHUNK
93            ),
94            LaneError::Batch { batch } => write!(f, "a batch of {batch}: a batch step decodes two sequences or more"),
95            LaneError::Io(e) => write!(f, "the checkpoint: {e}"),
96            LaneError::Memory { needed, budget } => {
97                write!(f, "the caches, arena and buffers take {needed} bytes of the device's {budget}, every matrix moved to the host")
98            }
99        }
100    }
101}
102
103impl std::error::Error for LaneError {}
104
105impl From<GraphError> for LaneError {
106    fn from(e: GraphError) -> Self {
107        LaneError::Graph(e)
108    }
109}
110
111/// How a lane is compiled: its caches' length in positions, its knobs, and its largest prefill chunk (a
112/// power of two up to [`plan::MAX_CHUNK`]; 1 runs the prompt a decode step at a time).
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct LaneOptions {
115    pub ctx: usize,
116    pub knobs: CudaKnobs,
117    pub prefill: usize,
118}
119
120/// What a form of a lane computes: the decode step, a prefill chunk of one sequence's tokens, or a batch's step of one
121/// token for each of its sequences.
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum FormKind {
124    Decode,
125    Prefill,
126    Batch,
127}
128
129/// One form of a lane: the step of `rows` rows (1 for the decode step; a batch's, one a sequence), its LOGOS source, its
130/// graph, its schedule and where its step-local arrays live in the program's arena.
131#[derive(Clone, Debug)]
132pub struct Form {
133    pub rows: usize,
134    pub kind: FormKind,
135    /// The cut of the model's one program this form compiles, and the digest of its text.
136    pub identity: Identity,
137    pub source: String,
138    pub graph: StepGraph,
139    pub schedule: Schedule,
140    pub arena: arena::Arena,
141}
142
143/// A compiled lane: its forms, the decode step first, and the one program that runs them.
144#[derive(Clone, Debug)]
145pub struct Lane {
146    pub forms: Vec<Form>,
147    pub program: Program,
148    /// What the program allocates, and the order its matrices leave the device in when the memory it finds runs short.
149    pub plan: residence::Plan,
150    /// Where the program's matrices live on the chip the lane is compiled for (`CudaChip::mem_bytes`, less what the
151    /// driver, the context and the program's graphs take): the placement the program makes when it finds that memory.
152    pub residence: residence::Residence,
153}
154
155impl Form {
156    /// The name the program knows the form's step by: its rows, a batch's prefixed `b` (a batch of four and the chunk of
157    /// four are different steps).
158    pub fn tag(&self) -> String {
159        match self.kind {
160            FormKind::Batch => format!("b{}", self.rows),
161            FormKind::Decode | FormKind::Prefill => self.rows.to_string(),
162        }
163    }
164}
165
166impl Lane {
167    /// The decode step.
168    pub fn decode(&self) -> &Form {
169        &self.forms[0]
170    }
171
172    /// The prefill chunk of `rows` rows.
173    pub fn chunk(&self, rows: usize) -> Option<&Form> {
174        self.forms.iter().find(|f| f.kind == FormKind::Prefill && f.rows == rows)
175    }
176
177    /// The batch step, where the lane was compiled with one ([`compile_batch`]).
178    pub fn batch(&self) -> Option<&Form> {
179        self.forms.iter().find(|f| f.kind == FormKind::Batch)
180    }
181}
182
183/// The names the authored decode step reads and writes across steps.
184pub fn decode_io() -> StepIo {
185    StepIo { token: "tok".into(), position: "pos".into(), logits: "logits".into(), next: "next".into(), more: Vec::new() }
186}
187
188/// The names an authored prefill chunk reads and writes across steps.
189pub fn prefill_io() -> StepIo {
190    StepIo { token: "toks".into(), position: "pos_s0".into(), logits: "logits".into(), next: "next".into(), more: Vec::new() }
191}
192
193/// The names an authored batch of `rows` sequences reads and writes across steps: its tokens, one a sequence, and
194/// sequence `b`'s position `pos_s<b>`, logits `logits_s<b>` and next token `next_s<b>`.
195pub fn batch_io(rows: usize) -> StepIo {
196    StepIo {
197        token: "toks".into(),
198        position: "pos_s0".into(),
199        logits: "logits_s0".into(),
200        next: "next_s0".into(),
201        more: (1..rows).map(|b| SeqIo { position: format!("pos_s{b}"), logits: format!("logits_s{b}"), next: format!("next_s{b}") }).collect(),
202    }
203}
204
205/// Lower one authored form: lift it, place its constants, plan it.
206fn form(
207    spec: &ModelSpec,
208    g: &mut Gguf,
209    rows: usize,
210    kind: FormKind,
211    cut: Cut,
212    opts: &LaneOptions,
213    io: StepIo,
214    held: &[&str],
215) -> Result<Form, LaneError> {
216    let ((source, _, q8), identity) = program::written(spec, cut);
217    let rp = recognize_stages(&source).map_err(|e| LaneError::Parse(format!("{e:?}")))?;
218    let graph = StepGraph::build(&rp)?;
219    let mut constants = BTreeMap::new();
220    for t in graph.of_role(Role::Constant) {
221        let placed = weights::place(spec, g, &t.name, t.len(), q8.contains_key(&t.name), opts.ctx)?;
222        constants.insert(t.name.clone(), placed);
223    }
224    let schedule = plan::plan(&graph, &|s| rp.name(s).to_string(), constants, opts.knobs.activations, &opts.knobs, io, held)?;
225    let arena = arena::place(&schedule);
226    Ok(Form { rows, kind, identity, source, graph, schedule, arena })
227}
228
229/// Compile the lane of `spec` against the checkpoint at `checkpoint`: its decode step and its prefill chunks.
230pub fn compile(spec: &ModelSpec, checkpoint: &Path, opts: &LaneOptions) -> Result<Lane, LaneError> {
231    compile_holding(spec, checkpoint, opts, &[])
232}
233
234/// [`compile`] with the fusion rules `held` (of [`plan::RULES`]) held back, their launches kept apart: the program each
235/// fusion is checked against, computing what the fused one computes.
236pub fn compile_holding(spec: &ModelSpec, checkpoint: &Path, opts: &LaneOptions, held: &[&str]) -> Result<Lane, LaneError> {
237    compile_forms(spec, checkpoint, opts, 1, held)
238}
239
240/// [`compile`] with a batch step beside the decode step and the chunks: the model's batch cut (`Cut::Batch`) of `batch`
241/// sequences, one token each a step, every weight read once for all of them, each sequence its own position, caches and
242/// next token. The program decodes up to `batch` prompts together, each prefilled into its own sequence's state.
243pub fn compile_batch(spec: &ModelSpec, checkpoint: &Path, opts: &LaneOptions, batch: usize) -> Result<Lane, LaneError> {
244    compile_batch_holding(spec, checkpoint, opts, batch, &[])
245}
246
247/// [`compile_batch`] with the fusion rules `held` held back.
248pub fn compile_batch_holding(spec: &ModelSpec, checkpoint: &Path, opts: &LaneOptions, batch: usize, held: &[&str]) -> Result<Lane, LaneError> {
249    if batch < 2 {
250        return Err(LaneError::Batch { batch });
251    }
252    compile_forms(spec, checkpoint, opts, batch, held)
253}
254
255/// What the author writes that the lane's kernels do not take yet: soft-capped scores or logits, one fused query, key
256/// and value projection, values unlike the keys in width, and a QK norm across every head.
257fn lane_check(spec: &ModelSpec) -> Result<(), NotYet> {
258    if spec.final_logit_softcap.is_some() {
259        return Err(NotYet("soft-capped logits on the CUDA lane"));
260    }
261    for layer in &spec.layers {
262        if let Mixer::Attention(a) = &layer.mixer {
263            if a.softcap.is_some() {
264                return Err(NotYet("soft-capped attention scores on the CUDA lane"));
265            }
266            if a.fused_qkv {
267                return Err(NotYet("one fused query, key and value projection on the CUDA lane"));
268            }
269            if a.value_dim != a.head_dim {
270                return Err(NotYet("values unlike the keys in width on the CUDA lane"));
271            }
272            if a.qk_norm_whole {
273                return Err(NotYet("a QK norm across every head on the CUDA lane"));
274            }
275        }
276    }
277    Ok(())
278}
279
280fn compile_forms(spec: &ModelSpec, checkpoint: &Path, opts: &LaneOptions, batch: usize, held: &[&str]) -> Result<Lane, LaneError> {
281    author::check(spec).map_err(LaneError::NotYet)?;
282    lane_check(spec).map_err(LaneError::NotYet)?;
283    if !opts.prefill.is_power_of_two() || opts.prefill > plan::MAX_CHUNK || opts.prefill > opts.ctx {
284        return Err(LaneError::Chunk { rows: opts.prefill, ctx: opts.ctx });
285    }
286    let io = |e: std::io::Error| LaneError::Io(e.to_string());
287    let mut g = Gguf::open(checkpoint).map_err(io)?;
288    let mut forms = vec![form(spec, &mut g, 1, FormKind::Decode, Cut::Decode { ctx: opts.ctx }, opts, decode_io(), held)?];
289    let mut rows = opts.prefill;
290    while rows > 1 {
291        forms.push(form(spec, &mut g, rows, FormKind::Prefill, Cut::Prefill { rows, ctx: opts.ctx }, opts, prefill_io(), held)?);
292        rows /= 2;
293    }
294    if batch > 1 {
295        forms.push(form(spec, &mut g, batch, FormKind::Batch, Cut::Batch { rows: batch, ctx: opts.ctx }, opts, batch_io(batch), held)?);
296    }
297    let ckpt = Checkpoint {
298        size: std::fs::metadata(checkpoint).map_err(io)?.len(),
299        data_start: g.data_start(),
300        digest: g.header_digest().map_err(io)?,
301    };
302    let plan = residence::plan(&forms, opts.ctx);
303    let residence = residence::place(&plan, opts.knobs.chip.mem_bytes.saturating_sub(residence::CONTEXT_RESERVE + plan.spare))?;
304    let program = emit::emit(&forms, &opts.knobs, opts.ctx, ckpt, &plan)?;
305    Ok(Lane { forms, program, plan, residence })
306}