Skip to main content

logicaffeine_compile/codegen_cuda/
forward_shim.rs

1//! What the older compiled forward runs whole (`compile_to_cuda_forward*`): its answer to the shared optimizer.
2//!
3//! A lifted op is the forward's exactly when its kernels compute it ([`kernel_refusal`] says why they do not
4//! otherwise). Its fused kernels are the ones its emitter writes:
5//!
6//!   • a sum and the RMS norm of it that is the next stage, as one row kernel writing the sum on (`add_norm`), the norm
7//!     without a residual of its own over the whole of the sum;
8//!   • a weighted RMS norm as one row kernel;
9//!   • a run of elementwise ops, each carrying the last one's result, as one register-carried kernel — the runs taken
10//!     before a norm takes a sum, so a sum a run carries stays in its run;
11//!   • under the single-pass int8 numerics, the activation a Q8_0 projection reads quantized in its producer's epilogue
12//!     — a norm's, a sum-then-norm's, an attention's;
13//!   • a sparse mixture as a top-k router, a gate-up kernel and a down-and-combine kernel, its Q8_0 matrices (the
14//!     forward's mixture manifest) dequantized inside the kernels that read them;
15//!   • the whole forward captured as one CUDA graph.
16//!
17//! It reads no part of a tensor in place and writes none into rows of another: every stage reads and writes whole
18//! buffers.
19
20use crate::tensor_graph::front::TensorOpSpec;
21use crate::model::quant::GgmlType;
22use crate::tensor_graph::fuse::{binary, leaves, moe, norm, weights, ChipRule, Weights, RULES};
23use crate::tensor_graph::graph::{Access, Op, QuantFormat, Sym, TensorGraph};
24use crate::tensor_graph::numerics::{holds_accumulator, Bound, Contract, Keep, Scheme};
25use crate::tensor_graph::shim::{Position, Shim, Support};
26use crate::tensor_graph::symbols::ClassId;
27
28use super::qwen_emit::Q8MatKind;
29use crate::tensor_recognize::qwen::{GateAct, MoeAct};
30use crate::tensor_recognize::RowReduceKind;
31
32/// The static shared memory one attention block may declare (`sc[L]` scores plus the 32-float reduction row) — the
33/// 48 KiB every CUDA architecture grants without an opt-in.
34const ATTN_STATIC_SHARED_BYTES: usize = 48 * 1024;
35
36/// Why the forward's kernels do not compute a lifted op, or `None` when they do. The spec is the op's meaning: a field
37/// of it the kernel does not implement is a refusal, never a field the kernel ignores.
38pub fn kernel_refusal(op: &TensorOpSpec) -> Option<&'static str> {
39    use TensorOpSpec as T;
40    match op {
41        T::Matmul(_) | T::Silu(_) | T::Transpose(_) => None,
42        T::Elementwise(e) if e.row.is_some() => {
43            Some("an elementwise op over a row combined with every row of a block — its kernel reads both at one index")
44        }
45        T::Elementwise(_) => None,
46        T::FusedRow(r) => match r.reduce {
47            RowReduceKind::RmsNorm if r.bias.is_some() => Some("a bias on an RMSNorm — its kernel scales and never shifts"),
48            RowReduceKind::Softmax if r.weight.is_some() || r.residual.is_some() || r.bias.is_some() => {
49                Some("a weight, residual or bias on a softmax")
50            }
51            RowReduceKind::LayerNorm if r.residual.is_some() => Some("a residual added before a layernorm"),
52            _ => None,
53        },
54        T::Rope(r) if r.from.is_some() => Some("a rope reading its angles from a position in a cache of angles"),
55        T::Rope(r) if r.rows != 1 => Some("a rope with a table row per row of the query — its kernel reads one row of angles"),
56        T::Rope(r) if r.hd != 2 * r.half => Some("a partial rotary — its kernel turns every dimension of the head"),
57        T::Rope(_) => None,
58        T::Attention(a) if a.softcap.is_some() => Some("a soft-capped attention (`capped at C`) — its kernel caps no score"),
59        T::GatedDelta(g) if g.gate == GateAct::Sigmoid => Some("a gated delta gated by a sigmoid (`with sigmoid gate Z`) — its kernel gates by SiLU"),
60        T::Attention(a) if a.pos.is_some() => Some("a masked attention (`up to P`) — its kernel attends every cached position"),
61        T::Softcap(_) => Some("a soft-capped tensor (`capped at C`) — no kernel caps one"),
62        T::Attention(a) if a.sinks.is_some() => Some("an attention with sinks (`with sinks S`) — its kernel's softmax has no sink"),
63        T::Attention(a) if a.window.is_some() => {
64            Some("a windowed attention (`within a window of W`) — its kernel attends every cached position")
65        }
66        T::Attention(a) if a.vd != a.hd => {
67            Some("an attention whose values are narrower than its keys — its kernel reads values as wide as keys")
68        }
69        T::Attention(a) if a.scale.is_some() => {
70            Some("an attention with a given scale (`scaled by S`) — its kernel scales the scores by 1/√HD")
71        }
72        T::Attention(a) if a.rows != 1 => Some("a causal block of query rows — its kernel attends for one query"),
73        T::Attention(a) if a.kv == 0 || a.h % a.kv != 0 => Some("query heads that do not group evenly over the key/value heads"),
74        T::Attention(a) if a.l * 4 + 32 * 4 > ATTN_STATIC_SHARED_BYTES => {
75            Some("a cache longer than the score row its kernel holds in static shared memory")
76        }
77        T::Attention(_) => None,
78        T::Moe(m) if m.router_bias.is_some() || m.biases.iter().any(Option::is_some) => Some("an expert mixture with biases — its kernels add none"),
79        T::Moe(m) if !m.renormalize => {
80            Some("an expert mixture without renormalizing — its router renormalizes the kept experts' weights")
81        }
82        T::Moe(m) if m.act != MoeAct::Swiglu => Some("an expert mixture with a clamped SwiGLU — its kernels gate with SwiGLU"),
83        T::Moe(m) if !m.weighs_outputs_by_softmax() => {
84            Some("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")
85        }
86        T::Moe(_) => None,
87        T::SwiGlu(_)
88        | T::Sigmoid(_)
89        | T::Gelu(_)
90        | T::Scale(_)
91        | T::Decay(_)
92        | T::L2Norm(_)
93        | T::HeadSlice(_)
94        | T::Join(_)
95        | T::HeadMatmul(_)
96        | T::CausalConv(_)
97        | T::GatedDelta(_)
98        | T::FullAttention(_)
99        | T::RowGather(_)
100        | T::RowsGather(_)
101        | T::ArgMax(_)
102        | T::SetRow(_)
103        | T::IntRequant(_)
104        | T::IntClamp(_)
105        | T::Relu(_)
106        | T::LutAttention(_) => Some("this lane has no kernel for the op"),
107    }
108}
109
110/// The older forward under its numerics.
111#[derive(Clone, Copy, Debug)]
112pub struct ForwardShim {
113    pub kind: Q8MatKind,
114}
115
116impl ForwardShim {
117    /// Whether a product reads Q8_0 rows: the forward's int8 projection.
118    fn q8_product(op: &Op, g: &TensorGraph) -> bool {
119        matches!(op.prim(), Some(TensorOpSpec::Matmul(_))) && matches!(weights(g, &op.ins[1]), Some(Weights::Rows(GgmlType::Q8_0)))
120    }
121}
122
123impl Shim for ForwardShim {
124    fn name(&self) -> String {
125        format!("cuda-forward:{:?}", self.kind)
126    }
127
128    fn supports(&self, class: ClassId) -> Support {
129        use ClassId::*;
130        use Support::{Decompose, Native, Refuse};
131        let producer_quant = if self.kind == Q8MatKind::Int8Act { Native } else { Decompose };
132        match class {
133            NormWeight | AddNorm | ElementwiseChains | ElementwiseRegions | GemmDequantInkernel | CudaGraphs => Native,
134            RouterTopk | MoeGateUpAct | MoeDownCombine => Native,
135            NormQuant | AddNormQuant | AttnOutputQuant => producer_quant,
136            AttnOutputGate | RowWriteQuant => Decompose,
137            NormThenAdd | NormScale | NormWeightOffset | DualNorm | GatedNorm | EmbedNorm | EmbedScale => Decompose,
138            GemvNormEpilogue | PrefillNormEpilogues | AttnResidualNorm | RecomputeFanout => Decompose,
139            QkNormRope | QkNormRopeKvAppend | RopeKvAppend | RopeQkOneLaunch | QkNormRopeGateSplit | QkvHeadEpilogue => Decompose,
140            HorizontalNormRopeInsert | CrossLayerLaunch | Mla | MlaPrologue | MlaUnabsorbInFold => Decompose,
141            GluActMul | GluActMulQuant | GemvGateUpGlu | MaskedExpertActivation => Decompose,
142            GemmBias | GemmScale | MergedQkv | MergedGateUp | AffineInputFold | RouterSharedGateMerge => Decompose,
143            MoeTokenSort | MoeGatherInOperand | MoeDeferredFinalize | SharedExpertFusion => Decompose,
144            GdnInputEpilogue | GdnDecodeWhole | GdnPostConvPrep | ConvBiasSilu | ConvParts | ConvSiluL2norm => Decompose,
145            GdnGateProjections | GdnGating | FusedSampling | LastLayerRows | AllreduceNorm => Decompose,
146            ElementwiseQuant | GdnOutputQuant | GemvQuant => Decompose,
147            ReluSqr => Refuse("the forward links no ReLU"),
148            FlashDecode | FlashPrefill => Refuse("the forward attends one query over a cache held in one block's shared scores"),
149            AttentionVariantsInline | Softcap | MixedBatchAttention | SparseAttention => {
150                Refuse("the forward's attention takes no mask, window, sink, soft cap or given scale")
151            }
152            AttnGqaRowPacking | AttnLseMerge => Refuse("the forward attends a cache in one block, never in chunks"),
153            GroupedTopk => Refuse("the forward routes by one top-k over every expert"),
154            Sampling => Refuse("the forward computes logits and picks no token"),
155            GdnStep => Refuse("the forward links no delta rule"),
156            KvCacheQuant | HadamardRotation => Refuse("the forward holds its caches in f32"),
157            CompensatedResidual => Refuse("the forward sums its residual in f32, uncompensated"),
158            HostResidentKv => Refuse("the forward keeps every buffer on the device"),
159            MoeDirectPairs => Refuse("the forward runs one sequence's row through its kept experts"),
160            PersistentLayerMegakernel => Refuse("the forward replays a graph of a kernel a stage"),
161            StridedRead | RowWrite | JoinInPlace | StridedValues => Refuse("the forward's stages read and write whole buffers"),
162        }
163    }
164
165    fn fits(&self, op: &Op, g: &TensorGraph) -> Result<(), String> {
166        holds_accumulator(op, g, 32)?;
167        if op.access.iter().any(|a| *a != Access::Whole) || op.dst.own().is_none() {
168            return Err("the forward's stages read and write whole buffers".to_string());
169        }
170        match &op.sym {
171            Sym::Prim(spec @ TensorOpSpec::Moe(_)) if op.ins[1..5].iter().any(|w| !matches!(weights(g, w), Some(Weights::Rows(GgmlType::Q8_0)))) => {
172                kernel_refusal(spec).map_or_else(|| Err(format!("moe stage `{}` has no entry in the MoE weight manifest", op.out)), |why| Err(why.to_string()))
173            }
174            Sym::Prim(spec) => kernel_refusal(spec).map_or(Ok(()), |why| Err(why.to_string())),
175            Sym::View { .. } => Err("the forward reads no view into another buffer".to_string()),
176            Sym::Quantize(_) if self.kind == Q8MatKind::Dequant => Err("the forward's precise numerics read no activation quantized".to_string()),
177            Sym::Quantize(_) => Ok(()),
178            Sym::Fused(f) => {
179                for leaf in leaves(op.clone()) {
180                    self.fits(&leaf, g)?;
181                }
182                match f.class {
183                    ClassId::AddNorm => {
184                        let next = match (f.parts.first(), f.parts.last()) {
185                            (Some(add), Some(n)) => add.covers.last().zip(n.covers.first()).is_some_and(|(a, b)| a + 1 == *b),
186                            _ => false,
187                        };
188                        match norm(op) {
189                            Some(n) if n.part.is_none() && !n.pre && next => Ok(()),
190                            Some(n) if n.part.is_none() && !n.pre => Err("the forward adds into a norm only where the norm is the next stage".to_string()),
191                            _ => Err("the forward adds into the norm that follows, over the whole of the sum".to_string()),
192                        }
193                    }
194                    ClassId::ElementwiseChains => {
195                        let carried = f.parts.windows(2).all(|w| {
196                            binary(&w[1]).is_some_and(|b| (b.a == w[0].out) ^ (b.b == w[0].out))
197                                || matches!(w[1].prim(), Some(TensorOpSpec::Silu(_)))
198                        });
199                        if carried {
200                            Ok(())
201                        } else {
202                            Err("the forward carries a chain's result through each step, as exactly one operand".to_string())
203                        }
204                    }
205                    ClassId::AttnOutputQuant => match f.parts.first().and_then(|host| host.prim()) {
206                        Some(TensorOpSpec::Attention(a)) if a.hd % 32 == 0 => Ok(()),
207                        _ => Err("the forward's attention quantizes its result only where each head's values are whole 32-value blocks".to_string()),
208                    },
209                    _ => Ok(()),
210                }
211            }
212        }
213    }
214
215    /// The forward takes its regions of elementwise ops, then its runs of them, before a norm takes a sum: the shared
216    /// rules, run first — a region of several joined ops is one kernel where a run would take only one of its arms.
217    fn rules(&self) -> Vec<(Position, ChipRule<'_>)> {
218        let rule = |name: &str| RULES.iter().find(|r| r.name == name).unwrap_or_else(|| panic!("the shared table runs {name}")).apply;
219        vec![
220            (Position::Before("add_norm"), ChipRule { name: "elementwise_regions", builds: &[ClassId::ElementwiseRegions], apply: Box::new(rule("elementwise_regions")) }),
221            (Position::Before("add_norm"), ChipRule { name: "elementwise_chains", builds: &[ClassId::ElementwiseChains], apply: Box::new(rule("elementwise_chains")) }),
222        ]
223    }
224
225    fn activation(&self, consumer: &Op, operand: usize, g: &TensorGraph) -> Option<QuantFormat> {
226        let format = match self.kind {
227            Q8MatKind::Dequant => return None,
228            Q8MatKind::Int8Act => QuantFormat::OneWord,
229            Q8MatKind::TwoPassInt8 => QuantFormat::TwoWords,
230        };
231        (operand == 0 && (Self::q8_product(consumer, g) || moe(consumer).is_some())).then_some(format)
232    }
233
234    fn numerics(&self) -> Option<Contract> {
235        let activations = match self.kind {
236            Q8MatKind::Dequant => None,
237            Q8MatKind::Int8Act => Some(Scheme::of(QuantFormat::OneWord)),
238            Q8MatKind::TwoPassInt8 => Some(Scheme::of(QuantFormat::TwoWords)),
239        };
240        let point = format!("the older forward: {:?} activations", self.kind);
241        Some(Contract { point, activations, cache: Keep::F32, state: Keep::F32, attention: None, fused: Bound::EXACT, accumulator_bits: 32 })
242    }
243}