logicaffeine_compile/spatial/decode_engine/
shim.rs1use crate::model::bind::{self, Source, View};
17use crate::model::spec::ModelSpec;
18use crate::tensor_graph::front::TensorOpSpec;
19use crate::tensor_graph::fuse::{activation, leaves};
20use crate::tensor_graph::graph::{Access, Op, QuantFormat, Sym, TensorGraph};
21use crate::spatial::serving::{AttentionMath, VALUE_BLOCK};
22use crate::tensor_graph::numerics::{AttentionNumerics, Block, Bound, Contract, Keep, Scale, Scheme, Taken};
23use crate::tensor_graph::shim::{Shim, Support};
24use crate::tensor_graph::symbols::ClassId;
25use crate::tensor_recognize::qwen::Activation;
26
27use super::capability::Refusal;
28use super::model::{CacheFormat, EngineNumerics, StateFormat};
29use super::numerics::{ActScaling, Words, ACT_BLOCK, WIDE_BITS};
30
31pub const NO_UNIT: &str = "the engine has no unit for ";
33
34pub struct EngineShim<'a> {
37 pub spec: &'a ModelSpec,
38 pub numerics: EngineNumerics,
39}
40
41impl EngineShim<'_> {
42 fn view(&self, constant: &str) -> Option<(String, View)> {
44 match bind::bind(self.spec, constant) {
45 Ok(Source::Weight(b)) => Some((b.tensor, b.view)),
46 _ => None,
47 }
48 }
49
50 fn one_matrix(&self, parts: &[Op]) -> bool {
54 let Some(views) = parts.iter().map(|p| p.ins.get(1).and_then(|w| self.view(w))).collect::<Option<Vec<_>>>() else {
55 return false;
56 };
57 if views.iter().any(|(t, _)| *t != views[0].0) {
58 return false;
59 }
60 let ranges = views.iter().try_fold(0usize, |next, (_, v)| match v {
61 View::RowRange { first, count, transpose: true } if *first == next => Some(first + count),
62 _ => None,
63 });
64 let halves = match views.as_slice() {
65 [(_, View::HeadPart { stride: s1, offset: 0, width: w1, transpose: true }), rest @ ..] if *s1 == 2 * w1 => match rest {
66 [] => true,
67 [(_, View::HeadPart { stride: s2, offset: o2, width: w2, transpose: true })] => s2 == s1 && o2 == w1 && w2 == w1,
68 _ => false,
69 },
70 _ => false,
71 };
72 ranges.is_some() || halves
73 }
74}
75
76impl Shim for EngineShim<'_> {
77 fn name(&self) -> String {
78 format!("d5005-engine:{}", self.spec.arch)
79 }
80
81 fn supports(&self, class: ClassId) -> Support {
82 use ClassId::*;
83 use Support::{Decompose, Native, Refuse};
84 match class {
85 MergedQkv | ConvBiasSilu | GdnGating | GluActMul => Native,
86 NormWeight | AddNorm | NormThenAdd | NormScale | NormWeightOffset | DualNorm | GatedNorm | EmbedNorm => Decompose,
87 GemvNormEpilogue | PrefillNormEpilogues | AttnResidualNorm | RecomputeFanout | ElementwiseChains | ElementwiseRegions => Decompose,
88 QkNormRope | QkNormRopeKvAppend | RopeKvAppend | RopeQkOneLaunch | QkNormRopeGateSplit | QkvHeadEpilogue => Decompose,
89 HorizontalNormRopeInsert | CrossLayerLaunch | ConvParts | ConvSiluL2norm | GdnGateProjections | GdnInputEpilogue => Decompose,
90 GdnPostConvPrep | GdnDecodeWhole | GemvGateUpGlu | MergedGateUp | GemmDequantInkernel | FusedSampling | LastLayerRows => Decompose,
91 NormQuant | AddNormQuant | GluActMulQuant | ElementwiseQuant | GdnOutputQuant | GemvQuant | AttnOutputQuant | GdnStep => Decompose,
92 AttnOutputGate | RowWriteQuant => Decompose,
93 GemmBias | GemmScale | AffineInputFold => Refuse("the engine's matvec unit adds no bias and scales by no factor"),
94 EmbedScale => Refuse("the engine reads its embedding unscaled"),
95 Mla | MlaPrologue | MlaUnabsorbInFold => Refuse("the engine has no unit for multi-head latent attention"),
96 RouterTopk | MoeTokenSort | MoeGateUpAct | MoeDownCombine | SharedExpertFusion | MoeGatherInOperand => {
97 Refuse("the engine has no unit for experts")
98 }
99 MoeDeferredFinalize | MoeDirectPairs | GroupedTopk | RouterSharedGateMerge | MaskedExpertActivation => {
100 Refuse("the engine has no unit for experts")
101 }
102 FlashDecode | FlashPrefill | AttnLseMerge | AttnGqaRowPacking => Refuse("the engine attends one query over its cache in its attention unit"),
103 AttentionVariantsInline | Softcap | MixedBatchAttention | SparseAttention => {
104 Refuse("the engine's attention takes no mask, window, sink or soft cap")
105 }
106 StridedRead | RowWrite | JoinInPlace | StridedValues => Refuse("the engine's units read and write whole slots"),
107 ReluSqr => Refuse("the engine has no ReLU unit"),
108 Sampling => Refuse("the engine picks the next token by arg-max"),
109 KvCacheQuant | HadamardRotation => Refuse("the engine's cache format is its numerics', not a fusion"),
110 CompensatedResidual => Refuse("the engine sums its residual uncompensated"),
111 HostResidentKv => Refuse("the engine keeps its caches in its card's memory"),
112 PersistentLayerMegakernel | CudaGraphs => Refuse("the engine runs a program of commands, not kernel launches"),
113 AllreduceNorm => Refuse("the engine runs on one card"),
114 }
115 }
116
117 fn fits(&self, op: &Op, _: &TensorGraph) -> Result<(), String> {
118 for leaf in leaves(op.clone()) {
119 if leaf.access.iter().any(|a| *a != Access::Whole) || leaf.dst.row.is_some() || leaf.dst.unit.is_some() {
120 return Err("the engine's units read and write whole slots".into());
121 }
122 let refused = match leaf.prim() {
123 Some(TensorOpSpec::Moe(_)) => Some(Refusal::Ffn { layer: 0, what: "experts" }.gap()),
124 Some(TensorOpSpec::Gelu(_)) => Some(Refusal::Ffn { layer: 0, what: "GELU" }.gap()),
125 Some(TensorOpSpec::Attention(a)) if a.window.is_some() => Some(Refusal::Attention { layer: 0, what: "a sliding window" }.gap()),
126 Some(TensorOpSpec::Attention(a)) if a.sinks.is_some() => Some(Refusal::Attention { layer: 0, what: "attention sinks" }.gap()),
127 Some(TensorOpSpec::Attention(a)) if a.softcap.is_some() => {
128 Some(Refusal::Attention { layer: 0, what: "soft-capped scores" }.gap())
129 }
130 Some(TensorOpSpec::Softcap(_)) => Some(Refusal::LogitSoftcap.gap()),
131 Some(TensorOpSpec::HeadMatmul(_)) => Some(Refusal::Mixer { layer: 0, what: "multi-head latent attention" }.gap()),
132 Some(TensorOpSpec::Scale(_)) => Some(Refusal::EmbeddingScale.gap()),
133 Some(spec @ (TensorOpSpec::Relu(_)
134 | TensorOpSpec::IntRequant(_)
135 | TensorOpSpec::IntClamp(_)
136 | TensorOpSpec::LutAttention(_)
137 | TensorOpSpec::FullAttention(_)
138 | TensorOpSpec::Transpose(_)
139 | TensorOpSpec::HeadSlice(_)
140 | TensorOpSpec::Join(_)
141 | TensorOpSpec::RowsGather(_))) => Some(format!("a {}", spec.kind())),
142 _ => None,
143 };
144 if let Some(what) = refused {
145 return Err(format!("{NO_UNIT}{what}"));
146 }
147 }
148 let Sym::Fused(f) = &op.sym else { return Ok(()) };
149 match f.class {
150 ClassId::MergedQkv if !self.one_matrix(&f.parts) => {
151 Err("the engine streams one checkpoint matrix a matvec: row ranges of one tensor from its first row, or each head's two halves".into())
152 }
153 ClassId::GluActMul if f.parts.iter().find_map(activation) != Some(Activation::Sigmoid) => {
154 Err("the engine gates an attention's result by a sigmoid only".into())
155 }
156 _ => Ok(()),
157 }
158 }
159
160 fn activation(&self, _: &Op, _: usize, _: &TensorGraph) -> Option<QuantFormat> {
161 None
162 }
163
164 fn numerics(&self) -> Option<Contract> {
165 let n = self.numerics;
166 let (block, significand_bits) = match n.scaling {
167 ActScaling::Sub32 => (Block::Values(ACT_BLOCK), 4),
168 ActScaling::PerBlock => (Block::OfTheWeights, WIDE_BITS),
169 };
170 let words = match n.activations {
171 Words::One => 1,
172 Words::Two => 2,
173 };
174 let state = match n.state {
175 StateFormat::F32 => Keep::F32,
176 StateFormat::F16 => Keep::F16,
177 };
178 let cache = match n.cache {
179 CacheFormat::F16 => Keep::F16,
180 CacheFormat::Int8 => Keep::Int8Rows,
181 CacheFormat::Int8Hadamard => Keep::Int8RowsRotated,
182 };
183 let activations = Scheme { block, scale: Scale::LeastPowerOfTwo { significand_bits }, words };
184 let attention = match n.attention {
188 AttentionMath::Vector => None,
189 AttentionMath::Array => Some(AttentionNumerics {
190 queries: Taken::Quantized(Scheme { block: if block == Block::OfTheWeights { Block::Row } else { block }, ..activations }),
191 keys_values: Taken::Kept(cache),
192 weights: Taken::Quantized(Scheme { block: Block::Values(VALUE_BLOCK as usize), scale: Scale::LeastPowerOfTwoOver255, words: 1 }),
193 }),
194 };
195 Some(Contract { point: format!("the engine: {n:?}"), activations: Some(activations), cache, state, attention, fused: Bound::EXACT, accumulator_bits: 64 })
198 }
199}