logicaffeine_compile/codegen_cuda/
forward_shim.rs1use 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
32const ATTN_STATIC_SHARED_BYTES: usize = 48 * 1024;
35
36pub 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#[derive(Clone, Copy, Debug)]
112pub struct ForwardShim {
113 pub kind: Q8MatKind,
114}
115
116impl ForwardShim {
117 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 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}