Skip to main content

logicaffeine_compile/codegen_cuda/
lane_link.rs

1//! The lane's link: each op of the one IR, as the shared optimizer leaves it under the lane's shim, to the launches the
2//! lane's kernels run it with.
3//!
4//! A lifted op takes the launch of its kind, refused in the lane's own words where no kernel computes it. A fused op of
5//! a class the lane links whole takes the one launch that class's kernel is — a norm with its residual sum or its
6//! sandwiched pre-norm, a product with its bias, a rotation with its per-head norm, a convolution with its SiLU and L2
7//! norm, the gated pair, the mixture with its shared expert or over a chunk's sorted rows, a launch over several parts —
8//! and a class it runs as its parts takes each part's launches in turn. A placement is a field of the launch: a part of
9//! every unit read in place, a result landing in rows of a table or units of a wider tensor, values read in the key rows.
10//! A producer that quantizes its result in its epilogue writes the quantized form beside, or instead of, its `f32`
11//! result. A wide chunk's latent attention runs expanded: its key half, its value half, then the attention.
12//!
13//! What a launch does is decided here; how it is scheduled — four rows a warp, the shared-row attention, the split
14//! counts, a batch's sequences as one launch — is the lane's schedule, after the link.
15
16use std::collections::BTreeMap;
17
18use crate::tensor_graph::front::TensorOpSpec;
19use crate::tensor_graph::fuse::{binary, class, gates_rows, gemv, leaves, Weights};
20use crate::tensor_graph::graph::{Access, Dst as IrDst, Fused, Op, RowSel as IrRowSel, Sym, TensorGraph};
21use crate::tensor_graph::shim::{Shim, Support};
22use crate::tensor_graph::symbols::ClassId;
23use crate::tensor_recognize::qwen::{Activation, GateAct, RowIndex, Window};
24use crate::tensor_recognize::RowReduceKind;
25
26use super::lane::kernels;
27use super::lane::plan::{
28    attended_span, Advance, ConvPart, Dst, GemmPart, Launch, Part, PreNorm, RopeSide, Router, RowSel, SharedExperts, StepIo, GEMV_ROWS,
29    QBLOCK,
30};
31use super::lane::weights::{Placement, Rows};
32use super::lane_shim::{chunks_apart, LaneShim, Latent};
33use super::qwen_emit::Q8MatKind;
34
35/// What the link reads beside an op: the optimized graph, where each constant lives, the lane's shim, and the names the
36/// step carries across steps.
37pub struct Link<'a> {
38    pub g: &'a TensorGraph,
39    pub constants: &'a BTreeMap<String, Placement>,
40    pub shim: &'a LaneShim,
41    pub io: &'a StepIo,
42}
43
44fn dst(d: &IrDst) -> Dst {
45    Dst {
46        buf: d.buf.clone(),
47        row: d.row.as_ref().map(|(sel, pitch)| {
48            let sel = match sel {
49                IrRowSel::Param(p) => RowSel::Param(p.clone()),
50                IrRowSel::Fixed(n) => RowSel::Fixed(*n),
51            };
52            (sel, *pitch)
53        }),
54        unit: d.unit.map(|u| Part { pitch: u.pitch, offset: u.offset }),
55    }
56}
57
58fn part(a: Access) -> Option<Part> {
59    match a {
60        Access::Whole => None,
61        Access::Strided(s) => Some(Part { pitch: s.pitch, offset: s.offset }),
62    }
63}
64
65/// The parameter a named row index reads, or the fixed row.
66fn row_sel(index: &RowIndex, name: Option<&String>) -> Result<RowSel, String> {
67    match index {
68        RowIndex::Named(_) => name.cloned().map(RowSel::Param).ok_or_else(|| "a row index with no operand".to_string()),
69        RowIndex::Literal(n) => Ok(RowSel::Fixed(*n as usize)),
70    }
71}
72
73impl Link<'_> {
74    fn numerics(&self) -> Q8MatKind {
75        self.shim.knobs.activations
76    }
77
78    fn rows_of(&self, t: &str) -> Option<&Rows> {
79        match self.constants.get(t) {
80            Some(Placement::Rows(r)) => Some(r),
81            _ => None,
82        }
83    }
84
85    fn values(&self, t: &str) -> bool {
86        matches!(self.constants.get(t), Some(Placement::Values(_)))
87    }
88
89    /// The launches `op` runs as, in order, each with the lifted ops it computes.
90    pub fn op(&self, op: &Op) -> Result<Vec<(Launch, Vec<usize>)>, String> {
91        let whole = |l: Launch| vec![(l, covered(op))];
92        match &op.sym {
93            Sym::View { offset } => Ok(whole(Launch::View { src: op.ins[0].clone(), offset: *offset, out: op.out.clone() })),
94            Sym::Quantize(_) => {
95                let n = self.g.tensors.get(&op.ins[0]).map(|t| t.len()).ok_or("a quantize of an unknown tensor")?;
96                Ok(whole(Launch::Quant { x: op.ins[0].clone(), n }))
97            }
98            Sym::Prim(spec) => {
99                let mut l = self.prim(op, spec)?;
100                self.place(op, &mut l);
101                Ok(whole(l))
102            }
103            Sym::Fused(f) => match self.shim.supports(f.class) {
104                Support::Native => self.class_launches(op, f),
105                Support::Decompose => f.parts.iter().map(|p| self.op(p)).collect::<Result<Vec<_>, _>>().map(|v| v.concat()),
106                Support::Refuse(why) => Err(format!("{}: {why}", f.class)),
107            },
108        }
109    }
110
111    /// The launches a fused op runs as when the lane links its class whole, whatever the shim answers for the class: an
112    /// error where the lane has no launch for the class, or where the op is one its kernels cannot run
113    /// ([`LaneShim::fits`]).
114    pub fn whole(&self, op: &Op) -> Result<Vec<(Launch, Vec<usize>)>, String> {
115        let Sym::Fused(f) = &op.sym else { return self.op(op) };
116        self.shim.fits(op, self.g)?;
117        self.class_launches(op, f)
118    }
119
120    /// The launches of a fused op of a class the lane links whole, each with the lifted ops it computes: a product or a
121    /// convolution in parts lists each part's in turn.
122    fn class_launches(&self, op: &Op, f: &Fused) -> Result<Vec<(Launch, Vec<usize>)>, String> {
123        match f.class {
124            ClassId::Mla => self.latent(op, f),
125            ClassId::AttnOutputGate if gates_rows(&f.parts) => self.gated_rows(op, f, false),
126            ClassId::GluActMulQuant => match &f.parts[0].sym {
127                Sym::Fused(gated) if gated.class == ClassId::AttnOutputGate && gates_rows(&gated.parts) => self.gated_rows(op, gated, true),
128                _ => self.class_launch(op, f).map(|l| vec![(l, covered(op))]),
129            },
130            _ => self.class_launch(op, f).map(|l| {
131                let ops = match l {
132                    Launch::GemmParts { .. } | Launch::ConvParts { .. } => f.parts.iter().flat_map(covered).collect(),
133                    _ => covered(op),
134                };
135                vec![(l, ops)]
136            }),
137        }
138    }
139
140    /// A batch's gated attentions (`attn_output_gate` over its sequences' rows): each sequence's attention storing its
141    /// output gated by its row of the gate, then the set of that output into its row of the gated product — the row the
142    /// batch's layout lays the output in, so the sets leave the step and the attentions run as one (`sequences`). Where
143    /// the product is read quantized (`quant`), each attention quantizes its gated row in its store too, and its set
144    /// carries the row's quantized form (PARITY-BATCH-ROW-QUANT). `op` is the op linked, the gated attentions `f` or a
145    /// fold of them and their quantize.
146    fn gated_rows(&self, op: &Op, f: &Fused, quant: bool) -> Result<Vec<(Launch, Vec<usize>)>, String> {
147        let (product, rows) = f.parts.split_last().ok_or("a gated attention of no parts")?;
148        let gated = binary(product).ok_or("an attention's gated product")?;
149        let act = gated.gate.ok_or("a gate's activation")?;
150        // The gated rows `f32` where something reads the product so; always, where it is not quantized.
151        let plain = !quant || !self.only_projections(&op.out);
152        let mut launches = Vec::new();
153        for set in rows {
154            let Some(TensorOpSpec::SetRow(sr)) = set.prim() else { continue };
155            let attention = rows.iter().find(|p| p.out == set.ins[1]).ok_or("a row set from no attention")?;
156            let index = row_sel(&sr.index, set.ins.get(2))?;
157            let RowSel::Fixed(row) = index else { return Err("a batch's attention set at a row the step names".into()) };
158            let mut l = self.one(attention)?;
159            let Launch::Attention { gate, goff, quant: q, f32, .. } = &mut l else { return Err("a gated attention".into()) };
160            (*gate, *goff, *q, *f32) = (Some((gated.b.clone(), act)), (row - 1) * sr.cols, quant, plain);
161            launches.push((l, covered(attention)));
162            let set_row = Launch::SetRow { table: op.out.clone(), index, src: set.ins[1].clone(), cols: sr.cols, count: sr.count, seq: None, f32: plain, quant };
163            launches.push((set_row, covered(set)));
164        }
165        // The product, and the quantize where it rides along, are computed in the attentions' stores: their lifted ops
166        // are counted with the last attention's.
167        let last = launches.iter().rposition(|(l, _)| matches!(l, Launch::Attention { .. })).ok_or("a batch's gate over no attention")?;
168        let counted: Vec<usize> = launches.iter().flat_map(|(_, ops)| ops.clone()).collect();
169        launches[last].1.extend(covered(op).into_iter().filter(|k| !counted.contains(k)));
170        launches[last].1.sort_unstable();
171        Ok(launches)
172    }
173
174    /// The placements an op's reads and result take, as its launch's fields.
175    fn place(&self, op: &Op, l: &mut Launch) {
176        if let Some(p) = part(op.access[0]) {
177            if let Launch::Norm { part, .. } | Launch::Rope { part, .. } | Launch::HeadGemv { part, .. } = l {
178                *part = Some(p);
179            }
180        }
181        if let (Launch::Attention { vstride, voff, .. }, Some(Access::Strided(s))) = (&mut *l, op.access.get(2)) {
182            (*vstride, *voff) = (s.pitch, s.offset);
183        }
184        if op.dst.own().is_some() {
185            return;
186        }
187        let to = dst(&op.dst);
188        match l {
189            Launch::Gemv { y, .. } | Launch::GemvF32 { y, .. } | Launch::Rope { y, .. } | Launch::Moe { y, .. } | Launch::Join { y, .. } | Launch::HeadGemv { y, .. } => *y = to,
190            Launch::Norm { into, .. } => *into = Some(to),
191            _ => {}
192        }
193    }
194
195    /// A lifted op's launch, as the lane lowers its kind.
196    fn prim(&self, op: &Op, spec: &TensorOpSpec) -> Result<Launch, String> {
197        let (ins, out) = (&op.ins, op.out.clone());
198        let numerics = self.numerics();
199        let (knobs, batch) = (&self.shim.knobs, self.shim.batch);
200        let refuse = |why: &str| Err(why.to_string());
201        Ok(match spec {
202            TensorOpSpec::Matmul(m) => {
203                if m.m == 0 {
204                    return refuse("a matmul of no rows");
205                }
206                let w = &ins[1];
207                if let Some(r) = self.rows_of(w) {
208                    if !r.transposed || r.rows != m.n || r.cols != m.k {
209                        return refuse("the weight's checkpoint rows are not the projection's outputs");
210                    }
211                    let rows = kernels::gemm_blk_rows(kernels::format_arg(r.ty), numerics == Q8MatKind::TwoPassInt8);
212                    let tiles = m.n.div_ceil(kernels::GEMM_TILE_N) * m.m.div_ceil(rows);
213                    let tail = match kernels::gemm_blk_tail(tiles, knobs.chip.sms, m.k.div_ceil(128)) {
214                        (whole, split) if split > 1 && m.m > GEMV_ROWS && numerics != Q8MatKind::Dequant && !batch => Some((whole, split)),
215                        _ => None,
216                    };
217                    Launch::Gemv {
218                        x: ins[0].clone(),
219                        w: w.clone(),
220                        format: r.ty,
221                        bias: None,
222                        y: Dst::to(&out),
223                        k: m.k,
224                        n: m.n,
225                        m: m.m,
226                        per_warp: 1,
227                        f32: true,
228                        quant: false,
229                        tail,
230                        batch,
231                    }
232                } else if self.values(w) {
233                    Launch::GemvF32 { x: ins[0].clone(), w: w.clone(), bias: None, y: Dst::to(&out), k: m.k, n: m.n, m: m.m, split: 1, batch }
234                } else {
235                    return refuse("a matmul by a computed matrix has no kernel");
236                }
237            }
238            TensorOpSpec::Elementwise(e) => Launch::Binary {
239                op: e.op,
240                a: ins[0].clone(),
241                b: ins[1].clone(),
242                gate: None,
243                row: e.row,
244                out,
245                f32: true,
246                quant: false,
247                n: e.n,
248            },
249            TensorOpSpec::SwiGlu(s) => Launch::SwiGlu { g: ins[0].clone(), u: ins[1].clone(), out, f32: true, quant: false, n: s.total },
250            TensorOpSpec::FusedRow(r) => {
251                if r.reduce != RowReduceKind::RmsNorm {
252                    return refuse("a softmax or layernorm row has no kernel in the lane yet");
253                }
254                if r.bias.is_some() {
255                    return refuse("an RMSNorm with a bias");
256                }
257                let mut it = ins.iter().skip(1);
258                let w = r.weight.map(|_| it.next().expect("the weight operand").clone());
259                let add = r.residual.map(|_| it.next().expect("the residual operand").clone());
260                Launch::Norm {
261                    x: ins[0].clone(),
262                    part: None,
263                    add,
264                    pre: None,
265                    sum: None,
266                    w,
267                    out,
268                    into: None,
269                    f32: true,
270                    quant: false,
271                    rows: r.rows,
272                    cols: r.cols,
273                    eps: r.eps,
274                }
275            }
276            TensorOpSpec::Rope(r) => {
277                if r.hd > 1024 || 2 * r.half > r.hd || r.rows == 0 || r.h % r.rows != 0 {
278                    return refuse("a head wider than 1024, a rotation wider than the head, or rows that do not divide the heads");
279                }
280                let pos = match &r.from {
281                    Some((RowIndex::Named(_), _)) => Some(ins[3].clone()),
282                    Some((RowIndex::Literal(_), _)) => return refuse("a rotation from a literal position"),
283                    None => None,
284                };
285                Launch::Rope {
286                    x: ins[0].clone(),
287                    part: None,
288                    norm: None,
289                    cos: ins[1].clone(),
290                    sin: ins[2].clone(),
291                    pos,
292                    y: Dst::to(&out),
293                    heads: r.h,
294                    per_row: r.h / r.rows,
295                    hd: r.hd,
296                    half: r.half,
297                }
298            }
299            TensorOpSpec::Silu(s) => Launch::Unary { act: Activation::Silu, x: ins[0].clone(), y: out, n: s.n },
300            TensorOpSpec::Sigmoid(s) => Launch::Unary { act: Activation::Sigmoid, x: ins[0].clone(), y: out, n: s.n },
301            TensorOpSpec::Gelu(s) => Launch::Unary { act: Activation::Gelu, x: ins[0].clone(), y: out, n: s.n },
302            TensorOpSpec::Scale(s) => Launch::Scale { x: ins[0].clone(), y: out, n: s.n, factor: s.factor.get() },
303            TensorOpSpec::Decay(d) => {
304                if d.rows == 0 || d.n % d.rows != 0 {
305                    return refuse("a decay whose rows do not divide its heads");
306                }
307                Launch::Decay { ar: ins[0].clone(), dt: ins[1].clone(), al: ins[2].clone(), y: out, n: d.n, heads: d.n / d.rows }
308            }
309            TensorOpSpec::L2Norm(l) => Launch::L2Norm { x: ins[0].clone(), y: out, rows: l.rows, cols: l.cols, eps: l.eps },
310            TensorOpSpec::CausalConv(c) => Launch::Conv {
311                x: ins[0].clone(),
312                w: ins[1].clone(),
313                ring: ins[2].clone(),
314                y: out,
315                chan: c.chan,
316                taps: c.taps,
317                rows: c.rows,
318                silu: false,
319                l2: None,
320            },
321            TensorOpSpec::GatedDelta(d) => {
322                if d.gate == GateAct::Sigmoid {
323                    return refuse("a gated delta gated by a sigmoid (`with sigmoid gate Z`)");
324                }
325                if d.d > 1024 || d.d % 32 != 0 {
326                    return refuse("a delta-rule head dim past 1024 or not a multiple of 32");
327                }
328                Launch::Delta {
329                    q: ins[0].clone(),
330                    k: ins[1].clone(),
331                    v: ins[2].clone(),
332                    z: ins[3].clone(),
333                    b: ins[4].clone(),
334                    a: ins[5].clone(),
335                    st: ins[6].clone(),
336                    gn: ins[7].clone(),
337                    out,
338                    f32: true,
339                    quant: false,
340                    hv: d.hv,
341                    hk: d.hk,
342                    d: d.d,
343                    rows: d.rows,
344                    eps: d.eps,
345                    seqs: 1,
346                }
347            }
348            TensorOpSpec::Softcap(_) => return refuse("a soft-capped tensor (`capped at C`)"),
349            TensorOpSpec::Attention(a) => {
350                if a.softcap.is_some() {
351                    return refuse("a soft-capped attention (`capped at C`)");
352                }
353                if a.kv == 0 || a.h % a.kv != 0 {
354                    return refuse("query heads that do not group evenly over the key/value heads");
355                }
356                if a.hd % 4 != 0 || a.vd % 2 != 0 {
357                    return refuse("a key head not a whole number of four-value words, or a value head of an odd width");
358                }
359                if matches!(a.window, Some(Window::Chunked(_))) {
360                    return refuse("a chunked-local attention — the lane's mask keeps a window's last positions");
361                }
362                let window = a.window.map(Window::span);
363                let span = attended_span(a.l, window);
364                let acc = kernels::accumulator(kernels::mode(numerics)).0;
365                let Some(per_block) = kernels::attn_heads_per_block(a.h / a.kv, a.hd, a.vd, span, knobs.chip.sms, knobs.chip.smem_kib_per_block * 1024, acc)
366                else {
367                    return refuse("a head wider than one block's shared memory holds");
368                };
369                let splits = kernels::attn_splits(a.h / per_block, span, knobs.chip.sms);
370                let mut rest = ins[3..].iter();
371                let pos = match &a.pos {
372                    Some(RowIndex::Named(_)) => rest.next().cloned(),
373                    Some(RowIndex::Literal(_)) => return refuse("an attention bounded by a literal position"),
374                    None => None,
375                };
376                Launch::Attention {
377                    q: ins[0].clone(),
378                    kc: ins[1].clone(),
379                    vc: ins[2].clone(),
380                    pos,
381                    out,
382                    heads: a.h,
383                    group: a.h / a.kv,
384                    hd: a.hd,
385                    vd: a.vd,
386                    len: a.l,
387                    rows: a.rows,
388                    window,
389                    sinks: a.sinks.map(|_| rest.next().cloned().expect("the sinks operand")),
390                    vstride: a.kv * a.vd,
391                    voff: 0,
392                    scale: a.scale.map_or(1.0 / (a.hd as f64).sqrt(), |s| s.get()),
393                    per_block,
394                    splits,
395                    parallel: chunks_apart(splits, a.rows, a.h / per_block, knobs.chip.sms),
396                    split: None,
397                    shared: None,
398                    seq: None,
399                    f32: true,
400                    quant: false,
401                    gate: None,
402                    goff: 0,
403                }
404            }
405            TensorOpSpec::Moe(m) => {
406                if !m.weighs_outputs_by_softmax() {
407                    return refuse("an expert mixture whose router is not a softmax weighting its experts' outputs — scored by sigmoids, biased in its choice, limited to groups, scaled, or weighting its inputs");
408                }
409                let ok = |t: &str, rows: usize, cols: usize, transposed: bool| self.rows_of(t).is_some_and(|r| r.rows == rows && r.cols == cols && r.transposed == transposed);
410                let router = if ok(&ins[1], m.ne, m.d, true) {
411                    Router::Rows(self.rows_of(&ins[1]).expect("rows").ty)
412                } else if self.values(&ins[1]) {
413                    Router::F32
414                } else {
415                    return refuse("an expert mixture whose router is neither quantized checkpoint rows nor values");
416                };
417                if !(ok(&ins[2], m.ne * m.dff, m.d, false) && ok(&ins[3], m.ne * m.dff, m.d, false) && ok(&ins[4], m.ne * m.d, m.dff, false)) {
418                    return refuse("an expert mixture whose experts are not quantized checkpoint rows");
419                }
420                let mut rest = ins[5..].iter();
421                let mut constant = |b: bool| -> Result<Option<String>, String> {
422                    match b.then(|| rest.next().cloned()).flatten() {
423                        Some(n) if self.values(&n) => Ok(Some(n)),
424                        Some(_) => Err("an expert mixture whose biases are not constant values".to_string()),
425                        None => Ok(None),
426                    }
427                };
428                let rb = constant(m.router_bias.is_some())?;
429                let biases = [constant(m.biases[0].is_some())?, constant(m.biases[1].is_some())?, constant(m.biases[2].is_some())?];
430                if m.k == 0 || m.k > 32 || m.ne > 8192 {
431                    return refuse("more than 32 experts kept, or more than 8192 experts");
432                }
433                Launch::Moe {
434                    x: ins[0].clone(),
435                    wr: ins[1].clone(),
436                    wg: ins[2].clone(),
437                    wu: ins[3].clone(),
438                    wd: ins[4].clone(),
439                    formats: [2, 3, 4].map(|k| self.rows_of(&ins[k]).expect("rows").ty),
440                    y: Dst::to(&out),
441                    d: m.d,
442                    dff: m.dff,
443                    experts: m.ne,
444                    k: m.k,
445                    router,
446                    rb,
447                    biases,
448                    renormalize: m.renormalize,
449                    act: m.act,
450                    shared: None,
451                    per_warp: 1,
452                    rows: 1,
453                    router_split: 1,
454                    batch,
455                }
456            }
457            TensorOpSpec::RowGather(r) => {
458                let table = ins[0].clone();
459                match (row_sel(&r.index, ins.get(1))?, self.constants.get(&table)) {
460                    (RowSel::Param(p), Some(Placement::Rows(rr))) if !rr.transposed && rr.rows == r.rows && rr.cols == r.cols => {
461                        Launch::Gather { table, index: p, out, cols: r.cols, rows: 1, format: Some(rr.ty), scale: None }
462                    }
463                    (RowSel::Param(p), Some(Placement::Values(_))) => Launch::Gather { table, index: p, out, cols: r.cols, rows: 1, format: None, scale: None },
464                    // A row of a computed table the optimizer left a copy: the table is written in place before the
465                    // row's last reader.
466                    (RowSel::Fixed(row), None) if row >= 1 && row <= r.rows => {
467                        Launch::Slice { x: table, y: out, groups: 1, every: r.rows * r.cols, from: (row - 1) * r.cols, width: r.cols }
468                    }
469                    _ => return refuse("a row of a constant table at a literal index, or of a table in no form a row reads"),
470                }
471            }
472            TensorOpSpec::RowsGather(r) => {
473                let table = ins[0].clone();
474                let format = match self.constants.get(&table) {
475                    Some(Placement::Rows(rr)) if !rr.transposed && rr.rows == r.rows && rr.cols == r.cols => Some(rr.ty),
476                    Some(Placement::Values(_)) => None,
477                    _ => return refuse("rows gathered from a table that is not a constant"),
478                };
479                Launch::Gather { table, index: ins[1].clone(), out, cols: r.cols, rows: r.count, format, scale: None }
480            }
481            TensorOpSpec::ArgMax(a) => Launch::ArgMax { x: ins[0].clone(), out, n: a.n, advance: None, rows: 1 },
482            TensorOpSpec::SetRow(r) => {
483                Launch::SetRow { table: ins[0].clone(), index: row_sel(&r.index, ins.get(2))?, src: ins[1].clone(), cols: r.cols, count: r.count, seq: None, f32: true, quant: false }
484            }
485            TensorOpSpec::Transpose(_) | TensorOpSpec::FullAttention(_) => return refuse("a transpose or an unmasked block attention has no kernel yet"),
486            TensorOpSpec::IntRequant(_) | TensorOpSpec::IntClamp(_) | TensorOpSpec::Relu(_) | TensorOpSpec::LutAttention(_) => {
487                return refuse("a requant, a clamp, a ReLU or a LUT attention has no kernel yet")
488            }
489            TensorOpSpec::HeadSlice(h) => Launch::Slice { x: ins[0].clone(), y: out, groups: h.n / h.every, every: h.every, from: h.from, width: h.width },
490            TensorOpSpec::Join(j) => Launch::Join { a: ins[0].clone(), b: ins[1].clone(), y: Dst::to(&out), rows: j.rows, heads: j.heads, ha: j.wa / j.heads, hb: j.wb / j.heads, yz: 0 },
491            TensorOpSpec::HeadMatmul(m) => {
492                let Some(r) = self.rows_of(&ins[1]) else { return refuse("a per-head matmul by a matrix that is not checkpoint rows") };
493                let rows_out = match (r.transposed, r.rows, r.cols) {
494                    (true, rows, cols) if rows == m.heads * m.b && cols == m.a => true,
495                    (false, rows, cols) if rows == m.heads * m.a && cols == m.b => false,
496                    _ => return refuse("a per-head matmul whose checkpoint rows are not each head's outputs or inputs"),
497                };
498                if m.a % QBLOCK != 0 || m.b % QBLOCK != 0 {
499                    return refuse("a per-head matmul whose heads are not whole 32-value blocks");
500                }
501                Launch::HeadGemv {
502                    x: ins[0].clone(),
503                    part: None,
504                    w: ins[1].clone(),
505                    format: r.ty,
506                    y: Dst::to(&out),
507                    m: m.m,
508                    heads: m.heads,
509                    a: m.a,
510                    b: m.b,
511                    rows_out,
512                    shared: false,
513                    upto: None,
514                    batch,
515                }
516            }
517        })
518    }
519
520    /// One launch of an op: its link, which is one launch.
521    fn one(&self, op: &Op) -> Result<Launch, String> {
522        let mut v = self.op(op)?;
523        match v.len() {
524            1 => Ok(v.remove(0).0),
525            n => Err(format!("a member the lane runs as one launch links to {n}")),
526        }
527    }
528
529    /// The launch a fused op of a class the lane links whole is.
530    fn class_launch(&self, op: &Op, f: &Fused) -> Result<Launch, String> {
531        let parts = &f.parts;
532        let mut l = match f.class {
533            ClassId::AddNorm => {
534                let mut l = self.one(&parts[1])?;
535                let Launch::Norm { x, add, sum, .. } = &mut l else { return Err("an add and norm over a norm".into()) };
536                let (a, b) = (parts[0].ins[0].clone(), parts[0].ins[1].clone());
537                (*x, *add, *sum) = (a, Some(b), op.keeps.first().cloned());
538                l
539            }
540            ClassId::NormThenAdd => {
541                let (inner, outer) = (&parts[0], &parts[1]);
542                let Launch::Norm { w, eps, .. } = self.one(inner)? else { return Err("a sandwich's inner norm".into()) };
543                let mut l = self.one(outer)?;
544                let Launch::Norm { x, add, pre, part, .. } = &mut l else { return Err("a sandwich's outer norm".into()) };
545                (*x, *add, *pre) = (op.ins[0].clone(), Some(op.ins[1].clone()), Some(PreNorm { w, eps }));
546                *part = self::part(op.access[0]);
547                l
548            }
549            // Each part run over what the parts before it wrote: a mixture's sum with another array between it and the norm.
550            ClassId::GemvNormEpilogue | ClassId::EmbedNorm | ClassId::MoeDeferredFinalize => {
551                let mut launches = parts.iter().map(|p| self.one(p));
552                let first = launches.next().ok_or("a fold of no parts")??;
553                let mut l = launches.try_fold(first, |first, then| Ok::<_, String>(Launch::Fold { first: Box::new(first), then: Box::new(then?) }))?;
554                // A projection takes its rows a warp in its fold as the GEMV takes them alone.
555                if let (Launch::Fold { first, .. }, Some(p)) = (&mut l, gemv(&parts[0], self.g)) {
556                    if let Launch::Gemv { per_warp, .. } = first.as_mut() {
557                        *per_warp = self.shim.per_warp(&p);
558                    }
559                }
560                l
561            }
562            // The set carries its rows' quantized form into the table's, as its source's producer stored it.
563            ClassId::RowWriteQuant => {
564                let mut l = self.one(&parts[0])?;
565                let Launch::SetRow { f32, quant, .. } = &mut l else { return Err("a row set that carries its quantized form".into()) };
566                (*f32, *quant) = (!self.only_projections(&op.out), true);
567                l
568            }
569            // The attention stores its output gated, the gate's input read where its producer wrote it.
570            ClassId::AttnOutputGate => {
571                let mut l = self.one(&parts[0])?;
572                let gated = binary(&parts[1]).ok_or("an attention's gated product")?;
573                let Launch::Attention { out, gate, .. } = &mut l else { return Err("a gated attention".into()) };
574                (*out, *gate) = (op.out.clone(), Some((gated.b, gated.gate.ok_or("a gate's activation")?)));
575                l
576            }
577            ClassId::GemmBias => {
578                let mut l = self.one(&parts[0])?;
579                let (Launch::Gemv { bias, y, .. } | Launch::GemvF32 { bias, y, .. }) = &mut l else { return Err("a bias on a product".into()) };
580                (*bias, *y) = (Some(op.ins[2].clone()), Dst::to(&op.out));
581                l
582            }
583            ClassId::QkNormRope => {
584                let Launch::Norm { w: Some(w), eps, .. } = self.one(&parts[0])? else { return Err("a rotation's per-head norm".into()) };
585                let mut l = self.one(&parts[1])?;
586                let Launch::Rope { x, norm, .. } = &mut l else { return Err("a normed rotation".into()) };
587                (*x, *norm) = (op.ins[0].clone(), Some((w, eps)));
588                l
589            }
590            ClassId::ConvBiasSilu | ClassId::ConvSiluL2norm => {
591                let mut l = self.one(&parts[0])?;
592                let Launch::Conv { y, silu, l2, .. } = &mut l else { return Err("a convolution's epilogue".into()) };
593                (*y, *silu) = (op.out.clone(), true);
594                if f.class == ClassId::ConvSiluL2norm {
595                    let Some(TensorOpSpec::L2Norm(n)) = parts[1].prim() else { return Err("a convolution's L2 norm".into()) };
596                    *l2 = Some((n.cols, n.eps));
597                }
598                l
599            }
600            ClassId::ConvParts => {
601                let mut convs = Vec::new();
602                for p in parts {
603                    let Launch::Conv { x, w, ring, y, chan, taps, l2, .. } = self.one(p)? else { return Err("a part of a launch of convolutions".into()) };
604                    convs.push((ConvPart { x, w, ring, y, chan, l2: l2.is_some() }, taps, l2));
605                }
606                let taps = convs[0].1;
607                let norm = convs.iter().find_map(|c| c.2);
608                let (block, eps) = match norm {
609                    Some((d, e)) => (d, Some(e)),
610                    None => (256, None),
611                };
612                Launch::ConvParts { parts: convs.into_iter().map(|c| c.0).collect(), taps, block, eps, seqs: 1 }
613            }
614            ClassId::GdnGating => {
615                let sigmoid = parts.iter().find(|p| matches!(p.prim(), Some(TensorOpSpec::Sigmoid(_)))).ok_or("the write strength")?;
616                let decay = parts.iter().find(|p| matches!(p.prim(), Some(TensorOpSpec::Decay(_)))).ok_or("the forget gate")?;
617                let Some(TensorOpSpec::Decay(d)) = decay.prim() else { unreachable!() };
618                Launch::Gates {
619                    br: sigmoid.ins[0].clone(),
620                    ar: decay.ins[0].clone(),
621                    dt: decay.ins[1].clone(),
622                    al: decay.ins[2].clone(),
623                    beta: sigmoid.out.clone(),
624                    decay: decay.out.clone(),
625                    n: d.n,
626                    heads: d.n / d.rows,
627                }
628            }
629            ClassId::GdnGateProjections => {
630                let gating = parts.iter().find(|p| class(p) == Some(ClassId::GdnGating)).ok_or("the gates")?;
631                let Launch::Gates { br, ar, dt, al, beta, decay, heads, .. } = self.one(gating)? else { return Err("the gates".into()) };
632                let product = |t: &str| parts.iter().find(|p| p.out == t).and_then(|p| gemv(p, self.g)).ok_or("a gate's product");
633                let (pb, pa) = (product(&br)?, product(&ar)?);
634                Launch::GateProj { x: pb.x, wb: pb.w, wa: pa.w, dt, al, beta, decay, k: pb.k, heads, parts: 1 }
635            }
636            ClassId::GluActMul => {
637                let act = &parts[0];
638                let gate = match act.prim() {
639                    Some(TensorOpSpec::Silu(_)) => Activation::Silu,
640                    Some(TensorOpSpec::Sigmoid(_)) => Activation::Sigmoid,
641                    Some(TensorOpSpec::Gelu(_)) => Activation::Gelu,
642                    _ => return Err("a gate's activation".into()),
643                };
644                let mut l = self.one(&parts[1])?;
645                let Launch::Binary { a, b, gate: g, .. } = &mut l else { return Err("a gated product".into()) };
646                let other = if *a == act.out { b.clone() } else { a.clone() };
647                (*a, *b, *g) = (other, act.ins[0].clone(), Some(gate));
648                l
649            }
650            ClassId::GemvGateUpGlu => {
651                let glu = parts.last().ok_or("a gated pair's activation")?;
652                let (g, u, act) = match glu.prim() {
653                    Some(TensorOpSpec::SwiGlu(_)) => (glu.ins[0].clone(), glu.ins[1].clone(), None),
654                    _ => match self.one(glu)? {
655                        Launch::Binary { a, b, gate: Some(act), .. } => (b, a, Some(act)),
656                        _ => return Err("a gated pair's activation".into()),
657                    },
658                };
659                let product = |t: &str| -> Result<Launch, String> { self.one(parts.iter().find(|p| p.out == t).ok_or("a gated pair's product")?) };
660                let (Launch::Gemv { x, w: wg, format: fg, bias: bg, k, n, m, tail, batch, .. }, Launch::Gemv { w: wu, format: fu, bias: bu, .. }) = (product(&g)?, product(&u)?) else {
661                    return Err("a gated pair of products by checkpoint rows".into());
662                };
663                let gemv_rows = m <= GEMV_ROWS || self.numerics() == Q8MatKind::Dequant || batch;
664                let paired = !gemv_rows && fg == fu && n % (kernels::GEMM_TILE_N / 2) == 0;
665                let tail = tail.filter(|_| paired).map(|(whole, split)| (2 * whole, split));
666                Launch::GemvGlu { x, wg, wu, formats: [fg, fu], biases: [bg, bu], act, out: op.out.clone(), f32: true, quant: false, k, n, m, per_warp: 1, tail, batch }
667            }
668            ClassId::SharedExpertFusion => {
669                let mixture = parts.iter().find(|p| matches!(p.prim(), Some(TensorOpSpec::Moe(_)))).ok_or("the mixture")?;
670                let glu = parts.iter().find(|p| class(p) == Some(ClassId::GemvGateUpGlu)).ok_or("the shared expert's gated pair")?;
671                let down = parts.iter().find(|p| gemv(p, self.g).is_some_and(|d| d.x == glu.out)).ok_or("the shared expert's down product")?;
672                let Launch::GemvGlu { wg, wu, formats: [fg, fu], n: sdff, .. } = self.one(glu)? else { return Err("the shared expert's gated pair".into()) };
673                let Launch::Gemv { w: wd, format: fd, .. } = self.one(down)? else { return Err("the shared expert's down product".into()) };
674                let mut l = self.one(mixture)?;
675                let Launch::Moe { y, shared, .. } = &mut l else { return Err("a mixture".into()) };
676                (*y, *shared) = (Dst::to(&op.out), Some(SharedExperts { wg, wu, wd, formats: [fg, fu, fd], dff: sdff }));
677                l
678            }
679            ClassId::MoeTokenSort => {
680                let mixtures: Vec<&Op> = parts.iter().filter(|p| matches!(p.prim(), Some(TensorOpSpec::Moe(_)))).collect();
681                let first = mixtures.first().ok_or("a chunk's mixtures")?;
682                let src = parts.iter().find(|p| matches!(p.sym, Sym::View { .. })).map(|v| v.ins[0].clone()).ok_or("the chunk's rows")?;
683                let mut l = self.prim(first, first.prim().expect("a mixture"))?;
684                let Launch::Moe { x, y, rows, .. } = &mut l else { return Err("a mixture".into()) };
685                (*x, *y, *rows) = (src, Dst::to(&op.out), mixtures.len());
686                l
687            }
688            ClassId::EmbedScale => {
689                let Some(TensorOpSpec::Scale(s)) = parts[1].prim() else { return Err("an embedding's scale".into()) };
690                let mut l = self.one(&parts[0])?;
691                let Launch::Gather { out, scale, .. } = &mut l else { return Err("a gather".into()) };
692                (*out, *scale) = (op.out.clone(), Some(s.factor.get()));
693                l
694            }
695            ClassId::RopeQkOneLaunch => {
696                let mut sides = Vec::new();
697                let mut shared = None;
698                for p in parts {
699                    let Launch::Rope { x, part, norm, y, heads, per_row, cos, sin, pos, hd, half } = self.one(p)? else { return Err("a side of a launch of rotations".into()) };
700                    shared.get_or_insert((cos, sin, pos, hd, half));
701                    sides.push(RopeSide { x, part, norm, y, heads, per_row, yz: 0 });
702                }
703                let (cos, sin, pos, hd, half) = shared.ok_or("a launch of rotations")?;
704                Launch::RopeParts { sides, cos, sin, pos, hd, half, seqs: false }
705            }
706            ClassId::FusedSampling => {
707                let mut l = self.one(&parts[0])?;
708                let Launch::ArgMax { out, advance, .. } = &mut l else { return Err("an arg-max".into()) };
709                let seqs: Vec<(&str, &str)> = std::iter::once((self.io.next.as_str(), self.io.position.as_str()))
710                    .chain(self.io.more.iter().map(|q| (q.next.as_str(), q.position.as_str())))
711                    .collect();
712                let item = seqs.iter().position(|(next, _)| next == out).ok_or("an arg-max that picks no sequence's token")?;
713                *advance = Some(Advance { token: self.io.token.clone(), position: seqs[item].1.to_string(), item });
714                l
715            }
716            ClassId::NormQuant
717            | ClassId::AddNormQuant
718            | ClassId::GluActMulQuant
719            | ClassId::ElementwiseQuant
720            | ClassId::GdnOutputQuant
721            | ClassId::GemvQuant
722            | ClassId::AttnOutputQuant => {
723                let mut l = self.one(&parts[0])?;
724                let only = self.only_projections(&op.out);
725                match &mut l {
726                    Launch::Norm { f32, quant, .. }
727                    | Launch::Binary { f32, quant, .. }
728                    | Launch::SwiGlu { f32, quant, .. }
729                    | Launch::Delta { f32, quant, .. }
730                    | Launch::GemvGlu { f32, quant, .. }
731                    | Launch::Gemv { f32, quant, .. }
732                    | Launch::Attention { f32, quant, .. } => (*quant, *f32) = (true, !only),
733                    Launch::Fold { then, .. } => match then.as_mut() {
734                        Launch::Norm { f32, quant, .. } => (*quant, *f32) = (true, !only),
735                        _ => return Err("a fold that quantizes its norm's result".into()),
736                    },
737                    _ => return Err("a producer that quantizes in its epilogue".into()),
738                }
739                l
740            }
741            ClassId::MergedQkv | ClassId::QkvHeadEpilogue => {
742                let members: Vec<&Op> = if f.class == ClassId::QkvHeadEpilogue { vec![op] } else { parts.iter().collect() };
743                let (mut products, mut sides) = (Vec::new(), Vec::new());
744                for m in members {
745                    let product = match &m.sym {
746                        Sym::Fused(e) if e.class == ClassId::QkvHeadEpilogue => {
747                            sides.push(self.one(&e.parts[1])?);
748                            &e.parts[0]
749                        }
750                        _ => m,
751                    };
752                    products.push(gemv(product, self.g).ok_or("products of one activation")?);
753                }
754                let first = &products[0];
755                let Weights::Rows(format) = first.weights else { return Err("products by checkpoint rows".into()) };
756                let gemm: Vec<GemmPart> = products.iter().map(|p| GemmPart { w: p.w.clone(), bias: p.bias.clone(), y: dst(&p.dst), n: p.n }).collect();
757                let projections = if self.shim.on_tensor_cores(first) {
758                    let n: usize = gemm.iter().map(|p| p.n).sum();
759                    let rows = kernels::gemm_blk_rows(kernels::format_arg(format), self.numerics() == Q8MatKind::TwoPassInt8);
760                    let tiles = n.div_ceil(kernels::GEMM_TILE_N) * first.m.div_ceil(rows);
761                    let tail = match kernels::gemm_blk_tail(tiles, self.shim.knobs.chip.sms, first.k.div_ceil(128)) {
762                        (whole, split) if split > 1 => Some((whole, split)),
763                        _ => None,
764                    };
765                    Launch::GemmParts { x: first.x.clone(), format, k: first.k, m: first.m, parts: gemm, tail, per_warp: 1, batch: false }
766                } else {
767                    Launch::GemmParts { x: first.x.clone(), format, k: first.k, m: first.m, parts: gemm, tail: None, per_warp: self.shim.per_warp(first), batch: self.shim.batch }
768                };
769                if sides.is_empty() {
770                    projections
771                } else {
772                    let mut turned = Vec::new();
773                    let mut shared = None;
774                    for side in sides {
775                        let Launch::Rope { x, part, norm, y, heads, per_row, cos, sin, pos, hd, half } = side else { return Err("a part's heads turned in its epilogue".into()) };
776                        shared.get_or_insert((cos, sin, pos, hd, half));
777                        turned.push(RopeSide { x, part, norm, y, heads, per_row, yz: 0 });
778                    }
779                    let (cos, sin, pos, hd, half) = shared.expect("a rotation");
780                    Launch::Fold { first: Box::new(projections), then: Box::new(Launch::RopeParts { sides: turned, cos, sin, pos, hd, half, seqs: false }) }
781                }
782            }
783            other => return Err(format!("the lane links `{other}` whole and has no launch for it")),
784        };
785        self.place(op, &mut l);
786        Ok(l)
787    }
788
789    /// Whether every op that reads `x` — itself, or a view of its whole blocks — reads it as a product's quantized
790    /// activation, and none reads it `f32` as well: the producer then writes only the quantized form.
791    fn only_projections(&self, x: &str) -> bool {
792        let mut names = vec![x.to_string()];
793        let mut seen: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
794        let mut readers: Vec<(&Op, String)> = Vec::new();
795        while let Some(t) = names.pop() {
796            if !seen.insert(t.clone()) {
797                continue;
798            }
799            for op in self.g.ops.iter().filter(|o| o.ins.contains(&t)) {
800                match &op.sym {
801                    Sym::View { offset } if offset % QBLOCK == 0 => names.push(op.out.clone()),
802                    // A row set carrying its rows' quantized form hands its source's rows to the table's readers; the
803                    // table it writes into it reads for no one.
804                    Sym::Fused(f) if f.class == ClassId::RowWriteQuant => {
805                        if op.ins.get(1) == Some(&t) {
806                            names.push(op.out.clone());
807                        }
808                    }
809                    _ => readers.push((op, t.clone())),
810                }
811            }
812        }
813        readers.iter().all(|(op, t)| {
814            let projected = op.ins.iter().enumerate().any(|(k, n)| n == t && self.shim.activation(op, k, self.g).is_some());
815            let f32_router = leaves((*op).clone()).iter().any(|l| matches!(l.prim(), Some(TensorOpSpec::Moe(_))) && self.values(&l.ins[1]));
816            projected && !f32_router && !matches!(op.sym, Sym::Quantize(_))
817        })
818    }
819
820    /// A wide chunk's latent attention, expanded: its three launches from the parts that define it absorbed — the key
821    /// half computing what the query's absorption did, the value half what the values' slice did, the attention the rest.
822    fn latent(&self, op: &Op, f: &Fused) -> Result<Vec<(Launch, Vec<usize>)>, String> {
823        let found = leaves(op.clone());
824        let attn = found.iter().find(|l| matches!(l.prim(), Some(TensorOpSpec::Attention(_)))).ok_or("a latent attention")?;
825        let Some(TensorOpSpec::Attention(a)) = attn.prim() else { unreachable!() };
826        let join = found.iter().find(|l| matches!(l.prim(), Some(TensorOpSpec::Join(_))) && l.out == attn.ins[0]).ok_or("the attention's joined query")?;
827        let heads_product = |x: &str| found.iter().find(|l| matches!(l.prim(), Some(TensorOpSpec::HeadMatmul(_))) && l.ins[0] == x);
828        let absorb = found.iter().find(|l| matches!(l.prim(), Some(TensorOpSpec::HeadMatmul(_))) && l.out == join.ins[0]).ok_or("the query's absorption")?;
829        let slice = found.iter().find(|l| l.out == absorb.ins[0]).ok_or("the query's unrotated part")?;
830        let expand = heads_product(&attn.out).ok_or("the value half")?;
831        let (Some(TensorOpSpec::HeadMatmul(ab)), Some(TensorOpSpec::HeadMatmul(ex))) = (absorb.prim(), expand.prim()) else { unreachable!() };
832        let (Some(rk), Some(rv)) = (self.rows_of(&absorb.ins[1]), self.rows_of(&expand.ins[1])) else { return Err("the expansion's checkpoint rows".into()) };
833        let pos = match &a.pos {
834            Some(RowIndex::Named(_)) => Some(attn.ins[3].clone()),
835            _ => None,
836        };
837        let latent = Latent {
838            qr: slice.ins[0].clone(),
839            qpp: join.ins[1].clone(),
840            kc: attn.ins[1].clone(),
841            pos,
842            heads: a.h,
843            kw: a.hd,
844            lora: a.vd,
845            nope: ab.a,
846            len: a.l,
847            rows: a.rows,
848            scale: a.scale.map_or(1.0 / (a.hd as f64).sqrt(), |s| s.get()),
849            wkb: absorb.ins[1].clone(),
850            fk: rk.ty,
851            kro: rk.transposed,
852            wvb: expand.ins[1].clone(),
853            fv: rv.ty,
854            vd: ex.b,
855            ct: f.parts.iter().map(|p| p.out.clone()).find(|o| *o == op.out).unwrap_or_else(|| op.out.clone()),
856        };
857        let [kh, vh, attend] = self.shim.latent_launches(&latent).ok_or_else(|| "a latent head wider than one block's shared memory holds".to_string())?;
858        let values = found.iter().find(|l| l.out == attn.ins[2]).ok_or("the values' slice")?;
859        let (keys_ops, values_ops) = (covered(absorb), covered(values));
860        let rest: Vec<usize> = covered(op).into_iter().filter(|k| !keys_ops.contains(k) && !values_ops.contains(k)).collect();
861        Ok(vec![(kh, keys_ops), (vh, values_ops), (attend, rest)])
862    }
863}
864
865/// The lifted ops an op computes, in order.
866fn covered(op: &Op) -> Vec<usize> {
867    let mut c = op.covers.clone();
868    c.sort_unstable();
869    c
870}
871
872/// The launches every op of `g` runs as, in order, each with the lifted ops it computes, or the first op the lane cannot
873/// link and why.
874pub fn link(g: &TensorGraph, constants: &BTreeMap<String, Placement>, shim: &LaneShim, io: &StepIo) -> Result<Vec<(Launch, Vec<usize>)>, (usize, String)> {
875    let l = Link { g, constants, shim, io };
876    let mut out = Vec::new();
877    for (k, op) in g.ops.iter().enumerate() {
878        out.extend(l.op(op).map_err(|why| (k, why))?);
879    }
880    Ok(out)
881}