Skip to main content

logicaffeine_compile/codegen_cuda/
lane_shim.rs

1//! What the compiled generation lane runs whole: the CUDA lane's answer to the shared optimizer.
2//!
3//! The shared rules decide a fusion by dataflow; the lane's kernels decide whether a fused op has a launch. Every
4//! condition here is a fact about a kernel of the library the lane links:
5//!
6//!   • a per-head product reads a part of every unit of its input in place only at whole 32-value blocks, the quantized
7//!     form's;
8//!   • an attention reads its values in place only from an even place at an even pitch, a pair at a time;
9//!   • a convolution's epilogue takes a per-head L2 norm over heads of at most 1024 values, whole warps of them, that
10//!     divide its channels;
11//!   • a gated pair of products is one launch as GEMVs (a decode step's rows, a batch's, or the precise numerics), or on
12//!     the tensor cores as one GEMM of the gate's and up's tiles side by side where their formats agree and the hidden
13//!     width is whole pair tiles;
14//!   • a mixture folds its shared expert into a block of at most 32 warps of kept experts, and never writes its result at
15//!     a row a parameter names;
16//!   • a product by checkpoint rows reads its activation quantized under the lane's quantized numerics — one int8 word a
17//!     value, or two — and a producer quantizes in its epilogue under the `fuse_quant` knob where its kernel can: a norm,
18//!     an elementwise op, a SwiGLU, a delta rule, a gated pair whose hidden width is whole 32-value blocks, a product of
19//!     four rows a warp whose outputs are whole blocks;
20//!   • several products of one activation are one tensor-core GEMM in parts — at most four, one format, each a whole
21//!     number of 128-output tiles — above the GEMV's rows.
22//!
23//! The lane's own rules link into the table: a wide chunk's latent attention taken whole before the shared rules (the
24//! lane runs it expanded), and the decode step's products of one activation as one GEMV in parts before the products
25//! on the tensor cores are merged.
26
27use std::collections::BTreeSet;
28
29use crate::codegen_cuda::knobs::{CachePrecision, CudaKnobs, StatePrecision};
30use crate::codegen_cuda::qwen_emit::Q8MatKind;
31use crate::tensor_graph::facts;
32use crate::tensor_graph::fuse::{class, conv, gemv, leaves, moe, norm, rope, slice, unit_output, ChipRule, ConvForm, GemvForm, Rewriter, Weights};
33use crate::tensor_graph::front::TensorOpSpec;
34use crate::tensor_graph::graph::{Access, Op, QuantFormat, RowSel, Storage, Sym, TensorGraph};
35use crate::tensor_graph::numerics::{holds_accumulator, AttentionNumerics, Block, Bound, Contract, Keep, Scale, Scheme, Taken};
36use crate::tensor_graph::shim::{Position, Shim, Support};
37use crate::tensor_graph::symbols::ClassId;
38use crate::tensor_recognize::qwen::{GateAct, MoeAct, RowIndex};
39use crate::tensor_recognize::ElementwiseOp;
40
41use super::lane::kernels;
42use super::lane::plan::{attended_span, Dst as LaneDst, Launch, Part as LanePart, Split, GEMV_ROWS, QBLOCK};
43
44/// The convolutions one launch takes as parts.
45const CONV_PARTS: usize = 4;
46
47/// The rotations one launch takes: a layer's query and its key.
48const ROPE_SIDES: usize = 2;
49
50/// The lane under its knobs, compiling a form whose products are a batch's (each row its own sequence) or not, with the
51/// lane rules `held` back.
52#[derive(Clone, Debug)]
53pub struct LaneShim {
54    pub knobs: CudaKnobs,
55    pub batch: bool,
56    pub held: Vec<&'static str>,
57}
58
59impl LaneShim {
60    /// The output rows a warp of a product's kernel takes: four for a wide product in a block format met one row at a
61    /// time (`rows_a_warp`), one otherwise.
62    pub fn per_warp(&self, p: &GemvForm) -> usize {
63        match p.weights {
64            Weights::Rows(format) if !self.held.contains(&"rows_a_warp") => kernels::rows_a_warp(format, p.n, p.k, p.m),
65            _ => 1,
66        }
67    }
68
69    /// A decode mixture's down projection launch (`kernels::moe_down_launch`): its experts' intermediate width from the
70    /// mixture's own spec, its shared expert's from that expert's down projection where it is folded in.
71    fn moe_down_launch(op: &Op, g: &TensorGraph) -> Option<(usize, usize)> {
72        let m = moe(op)?;
73        let dff = leaves(op.clone()).iter().find_map(|p| match p.prim()? {
74            TensorOpSpec::Moe(s) => Some(s.dff),
75            _ => None,
76        })?;
77        let sdff = match &op.sym {
78            Sym::Fused(f) if f.class == ClassId::SharedExpertFusion => gemv(f.parts.get(2)?, g)?.k,
79            _ => 0,
80        };
81        Some(kernels::moe_down_launch(m.d, dff, m.k, sdff))
82    }
83
84    /// Whether a product multiplies on the int8 tensor cores: past the GEMV's rows, not a batch's, under the quantized
85    /// numerics, by checkpoint rows.
86    pub fn on_tensor_cores(&self, p: &GemvForm) -> bool {
87        p.m > GEMV_ROWS && !self.batch && self.knobs.activations != Q8MatKind::Dequant && matches!(p.weights, Weights::Rows(_))
88    }
89
90    /// Whether a product lands where a GEMM in parts can write it: a buffer of its own, or a table's rows its width wide.
91    fn lands_whole(p: &GemvForm) -> bool {
92        p.dst.unit.is_none() && p.dst.row.as_ref().map_or(true, |(_, pitch)| *pitch == p.n)
93    }
94
95    /// A product's form, where it is a plain product by checkpoint rows (not one that quantizes its output).
96    fn rows_product(op: &Op, g: &TensorGraph) -> Option<GemvForm> {
97        gemv(op, g).filter(|p| matches!(p.weights, Weights::Rows(_)))
98    }
99
100    /// The product a member of a GEMV in parts computes: a plain product by checkpoint rows, or one that turns its
101    /// heads in its epilogue.
102    fn part_product(op: &Op, g: &TensorGraph) -> Option<GemvForm> {
103        match &op.sym {
104            Sym::Fused(f) if f.class == ClassId::QkvHeadEpilogue => Self::rows_product(&f.parts[0], g),
105            _ => Self::rows_product(op, g),
106        }
107    }
108
109    /// Whether the members of a GEMV in parts that turn their heads in its epilogue turn them as one launch of
110    /// rotations would: at most a query's and a key's, by one table at one position, one head width, both normed or
111    /// neither.
112    fn turned_alike(members: &[Op]) -> bool {
113        let sides: Vec<_> = members
114            .iter()
115            .filter_map(|m| match &m.sym {
116                Sym::Fused(f) if f.class == ClassId::QkvHeadEpilogue => rope(&f.parts[1]),
117                _ => None,
118            })
119            .collect();
120        sides.len() <= ROPE_SIDES
121            && sides.iter().all(|s| {
122                (&s.cos, &s.sin, &s.pos, s.hd, s.half, s.norm.is_some()) == (&sides[0].cos, &sides[0].sin, &sides[0].pos, sides[0].hd, sides[0].half, sides[0].norm.is_some())
123            })
124    }
125
126    /// Whether a product is one the GEMV takes in parts: within the GEMV's rows (or a batch's), landing whole, its
127    /// outputs whole warps of rows.
128    fn gemv_mergeable(&self, p: &GemvForm) -> bool {
129        (p.m <= GEMV_ROWS || self.batch) && Self::lands_whole(p) && p.n % self.per_warp(p) == 0
130    }
131
132    /// Whether every member of a GEMV in parts is one the GEMV takes together: at most four, one activation, one
133    /// format, one number of rows a warp.
134    fn gemv_in_parts(&self, members: &[Op], g: &TensorGraph) -> bool {
135        let products: Option<Vec<GemvForm>> = members.iter().map(|m| Self::part_product(m, g)).collect();
136        let Some(products) = products else { return false };
137        let first = &products[0];
138        products.len() <= kernels::GEMM_PARTS
139            && Self::turned_alike(members)
140            && products.iter().all(|p| {
141                self.gemv_mergeable(p)
142                    && p.weights == first.weights
143                    && (p.x.as_str(), p.k, p.m) == (first.x.as_str(), first.k, first.m)
144                    && self.per_warp(p) == self.per_warp(first)
145            })
146    }
147
148    /// Whether every member of a GEMM in parts is one the tensor cores take together.
149    fn gemm_parts(&self, members: &[Op], g: &TensorGraph) -> bool {
150        let products: Option<Vec<GemvForm>> = members.iter().map(|m| Self::rows_product(m, g)).collect();
151        let Some(products) = products else { return false };
152        let first = &products[0];
153        products.len() <= kernels::GEMM_PARTS
154            && products.iter().all(|p| {
155                self.on_tensor_cores(p)
156                    && Self::lands_whole(p)
157                    && p.n % kernels::GEMM_TILE_N == 0
158                    && p.weights == first.weights
159                    && (p.x.as_str(), p.k, p.m) == (first.x.as_str(), first.k, first.m)
160            })
161    }
162}
163
164/// The norm a fold of the embedding's gather and its first norm closes with.
165fn embedded_norm(op: &Op) -> Option<crate::tensor_graph::fuse::NormForm> {
166    match &op.sym {
167        Sym::Fused(f) if f.class == ClassId::EmbedNorm => norm(f.parts.last()?),
168        _ => None,
169    }
170}
171
172impl Shim for LaneShim {
173    fn name(&self) -> String {
174        format!("cuda-lane:{}:{:?}", self.knobs.chip.name, self.knobs.activations)
175    }
176
177    fn supports(&self, class: ClassId) -> Support {
178        use ClassId::*;
179        use Support::{Decompose, Native, Refuse};
180        let epilogue = if self.knobs.fuse_quant { Native } else { Decompose };
181        match class {
182            NormWeight | AddNorm | NormThenAdd | GatedNorm | EmbedScale => Native,
183            QkNormRope | QkNormRopeKvAppend | RopeKvAppend | RopeQkOneLaunch | Mla => Native,
184            FlashDecode | FlashPrefill | AttentionVariantsInline | AttnLseMerge => Native,
185            GluActMul | GemvGateUpGlu | GemmBias | MergedQkv | MergedGateUp | GemmDequantInkernel => Native,
186            RouterTopk | MoeTokenSort | MoeGateUpAct | MoeDownCombine | SharedExpertFusion => Native,
187            GdnStep | ConvBiasSilu | ConvParts | ConvSiluL2norm | GdnGateProjections | GdnGating => Native,
188            FusedSampling | StridedRead | RowWrite | JoinInPlace | StridedValues | CudaGraphs => Native,
189            NormQuant | AddNormQuant | GluActMulQuant | ElementwiseQuant | GdnOutputQuant | GemvQuant => epilogue,
190            GemvNormEpilogue => Native,
191            EmbedNorm => Native,
192            NormScale | NormWeightOffset | DualNorm | PrefillNormEpilogues | AttnResidualNorm => Decompose,
193            QkvHeadEpilogue => Native,
194            RecomputeFanout | QkNormRopeGateSplit | HorizontalNormRopeInsert | CrossLayerLaunch => Decompose,
195            AttnOutputQuant => epilogue,
196            AttnOutputGate | RowWriteQuant => Native,
197            MlaPrologue | MlaUnabsorbInFold | ElementwiseChains | ElementwiseRegions => Decompose,
198            MaskedExpertActivation | GemmScale | AffineInputFold | RouterSharedGateMerge | MoeGatherInOperand => Decompose,
199            MoeDeferredFinalize => Native,
200            GdnInputEpilogue | GdnDecodeWhole | GdnPostConvPrep | LastLayerRows => Decompose,
201            ReluSqr => Refuse("the lane links no ReLU"),
202            Softcap => Refuse("the lane's attention takes no soft cap"),
203            MixedBatchAttention => Refuse("the lane attends a batch's sequences each at its own position, none mid-prefill"),
204            SparseAttention => Refuse("the lane's attention reads every cached row of its span"),
205            GroupedTopk => Refuse("the lane routes by one top-k over every expert"),
206            Sampling => Refuse("the lane picks the next token by arg-max"),
207            KvCacheQuant => Refuse("the lane stores its caches in f32 or f16"),
208            HadamardRotation => Refuse("the lane rotates no cache"),
209            CompensatedResidual => Refuse("the lane sums its residual in f32, uncompensated"),
210            HostResidentKv => Refuse("the lane keeps its caches on the device; only matrices move to host memory"),
211            AttnGqaRowPacking => Refuse("the lane's decode attention runs a head's dots on the CUDA cores"),
212            MoeDirectPairs => Refuse("the lane sorts a chunk's rows by expert"),
213            PersistentLayerMegakernel => Refuse("the lane replays a graph of a launch an op"),
214            AllreduceNorm => Refuse("the lane runs on one device"),
215        }
216    }
217
218    fn fits(&self, op: &Op, g: &TensorGraph) -> Result<(), String> {
219        holds_accumulator(op, g, 32)?;
220        let refuse = |why: &str| Err(why.to_string());
221        for leaf in leaves(op.clone()) {
222            match leaf.prim() {
223                Some(TensorOpSpec::Attention(a)) if a.softcap.is_some() => return refuse("the lane's attention takes no soft cap"),
224                Some(TensorOpSpec::Softcap(_)) => return refuse("the lane links no soft-capped tensor"),
225                Some(TensorOpSpec::GatedDelta(g)) if g.gate == GateAct::Sigmoid => {
226                    return refuse("the lane's gated delta gates by SiLU, not a sigmoid")
227                }
228                _ => {}
229            }
230            if let Some(u) = &leaf.dst.unit {
231                if !unit_output(&leaf).is_some_and(|(_, width)| width == u.width) {
232                    return refuse("the lane writes a part of every unit of a wider tensor only from a norm, a rotation or a per-head product");
233                }
234            }
235            for access in &leaf.access {
236                let Access::Strided(s) = access else { continue };
237                match leaf.prim() {
238                    Some(TensorOpSpec::HeadMatmul(_)) if s.pitch % QBLOCK != 0 || s.offset % QBLOCK != 0 => {
239                        return refuse("a per-head product reads a part in place only at whole 32-value blocks");
240                    }
241                    Some(TensorOpSpec::Attention(_)) if s.pitch % 2 != 0 || s.offset % 2 != 0 => {
242                        return refuse("an attention reads its values in place a pair at a time, from an even place");
243                    }
244                    _ => {}
245                }
246            }
247        }
248        if moe(op).is_some() && matches!(op.dst.row, Some((RowSel::Param(_), _))) {
249            return refuse("a mixture writes its result at a fixed row, never at one a parameter names");
250        }
251        let Sym::Fused(f) = &op.sym else { return Ok(()) };
252        match f.class {
253            ClassId::NormThenAdd if norm(op).is_none() => Err("the lane's norm-then-add kernel norms the sum it writes".to_string()),
254            ClassId::GemvNormEpilogue => {
255                let (Some(p), Some(n)) = (gemv(&f.parts[0], g), norm(&f.parts[1])) else {
256                    return refuse("a projection's epilogue norms the add of its result");
257                };
258                let rows = matches!(p.weights, Weights::Rows(ty) if ty == crate::model::quant::GgmlType::Q8_0 || kernels::decodes(ty));
259                let one_gemv = rows && p.m == 1 && !self.batch && p.dst.own().is_some();
260                let whole_row = n.part.is_none() && n.rows == p.m && n.cols == p.n && f.parts[1].dst.own().is_some();
261                if !(one_gemv && whole_row) {
262                    return refuse("a projection norms in its last block the one row it writes whole, a GEMV of a step's one row by checkpoint rows");
263                }
264                // A Q8_0 projection's block is the norm's own; a block format's is eight warps or four.
265                let block = match p.weights {
266                    Weights::Rows(crate::model::quant::GgmlType::Q8_0) => kernels::norm_threads(n.cols),
267                    _ => kernels::gemv_blk_norm_threads(self.per_warp(&p)),
268                };
269                if kernels::block_holds_its_norm(block, n.cols) {
270                    Ok(())
271                } else {
272                    refuse("a projection norms in its last block only where its block is at least the norm's width")
273                }
274            }
275            ClassId::MoeDeferredFinalize => {
276                let (Some(m), Some(n)) = (moe(&f.parts[0]), f.parts.last().and_then(norm)) else {
277                    return refuse("a mixture's last step norms the add of its result");
278                };
279                let summed = f.parts.len() == 2 || f.parts[1].prim().is_some_and(|p| matches!(p, TensorOpSpec::Elementwise(e) if e.op == ElementwiseOp::Add));
280                let whole_rows = n.part.is_none() && n.rows == m.rows && n.cols == m.d && n.own && m.dst.own().is_some();
281                if !(summed && whole_rows) {
282                    return refuse("a mixture, alone or summed with another array, adds and norms in its last step the whole rows it writes");
283                }
284                // The decode step's last down block norms the row; a chunk's or a batch's combine takes a block of the
285                // norm's threads a row.
286                if m.rows == 1 && !self.batch {
287                    let Some((_, threads)) = Self::moe_down_launch(&f.parts[0], g) else {
288                        return refuse("a decode mixture's down projection reads its experts' widths from its parts");
289                    };
290                    if !kernels::block_holds_its_norm(threads, m.d) {
291                        return refuse("a decode mixture norms in its last down block only where the block is at least the norm's width");
292                    }
293                }
294                Ok(())
295            }
296            ClassId::EmbedNorm => {
297                let gathered = |o: &Op| match o.prim() {
298                    Some(TensorOpSpec::RowsGather(_) | TensorOpSpec::RowGather(_)) => true,
299                    _ => class(o) == Some(ClassId::EmbedScale),
300                };
301                match embedded_norm(op) {
302                    Some(n) if gathered(&f.parts[0]) && n.own => Ok(()),
303                    _ => refuse("a gather norms in its own blocks the whole rows it gathers"),
304                }
305            }
306            ClassId::QkvHeadEpilogue => {
307                let (Some(p), Some(ro)) = (Self::rows_product(&f.parts[0], g), rope(&f.parts[1])) else {
308                    return refuse("a projection's epilogue turns the heads of its result");
309                };
310                let one_row = p.m == 1 && !self.batch && self.gemv_mergeable(&p) && p.dst.own().is_some();
311                let its_heads = ro.part.is_none() && ro.heads == ro.per_row && p.n == ro.heads * ro.hd && ro.hd <= kernels::ROPE_HEAD;
312                if one_row && its_heads {
313                    Ok(())
314                } else {
315                    refuse("a projection turns in its epilogue the heads of the one row it writes whole, a GEMV of a step's one row")
316                }
317            }
318            ClassId::ConvSiluL2norm => {
319                let found = leaves(op.clone());
320                let chan = found.iter().find_map(|p| match p.prim()? {
321                    TensorOpSpec::CausalConv(c) => Some(c.chan),
322                    _ => None,
323                });
324                let cols = found.iter().find_map(|p| match p.prim()? {
325                    TensorOpSpec::L2Norm(l) => Some(l.cols),
326                    _ => None,
327                });
328                match (chan, cols) {
329                    (Some(chan), Some(cols)) if cols <= 1024 && cols % 32 == 0 && chan % cols == 0 => Ok(()),
330                    _ => refuse("a convolution's epilogue norms heads of at most 1024 values, whole warps, that divide its channels"),
331                }
332            }
333            ClassId::GemvGateUpGlu => {
334                let products: Vec<GemvForm> = f.parts.iter().filter_map(|p| gemv(p, g)).collect();
335                let (Some(gate), Some(up)) = (products.first(), products.get(1)) else {
336                    return refuse("a gated pair is two products");
337                };
338                if !matches!((gate.weights, up.weights), (Weights::Rows(_), Weights::Rows(_))) {
339                    return refuse("the lane pairs a gate and an up product only over checkpoint rows");
340                }
341                let gemv_rows = gate.m <= GEMV_ROWS || self.knobs.activations == Q8MatKind::Dequant || self.batch;
342                let paired = !gemv_rows && gate.weights == up.weights && gate.n % (kernels::GEMM_TILE_N / 2) == 0;
343                if gemv_rows || paired {
344                    Ok(())
345                } else {
346                    refuse("a gated pair on the tensor cores pairs tiles of one format, the hidden width whole pair tiles")
347                }
348            }
349            ClassId::ConvParts => {
350                let members: Option<Vec<ConvForm>> = f.parts.iter().map(conv).collect();
351                let Some(members) = members else { return refuse("a launch of convolutions takes convolutions") };
352                let normed: BTreeSet<_> = members.iter().filter_map(|c| c.l2.map(|(cols, eps)| (cols, eps.get().to_bits()))).collect();
353                let one = members.len() <= CONV_PARTS
354                    && members.iter().all(|c| c.rows == 1 && c.silu && c.taps == members[0].taps)
355                    && normed.len() <= 1;
356                if one {
357                    Ok(())
358                } else {
359                    refuse("one launch convolves a step's one row through the SiLU in at most four parts of one length of taps, the normed ones over heads of one width")
360                }
361            }
362            ClassId::GdnGateProjections => {
363                let by_values = f.parts.iter().filter_map(|p| gemv(p, g)).all(|p| p.weights == Weights::Values);
364                if by_values {
365                    Ok(())
366                } else {
367                    refuse("the lane takes a delta rule's gates from its activation only by matrices it holds as values")
368                }
369            }
370            ClassId::SharedExpertFusion => {
371                let found = leaves(op.clone());
372                let swiglu = found.iter().any(|p| matches!(p.prim(), Some(TensorOpSpec::SwiGlu(_))));
373                let biased = found.iter().any(|p| matches!(p.prim(), Some(TensorOpSpec::Elementwise(e)) if e.op == ElementwiseOp::Add && e.row.is_some()));
374                let rows = found.iter().filter_map(|p| gemv(p, g)).all(|p| matches!(p.weights, Weights::Rows(_)) && p.m == 1);
375                match moe(op) {
376                    Some(m) if m.k < 32 && m.rows == 1 && m.act == MoeAct::Swiglu && swiglu && !biased && rows => Ok(()),
377                    _ => refuse("a mixture folds its unbiased SwiGLU shared expert, a step's one row of checkpoint rows, into at most 32 warps of kept experts"),
378                }
379            }
380            ClassId::RopeQkOneLaunch => {
381                let sides: Option<Vec<_>> = f.parts.iter().map(rope).collect();
382                let Some(sides) = sides else { return refuse("a launch of rotations takes rotations") };
383                let one = sides.len() <= ROPE_SIDES
384                    && sides.iter().all(|s| s.heads == s.per_row && s.norm.is_some() == sides[0].norm.is_some());
385                if one {
386                    Ok(())
387                } else {
388                    refuse("one launch rotates at most a query and a key of a step's one row, both normed or neither")
389                }
390            }
391            // The gate rides the decode step's, a short chunk's and a batch's sequences' attention stores, a pair a thread;
392            // a chunk in tiles gates apart.
393            ClassId::AttnOutputGate => {
394                let attentions: Vec<_> = f.parts.iter().filter_map(|p| match p.prim() {
395                    Some(TensorOpSpec::Attention(a)) => Some(a),
396                    _ => None,
397                }).collect();
398                if !attentions.is_empty() && attentions.iter().all(|a| a.rows <= GEMV_ROWS && a.vd % 2 == 0) {
399                    Ok(())
400                } else {
401                    refuse("an attention gates its output only in the decode step's, a short chunk's and a batch's sequences' kernels, its heads whole pairs")
402                }
403            }
404            // A row set carries its row's quantized form a block at a time: the rows whole 32-value blocks.
405            ClassId::RowWriteQuant => match f.parts[0].prim() {
406                Some(TensorOpSpec::SetRow(r)) if r.cols % QBLOCK == 0 => Ok(()),
407                _ => refuse("a row set carries its rows' quantized form only where they are whole 32-value blocks"),
408            },
409            // A batch's sequences each quantize their gated row in their store, set with its quantized form into the
410            // product's (PARITY-BATCH-ROW-QUANT).
411            ClassId::GluActMulQuant if class(&f.parts[0]) == Some(ClassId::AttnOutputGate) => match &f.parts[0].sym {
412                Sym::Fused(gated) => {
413                    let whole = gated.parts.iter().all(|p| match p.prim() {
414                        Some(TensorOpSpec::Attention(a)) => a.vd % 32 == 0,
415                        Some(TensorOpSpec::SetRow(r)) => r.cols % QBLOCK == 0,
416                        _ => true,
417                    });
418                    if whole {
419                        Ok(())
420                    } else {
421                        refuse("a gated attention quantizes its output in its store only where its heads, and a batch's rows, are whole 32-value groups")
422                    }
423                }
424                _ => refuse("a gated attention is a fold of its attention and its gate"),
425            },
426            ClassId::AttnOutputQuant => match f.parts[0].prim() {
427                // The decode step's and a short chunk's attention kernels store their output a pair a thread, each 32-value
428                // group in one half-warp's sixteen lanes; a chunk the attention takes in tiles quantizes apart.
429                Some(TensorOpSpec::Attention(a)) if a.rows <= GEMV_ROWS && a.vd % 32 == 0 => Ok(()),
430                _ => refuse("an attention quantizes its output only in the decode step's and a short chunk's kernels, its heads whole 32-value groups"),
431            },
432            ClassId::NormQuant if norm(&f.parts[0]).or_else(|| embedded_norm(&f.parts[0])).is_none() => {
433                refuse("the lane's norm quantizes its result only from an RMS norm")
434            }
435            ClassId::GemvQuant => match gemv(&f.parts[0], g) {
436                Some(p) if self.per_warp(&p) == 4 && p.n % QBLOCK == 0 && p.dst.own().is_some() => Ok(()),
437                _ => refuse("a product quantizes its outputs only four rows a warp, whole blocks of them"),
438            },
439            ClassId::GluActMulQuant if class(&f.parts[0]) == Some(ClassId::GemvGateUpGlu) => {
440                let hidden = match &f.parts[0].sym {
441                    Sym::Fused(glu) => glu.parts.iter().find_map(|p| gemv(p, g)).map(|p| p.n),
442                    _ => None,
443                };
444                match hidden {
445                    Some(n) if n % QBLOCK == 0 => Ok(()),
446                    _ => refuse("a gated pair quantizes its hidden values in whole 32-value blocks"),
447                }
448            }
449            ClassId::MergedQkv => {
450                // The GEMV in parts is the lane's own rule, held with it.
451                if self.gemm_parts(&f.parts, g) || (!self.held.contains(&"gemv_parts") && self.gemv_in_parts(&f.parts, g)) {
452                    Ok(())
453                } else {
454                    refuse("products of one activation are one launch in parts: at most four, one format — on the tensor cores whole 128-output tiles, in the GEMV one number of rows a warp")
455                }
456            }
457            _ => Ok(()),
458        }
459    }
460
461    fn activation(&self, consumer: &Op, operand: usize, g: &TensorGraph) -> Option<QuantFormat> {
462        // A producer that quantizes its own result in its epilogue reads its operands as it reads them alone.
463        if let Sym::Fused(f) = &consumer.sym {
464            use ClassId::*;
465            if matches!(f.class, NormQuant | AddNormQuant | GluActMulQuant | ElementwiseQuant | GdnOutputQuant | GemvQuant | AttnOutputQuant) {
466                return self.activation(&f.parts[0], operand, g);
467            }
468            // A projection with its epilogue folded in reads its operands first, as the projection alone reads them.
469            if matches!(f.class, GemvNormEpilogue | QkvHeadEpilogue | MoeDeferredFinalize) {
470                return self.activation(&f.parts[0], operand, g).filter(|_| operand < f.parts[0].ins.len());
471            }
472        }
473        let format = match self.knobs.activations {
474            Q8MatKind::Dequant => return None,
475            Q8MatKind::Int8Act => QuantFormat::OneWord,
476            Q8MatKind::TwoPassInt8 => QuantFormat::TwoWords,
477        };
478        let projected = operand == 0
479            && (Self::rows_product(consumer, g).is_some()
480                || moe(consumer).is_some()
481                || matches!(class(consumer), Some(ClassId::GemvGateUpGlu | ClassId::MergedQkv)));
482        projected.then_some(format)
483    }
484
485    fn rules(&self) -> Vec<(Position, ChipRule<'_>)> {
486        vec![
487            (Position::Before("add_norm"), ChipRule { name: "latent_expanded", builds: &[ClassId::Mla], apply: Box::new(move |r| self.latent_expanded(r)) }),
488            (Position::Before("merged_projections"), ChipRule { name: "gemv_parts", builds: &[ClassId::MergedQkv], apply: Box::new(move |r| self.gemv_parts(r)) }),
489        ]
490    }
491
492    fn numerics(&self) -> Option<Contract> {
493        let activations = match self.knobs.activations {
494            Q8MatKind::Dequant => None,
495            Q8MatKind::Int8Act => Some(Scheme::of(QuantFormat::OneWord)),
496            Q8MatKind::TwoPassInt8 => Some(Scheme::of(QuantFormat::TwoWords)),
497        };
498        let cache = match self.knobs.cache_precision {
499            CachePrecision::F32 => Keep::F32,
500            CachePrecision::F16 => Keep::F16,
501        };
502        let state = match self.knobs.recurrent_state {
503            StatePrecision::F32 => Keep::F32,
504            StatePrecision::Bf16 => Keep::Bf16,
505        };
506        let point = format!("the lane: {:?} activations, {:?} cache, {:?} state", self.knobs.activations, self.knobs.cache_precision, self.knobs.recurrent_state);
507        Some(Contract { point, activations, cache, state, attention: Self::attention(self.knobs.activations), fused: Bound::EXACT, accumulator_bits: 32 })
508    }
509}
510
511impl LaneShim {
512    /// How a wide chunk's attention takes its operands at `activations`: on the f16 tensor cores under one word (its
513    /// queries, keys, values and weights rounded to f16, `lk_attention_mma`); on the int8 tensor cores under two
514    /// (`lk_attention_i8`: each query, key and value row in two words under its largest over 127, each weight times its
515    /// value row's scale in three under the largest of its row in the tile, every product of the words taken); in
516    /// `f32` under the dequantized numerics.
517    pub fn attention(activations: Q8MatKind) -> Option<AttentionNumerics> {
518        let row = |words| Taken::Quantized(Scheme { block: Block::Row, scale: Scale::MaxOver127, words });
519        match activations {
520            Q8MatKind::Dequant => None,
521            Q8MatKind::Int8Act => Some(AttentionNumerics { queries: Taken::Kept(Keep::F16), keys_values: Taken::Kept(Keep::F16), weights: Taken::Kept(Keep::F16) }),
522            Q8MatKind::TwoPassInt8 => Some(AttentionNumerics { queries: row(2), keys_values: row(2), weights: row(3) }),
523        }
524    }
525
526    /// A step's products of one activation's rows within one GEMV's — in one format, at one number of rows a warp, each
527    /// into a buffer of its own or a table's rows its width wide, none quantizing its result, those turning their heads
528    /// in the epilogue turning them alike — as one GEMV over their outputs side by side, at the first one's place: the
529    /// short ones ride with the wide. Nothing between them may write the activation or what a later member reads, or
530    /// touch what it writes.
531    fn gemv_parts(&self, r: &mut Rewriter) {
532        for i in 0..r.s.len() {
533            let Some(first) = r.s.slot(i).and_then(|o| Self::part_product(o, r.g)).filter(|p| self.gemv_mergeable(p)) else { continue };
534            let mut group = vec![i];
535            for j in i + 1..r.s.len() {
536                let Some(op) = r.s.slot(j) else { continue };
537                if group.len() == kernels::GEMM_PARTS || facts::writes(op).contains(&first.x.as_str()) {
538                    break;
539                }
540                let members: Vec<Op> = group.iter().map(|&k| r.s.get(k).clone()).chain([op.clone()]).collect();
541                let fits = Self::part_product(op, r.g).is_some() && self.gemv_in_parts(&members, r.g);
542                if !fits {
543                    continue;
544                }
545                let (written, read) = (facts::writes(op), facts::reads(op));
546                let crossed = (i + 1..j).filter(|b| !group.contains(b)).filter_map(|b| r.s.slot(b)).any(|between| {
547                    let (br, bw) = (facts::reads(between), facts::writes(between));
548                    written.iter().any(|t| br.contains(t) || bw.contains(t)) || read.iter().any(|t| bw.contains(t))
549                });
550                if !crossed {
551                    group.push(j);
552                }
553            }
554            if group.len() < 2 {
555                continue;
556            }
557            let members: Vec<Op> = group.iter().map(|&k| r.s.get(k).clone()).collect();
558            let mut ins: Vec<(String, Access)> = vec![(first.x.clone(), members[0].access[0])];
559            ins.extend(members.iter().flat_map(|m| m.ins[1..].iter().map(|t| (t.clone(), Access::Whole))));
560            let (out, dst) = (members[0].out.clone(), members[0].dst.clone());
561            r.fuse("gemv_parts", ClassId::MergedQkv, i, &group[1..], ins, out, dst);
562        }
563    }
564
565    /// A wide chunk's latent attention whole (`mla`), where the lane runs it expanded: under the quantized numerics, a
566    /// chunk of more rows than the GEMV takes attending the cache absorbed — each head's unrotated query sliced from the
567    /// query projection and absorbed by the key half of the latent's expansion, joined with its rotated part, attending
568    /// every cached row with the rows' latents as its values, and the attended latent expanded by the value half. The
569    /// op's meaning is those parts; the lane runs each cached latent row through both halves instead, and each head's
570    /// query attends its own expanded keys and values, multiplying each cached row once rather than every query a
571    /// head's whole latent wide. Taken where both halves and the attention run in tiles.
572    fn latent_expanded(&self, r: &mut Rewriter) {
573        let numerics = self.knobs.activations;
574        if numerics == Q8MatKind::Dequant || self.batch {
575            return;
576        }
577        let whole = |op: &Op| op.access.iter().all(|a| *a == Access::Whole);
578        let rows_of = |t: &str| match r.g.tensors.get(t).map(|t| &t.storage) {
579            Some(Storage::Rows { format, transposed }) => Some((*format, *transposed)),
580            _ => None,
581        };
582        let mut found = Vec::new();
583        for i in 0..r.s.len() {
584            let Some(op) = r.s.slot(i) else { continue };
585            let Some(TensorOpSpec::Attention(a)) = op.prim() else { continue };
586            if a.sinks.is_some() || a.window.is_some() || a.kv != 1 || a.rows <= GEMV_ROWS || a.vd >= a.hd || !whole(op) {
587                continue;
588            }
589            let (q, kc, vc, ol) = (op.ins[0].clone(), op.ins[1].clone(), op.ins[2].clone(), op.out.clone());
590            let pos = match &a.pos {
591                Some(RowIndex::Named(_)) => Some(op.ins[3].clone()),
592                Some(RowIndex::Literal(_)) => continue,
593                None => None,
594            };
595            let (heads, kw, lora, len, rows, scale) = (a.h, a.hd, a.vd, a.l, a.rows, a.scale.map_or(1.0 / (a.hd as f64).sqrt(), |s| s.get()));
596            let rope = kw - lora;
597            // The values: the latent part of every cache row.
598            let Some(vi) = r.s.producer(&vc) else { continue };
599            if !slice(r.s.get(vi)).is_some_and(|v| v.x == kc && v.every == kw && v.from == 0 && v.width == lora) || r.s.readers(&vc) != [i] {
600                continue;
601            }
602            // The query: each head's absorbed part, then its rotated part.
603            let Some(ji) = r.s.producer(&q) else { continue };
604            let join = r.s.get(ji);
605            let Some(TensorOpSpec::Join(j)) = join.prim() else { continue };
606            if join.dst.own() != Some(q.as_str()) || !whole(join) || (j.rows, j.heads, j.wa / j.heads, j.wb / j.heads) != (rows, heads, lora, rope) || r.s.readers(&q) != [i] {
607                continue;
608            }
609            let (ql, qpp) = (join.ins[0].clone(), join.ins[1].clone());
610            let Some(ai) = r.s.producer(&ql) else { continue };
611            let absorb = r.s.get(ai);
612            let Some(TensorOpSpec::HeadMatmul(m)) = absorb.prim() else { continue };
613            if absorb.dst.own() != Some(ql.as_str()) || !whole(absorb) || (m.m, m.heads, m.b) != (rows, heads, lora) || r.s.readers(&ql) != [ji] {
614                continue;
615            }
616            let (qn, wkb, nope) = (absorb.ins[0].clone(), absorb.ins[1].clone(), m.a);
617            let Some((fk, kro)) = rows_of(&wkb) else { continue };
618            let qk = nope + rope;
619            let Some(qi) = r.s.producer(&qn) else { continue };
620            let Some(qs) = slice(r.s.get(qi)).filter(|s| (s.groups, s.every, s.from, s.width) == (rows * heads, qk, 0, nope)) else { continue };
621            if r.s.readers(&qn) != [ai] {
622                continue;
623            }
624            let qr = qs.x;
625            let Some(ri) = r.s.producer(&qpp) else { continue };
626            let rot = r.s.get(ri);
627            if !matches!(rot.prim(), Some(TensorOpSpec::Rope(_))) || rot.dst.own() != Some(qpp.as_str()) || !whole(rot) || r.s.readers(&qpp) != [ji] {
628                continue;
629            }
630            let rotated_from_query = r.s.producer(&rot.ins[0]).and_then(|e| slice(r.s.get(e))).is_some_and(|e| e.x == qr && (e.every, e.from, e.width) == (qk, nope, rope));
631            if !rotated_from_query {
632                continue;
633            }
634            // The attended latent through the value half.
635            let [ci] = r.s.readers(&ol)[..] else { continue };
636            let expand = r.s.get(ci);
637            let Some(TensorOpSpec::HeadMatmul(c)) = expand.prim() else { continue };
638            let Some((fv, true)) = rows_of(&expand.ins[1]) else { continue };
639            let Some(ct) = expand.dst.own().map(str::to_string) else { continue };
640            if !whole(expand) || (c.m, c.heads, c.a) != (rows, heads, lora) {
641                continue;
642            }
643            let latent = Latent {
644                qr,
645                qpp,
646                kc,
647                pos,
648                heads,
649                kw,
650                lora,
651                nope,
652                len,
653                rows,
654                scale,
655                wkb,
656                fk,
657                kro,
658                wvb: expand.ins[1].clone(),
659                fv,
660                vd: c.b,
661                ct,
662            };
663            if self.latent_launches(&latent).is_some_and(|[kh, vh, attend]| attend.attends_in_tiles(numerics) && kh.heads_in_tiles(numerics) && vh.heads_in_tiles(numerics)) {
664                found.push((i, [qi, ai, ji, vi, ci], expand.dst.clone()));
665            }
666        }
667        for (i, gone, dst) in found {
668            let members: Vec<Op> = std::iter::once(i).chain(gone).map(|k| r.s.get(k).clone()).collect();
669            let made: Vec<&str> = members.iter().map(|m| m.out.as_str()).collect();
670            let mut ins: Vec<(String, Access)> = Vec::new();
671            let mut parts = members.clone();
672            parts.sort_by_key(|p| p.covers.first().copied().unwrap_or(0));
673            for p in &parts {
674                for (t, a) in p.ins.iter().zip(&p.access) {
675                    if !made.contains(&t.as_str()) && !ins.iter().any(|(n, _)| n == t) {
676                        ins.push((t.clone(), *a));
677                    }
678                }
679            }
680            r.fuse("latent_expanded", ClassId::Mla, i, &gone, ins, dst.buf.clone(), dst);
681        }
682    }
683
684    /// A latent attention's expanded launches: the key half and the value half over every cached latent row, then the
685    /// attention of each head's split query over its own keys and values — `None` where one block's shared memory holds
686    /// no head.
687    pub fn latent_launches(&self, l: &Latent) -> Option<[Launch; 3]> {
688        let numerics = self.knobs.activations;
689        let acc = kernels::accumulator(kernels::mode(numerics)).0;
690        let sms = self.knobs.chip.sms;
691        let qk = l.nope + (l.kw - l.lora);
692        let upto = l.pos.clone().map(|p| (p, l.rows - 1));
693        let half = |w: &str, format, b, rows_out, y: &str| Launch::HeadGemv {
694            x: l.kc.clone(),
695            part: Some(LanePart { pitch: l.kw, offset: 0 }),
696            w: w.to_string(),
697            format,
698            y: LaneDst::to(y),
699            m: l.len,
700            heads: l.heads,
701            a: l.lora,
702            b,
703            rows_out,
704            shared: true,
705            upto: upto.clone(),
706            batch: false,
707        };
708        let (keys, values) = (format!("{}.keys", l.ct), format!("{}.values", l.ct));
709        let kh = half(&l.wkb, l.fk, l.nope, !l.kro, &keys);
710        let vh = half(&l.wvb, l.fv, l.vd, true, &values);
711        let span = attended_span(l.len, None);
712        let per_block = kernels::attn_heads_per_block(1, qk, l.vd, span, sms, self.knobs.chip.smem_kib_per_block * 1024, acc)?;
713        let splits = kernels::attn_splits(l.heads / per_block, span, sms);
714        let attend = Launch::Attention {
715            q: l.qr.clone(),
716            kc: keys,
717            vc: values,
718            pos: l.pos.clone(),
719            sinks: None,
720            out: l.ct.clone(),
721            heads: l.heads,
722            group: 1,
723            hd: qk,
724            vd: l.vd,
725            len: l.len,
726            rows: l.rows,
727            window: None,
728            vstride: l.heads * l.vd,
729            voff: 0,
730            scale: l.scale,
731            per_block,
732            splits,
733            parallel: chunks_apart(splits, l.rows, l.heads / per_block, sms),
734            split: Some(Split {
735                own: l.nope,
736                q_pitch: qk,
737                q_tail: (l.qpp.clone(), LanePart { pitch: l.kw - l.lora, offset: 0 }),
738                k_tail: (l.kc.clone(), LanePart { pitch: l.kw, offset: l.lora }),
739            }),
740            shared: None,
741            seq: None,
742            f32: true,
743            quant: false,
744            gate: None,
745            goff: 0,
746        };
747        Some([kh, vh, attend])
748    }
749}
750
751/// A latent attention's shape and names, as its expanded launches read them: the query projection `qr` (each head's
752/// unrotated part its first `nope` values) and the rotated part `qpp`; the cache `kc`, `kw` wide a row — the latent
753/// `lora` values then the shared rotated key — `len` rows; the key and value halves of the latent's expansion and their
754/// formats (`kro`: the key half's checkpoint rows are its outputs); the result `ct`.
755pub struct Latent {
756    pub qr: String,
757    pub qpp: String,
758    pub kc: String,
759    pub pos: Option<String>,
760    pub heads: usize,
761    pub kw: usize,
762    pub lora: usize,
763    pub nope: usize,
764    pub len: usize,
765    pub rows: usize,
766    pub scale: f64,
767    pub wkb: String,
768    pub fk: crate::model::quant::GgmlType,
769    pub kro: bool,
770    pub wvb: String,
771    pub fv: crate::model::quant::GgmlType,
772    pub vd: usize,
773    pub ct: String,
774}
775
776/// Whether an attention cut into `splits` chunks runs a block a chunk: where its rows' head blocks alone leave the chip
777/// idle.
778pub fn chunks_apart(splits: usize, rows: usize, head_blocks: usize, sms: usize) -> bool {
779    splits > 1 && rows * head_blocks < 2 * sms
780}