spark_model/layers/mod.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3pub mod attn_v41;
4pub mod deepseek_v41_layer;
5pub mod deepseek_v41_ref;
6pub mod deepseek_v4_mtp;
7pub mod dense_ffn;
8pub mod dflash_head;
9pub mod engram_v41;
10pub mod ep_dispatch;
11pub mod fp8_calibration;
12mod gemv_tier;
13/// GLM-5.3-Flash KDA integrated layer (Slice 6 -- one layer, no scheduler/cache wiring).
14pub mod glm5next_dsa;
15/// GLM-5.3-Flash DSA + kpool indexer CPU reference (Slice 8 design artifact).
16pub mod glm5next_dsa_ref;
17pub mod glm5next_kda;
18/// GLM-5.3-Flash KDA CPU reference (Slice 2 design artifact -- not a production forward path).
19pub mod glm5next_kda_ref;
20/// GLM-5.3-Flash composite decoder layer -- mixer (KDA|DSA) + MLP (dense|MoE) + mHC.
21pub mod glm5next_layer;
22/// GLM-5.3-Flash MLP production surface -- dense FFN + routed NVFP4 MoE (TP + EP sharded).
23pub mod glm5next_mlp;
24pub mod glm5next_mtp_head;
25/// GLM-5.3-Flash 45-layer text-model skeleton (Slice 9 -- topology, wiring, structural binding).
26pub mod glm5next_skeleton;
27pub mod glm_vit;
28pub mod moe;
29pub mod moe_v41;
30pub mod mtp_head;
31pub(crate) mod mtp_meta;
32pub mod mtp_multi;
33pub mod nemotron_mamba2;
34pub mod nemotron_moe;
35pub mod ngram_embed;
36pub mod ops;
37pub mod ple;
38pub mod qsa;
39pub mod qwen3_attention;
40pub mod qwen3_ssm;
41pub mod vision_encoder;
42pub mod vision_tower;
43pub mod w4a16_gemv_tiers;
44
45/// Minimum K at which the deep-K `w4a16_gemm_t_k64` (K_STEP_T=64) beats the
46/// K_STEP_T=32 `w4a16_gemm_t`.
47///
48/// ★ 6144, not 4096. Measured with `w4a16_m17_bench` on the REAL decode shapes at
49/// M=16 against the STREAM-measured 230 GB/s ceiling — `_k64` is the WORST tile
50/// variant at K=5120 and the best only at K>=6144:
51///
52/// ssm_qkvz N=16384 K=5120 _t 281.9us _k64 341.6us _m128 272.4us
53/// attn qkv N=14336 K=5120 _t 273.9us _k64 328.5us _m128 262.8us
54/// ssm_out_proj N=5120 K=6144 _t 237.7us _k64 163.3us _m128 240.7us
55///
56/// The original 4096 threshold (this session) was derived from the ffn/out_proj
57/// shapes and wrongly generalised to K=5120, sending 48 qkvz + 16 fused-qkv
58/// launches per step to the slowest variant. Both variants accumulate K
59/// sequentially, so moving between them is byte-identical.
60///
61/// `AVAROK_NO_W4A16_K64=1` restores the pre-session 8192 threshold.
62pub(crate) fn w4a16_k64_min_k() -> u32 {
63 static MIN_K: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
64 *MIN_K.get_or_init(|| {
65 // Explicit override so an A/B can pin a previous threshold exactly.
66 if let Some(n) = std::env::var("AVAROK_W4A16_K64_MIN_K")
67 .ok()
68 .and_then(|v| v.parse::<u32>().ok())
69 {
70 return n;
71 }
72 if std::env::var("AVAROK_NO_W4A16_K64").ok().as_deref() == Some("1") {
73 8192
74 } else {
75 6144
76 }
77 })
78}
79
80pub use deepseek_v4_mtp::{DeepseekV4MtpHead, DeepseekV4MtpProposerState};
81pub use dense_ffn::{DenseFfnLayer, DenseFfnWeights, FfnActivation};
82pub use dflash_head::{
83 BlockDiffusionDraftHead, DflashLayer, DflashProposerState, DflashQuantization, dflash_ctx_cap,
84};
85pub use glm_vit::{GlmVit, GlmVitBlock, GlmVitMerger};
86pub use glm5next_mtp_head::Glm5NextMtpHead;
87pub use moe::MoeLayer;
88pub use mtp_head::{MtpHead, MtpQuantization, mtp_drafter_prefill_enabled};
89pub use nemotron_mamba2::NemotronMamba2Layer;
90pub use nemotron_moe::NemotronMoeLayer;
91pub use qwen3_attention::Qwen3AttentionLayer;
92pub use qwen3_ssm::Qwen3SsmLayer;
93pub use vision_encoder::{MergerLayer, ViTBlock, VisionEncoder};
94pub use vision_tower::VisionTower;
95
96use crate::layer::ForwardContext;
97use anyhow::Result;
98use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
99
100/// Try to load an optional kernel, logging at debug level if it's not found.
101/// Returns `KernelHandle(0)` (null) on failure — callers must check before use.
102///
103/// Debug (not warn) because misses are expected when a model doesn't use a
104/// given feature: e.g. Qwen3-Coder-Next (GDN+attention) never calls MLA
105/// kernels, but the layer builder still probes them. Warning on expected
106/// misses drowned out genuine problems in startup logs.
107/// Resolve the `w4a16_gemm_t_m128_v2` handle honoring `AVAROK_W4A16_VARIANT`.
108///
109/// One resolver for the THREE sites that dispatch on this handle (attention
110/// projections, dense-FFN prefill, SSM batched decode), so variant policy and
111/// rollback live in exactly one place. Default (unset) resolves to a ZERO
112/// handle — v1 everywhere — because the 27B port measured SLOWER than v1
113/// (see body). `AVAROK_W4A16_VARIANT=v2` opts in on all three sites at once;
114/// requesting it on a target without the kernel is a HARD startup error
115/// (fail fast, not a silent fallback discovered in a perf regression).
116#[track_caller]
117pub fn w4a16_v2_kernel(gpu: &dyn GpuBackend) -> KernelHandle {
118 let variant = std::env::var("AVAROK_W4A16_VARIANT").ok();
119 // DEFAULT OFF on the qwen3 layer stack: the 27B port of the 8-warp v2
120 // crush kernel is bit-identical to v1 (microtest 100% on 8 shapes) but
121 // MEASURED SLOWER on the 27B FFN shapes — 0.78-0.82x of v1 standalone
122 // (w4a16_bf16_v2_bench, 2026-07-30; v1 58-74 TFLOP/s). The kernel stays
123 // in the PTX set for A/B and for shape regimes where the extra warps
124 // might pay; nothing auto-activates it. `AVAROK_W4A16_VARIANT=v2` opts in
125 // (hard error if the target lacks the kernel).
126 if !matches!(variant.as_deref(), Some("v2") | Some("v3")) {
127 if variant.as_deref() == Some("v1") {
128 tracing::debug!("AVAROK_W4A16_VARIANT=v1: w4a16 m128 v2 suppressed (explicit)");
129 }
130 return KernelHandle(0);
131 }
132 let h = try_kernel(gpu, "w4a16_v2", "w4a16_gemm_t_m128_v2");
133 if h.0 == 0 {
134 panic!(
135 "AVAROK_W4A16_VARIANT={} requested but w4a16_v2::w4a16_gemm_t_m128_v2 is not in this \
136 target's kernel set — refusing to start with a silently-degraded config",
137 variant.unwrap()
138 );
139 }
140 tracing::debug!(
141 handle = h.0,
142 "w4a16_gemm_t_m128_v2 resolution (explicit opt-in)"
143 );
144 h
145}
146
147/// Resolve the W4A16 m128 **v3** GEMM. Opt-in ONLY, same contract as
148/// [`w4a16_v2_kernel`]: `AVAROK_W4A16_VARIANT=v3` selects it, anything else
149/// resolves to a ZERO handle WITHOUT issuing a lookup.
150///
151/// Not issuing the lookup is the point. `prefill_weights` dispatches on
152/// `v == 3 && handle != 0`, so on the default (`v1`) the probe could never be
153/// used — it only ever added a permanently-failing row to the boot audit on
154/// every target that does not ship `w4a16_v3`. Requesting the variant on such a
155/// target is a HARD error, not a silent fallback discovered in a perf report.
156#[track_caller]
157pub fn w4a16_v3_kernel(gpu: &dyn GpuBackend) -> KernelHandle {
158 if std::env::var("AVAROK_W4A16_VARIANT").as_deref() != Ok("v3") {
159 return KernelHandle(0);
160 }
161 let h = try_kernel(gpu, "w4a16_v3", "w4a16_gemm_t_m128_v3");
162 if h.0 == 0 {
163 panic!(
164 "AVAROK_W4A16_VARIANT=v3 requested but w4a16_v3::w4a16_gemm_t_m128_v3 is not in this \
165 target's kernel set — refusing to start with a silently-degraded config"
166 );
167 }
168 h
169}
170
171/// Resolve the N128/M64 tile GEMM, preferring the 3-deep weight-pipeline variant.
172/// **ON by default**; `AVAROK_NO_TGEMM_PIPELINE3` (presence — `=0` is NOT "off")
173/// falls back to the 2-stage parent. Falls back automatically on any target that
174/// does not ship `_p3`.
175///
176/// Same mechanism as [`k64_kernel`]: the parent drains its cp.async group before
177/// the dequant phase, which only a co-resident CTA can cover. This kernel's live
178/// shapes — ssm_qkvz (128 CTAs) and the fused QKV (112) — sit in the exposed
179/// band of the grid.x-vs-efficiency curve. Bit-identical.
180#[track_caller]
181pub fn tgemm_kernel(gpu: &dyn GpuBackend) -> KernelHandle {
182 if std::env::var("AVAROK_NO_TGEMM_PIPELINE3").is_err() {
183 let h = try_kernel(gpu, "w4a16", "w4a16_gemm_t_p3");
184 if h.0 != 0 {
185 return h;
186 }
187 }
188 try_kernel(gpu, "w4a16", "w4a16_gemm_t")
189}
190
191/// K3's host-reference layer binds BF16/FP32, not the NVFP4 tile GEMM.
192pub fn tgemm_probe_ok(model_type: &str) -> bool {
193 !matches!(model_type, "kimi_k3" | "kimi_linear")
194}
195
196/// Resolve the k64 deep-K tile GEMM, preferring the 3-deep weight-pipeline
197/// variant. **ON by default**; `AVAROK_NO_K64_PIPELINE3` (presence — `=0` is NOT
198/// "off") falls back to the 2-stage parent.
199///
200/// The parent issues one cp.async group then `wait_all`s it before the dequant
201/// phase, so with a small grid there are ZERO outstanding loads across that
202/// phase. The out_proj/o_proj shapes (N=5120, K=6144) launch 40 CTAs on 48 SMs —
203/// exactly 1 CTA/SM — so nothing covers the drain, and they measure ~38% of
204/// achievable while lm_head (1938 CTAs) reaches 83% on the identical loop.
205/// `_p3` keeps step i+2's loads in flight across dequant(i+1). Bit-identical.
206#[track_caller]
207pub fn k64_kernel(gpu: &dyn GpuBackend) -> Result<KernelHandle> {
208 let want_p3 = std::env::var("AVAROK_NO_K64_PIPELINE3").is_err();
209 if want_p3 {
210 let h = try_kernel(gpu, "w4a16", "w4a16_gemm_t_k64_p3");
211 if h.0 != 0 {
212 return Ok(h);
213 }
214 }
215 gpu.kernel("w4a16", "w4a16_gemm_t_k64")
216}
217
218/// Resolve the NARROW-N (N_TILE=64) deep-K twin. `KernelHandle(0)` when the
219/// kernel is absent or the presence kill switch `AVAROK_NO_K64_N64` is set
220/// (`=0` is NOT "off"). Callers must store the handle — `kernel()` is an
221/// init-time lookup, not a per-launch one.
222#[track_caller]
223pub fn k64_n64_kernel(gpu: &dyn GpuBackend) -> KernelHandle {
224 if std::env::var("AVAROK_NO_K64_N64").is_ok() {
225 return KernelHandle(0);
226 }
227 try_kernel(gpu, "w4a16", "w4a16_gemm_t_k64_n64_p3")
228}
229
230/// Wide-tile CTA count below which the N_TILE=64 deep-K twin wins.
231///
232/// `w4a16_gemm_t_k64_p3` owns a 128-wide N tile and a 64-row M tile, so a
233/// launch is `ceil(n/128) * ceil(m/64)` CTAs of 128 threads. At the out_proj /
234/// o_proj shape (N=5120, K=6144 — 64 launches per decode step at n=64) that is
235/// **40 CTAs on a 48-SM device**: 8 SMs idle by construction and the other 40
236/// hold ONE CTA each, i.e. 4 warps against a 48-warp SM budget.
237///
238/// That is NOT a bandwidth problem. A full-working-set replay (rotating over
239/// enough distinct weight tensors that every launch sees a cold 24 MB L2)
240/// decomposes the 167.8 us launch as: memory pipe alone 87.2 us = **95.6% of
241/// the 229.6 GB/s row-strided ceiling** — already saturated — with the
242/// remaining 80.6 us being barrier-serialized dequant that has no co-resident
243/// warp to hide it. Halving the N tile doubles the grid to 80 CTAs, filling all
244/// 48 SMs with 1-2 co-resident CTAs, and moves the SAME bytes: each CTA still
245/// owns a disjoint N slice and the extra A re-reads are L2 hits (A is 786 KB).
246///
247/// Measured (replay, cold L2, bit-identical output at every point):
248/// wide CTAs 20 -> 1.74x 40 -> 1.42x 48 -> 1.33x 64 -> 1.15x
249/// wide CTAs 76 -> 0.77x 80 -> 0.80x 96 -> 0.79x 244 -> 0.95x
250/// Above ~64 CTAs the wide tile already fills the machine and the narrow tile
251/// only pays more epilogue and A traffic, so the gate is a hard `<= 64`.
252const K64_N64_MAX_WIDE_CTAS: u32 = 64;
253
254/// Should the narrow-N deep-K twin serve this shape? See
255/// `K64_N64_MAX_WIDE_CTAS` for the derivation and the measured curve.
256pub fn k64_n64_wins(m: u32, n: u32) -> bool {
257 n.div_ceil(128) * m.div_ceil(64) <= K64_N64_MAX_WIDE_CTAS
258}
259
260/// Optional kernel lookup: `KernelHandle(0)` instead of an error.
261///
262/// `#[track_caller]` so the audit names the DISPATCH SITE — this helper stands
263/// between ~500 call sites and `GpuBackend::kernel`, and without it every
264/// optional lookup in the binary would be reported against this one line.
265///
266/// A zero handle is a SILENT slower path, so a lookup that lands here for a
267/// model that genuinely needs the kernel is a bug. Either gate the call on the
268/// model's config so it is never issued, or declare it in the target's
269/// MODEL.toml `[expected_absent]` with a reason; the boot gate
270/// (`kernel_audit::classify_failures`) fails closed on anything else.
271/// Minimum rows in flight for the grouped-GEMM MoE decode arm. SSOT for the
272/// SSM stack (`qwen3_ssm::trait_decode_multi_seq`) and the attention layers
273/// (`qwen3_attention::…::multi_seq::ffn`), which must agree — they are the
274/// same trade on the same weights.
275///
276/// The arm reads each routed expert ONCE instead of once per token, so it
277/// wins when there are enough tokens to amortise the expert sort/permute
278/// launch overhead, and loses when there are not. Both ends are measured:
279///
280/// | n | verdict | measurement |
281/// |---|---|---|
282/// | 4 | LOSS | 31 vs 56 tok/s on Holo — the fixed per-layer sort/permute dominates at small N |
283/// | >=16 | WIN | SSM-side alone C=32 172.7 -> 216.2 tok/s (+25%); #415's attention-side extension +7.9% at C=32 / +9.7% at C=64 on Qwen3.6-35B-A3B-NVFP4, paired gsm8k n=200 strict 0.960 vs 0.900 baseline, zero regressions |
284///
285/// 16 is the smallest width measured on the winning side. n=5..15 is
286/// UNMEASURED, not a known win — it sits on the losing side of this gate on
287/// purpose, because the one thing we know about the gap is that the loss at
288/// n=4 is large (-45%) and the win at n=16 is smaller (+25%).
289pub fn moe_grouped_decode_min_rows() -> usize {
290 16
291}
292
293/// Kill switch for the grouped-GEMM MoE decode arm. PRESENCE check per the
294/// house convention (`AVAROK_NO_MOE_GROUPED_DECODE=0` is NOT off), read once
295/// per process — this predicate sits in the decode path, and the `env::var`
296/// it replaces ran on every dispatch for MoE models.
297pub fn moe_grouped_decode_enabled() -> bool {
298 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
299 *ON.get_or_init(|| std::env::var_os("AVAROK_NO_MOE_GROUPED_DECODE").is_none())
300}
301
302/// Force the grouped arm BELOW `moe_grouped_decode_min_rows()`. Diagnostic
303/// only — it exists so the n=5..15 gap can be measured without a rebuild, and
304/// it is the same var #415's measurements used, kept working on purpose.
305/// Never a production setting: if forcing wins at a width, move the THRESHOLD.
306pub fn moe_grouped_decode_forced() -> bool {
307 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
308 *ON.get_or_init(|| std::env::var("AVAROK_MOE_GROUPED_DECODE").as_deref() == Ok("1"))
309}
310
311/// Whether the grouped-GEMM MoE decode arm should run for `n` rows —
312/// PURE, so both polarities are testable without touching process env or the
313/// `OnceLock`s below (which latch, and would make the tests order-dependent).
314pub fn moe_grouped_decode_decide(n: usize, enabled: bool, forced: bool) -> bool {
315 enabled && (n >= moe_grouped_decode_min_rows() || forced)
316}
317
318/// Whether the grouped-GEMM MoE decode arm should run for `n` rows.
319pub fn moe_grouped_decode_for(n: usize) -> bool {
320 moe_grouped_decode_decide(n, moe_grouped_decode_enabled(), moe_grouped_decode_forced())
321}
322
323#[cfg(test)]
324#[path = "moe_grouped_decode_tests.rs"]
325mod moe_grouped_decode_tests;
326
327mod kernel_probe;
328pub use kernel_probe::{try_kernel, try_target_kernel};
329
330/// FFN component: MoE (expert routing), dense SwiGLU, or None (standalone attention).
331#[allow(clippy::large_enum_variant)]
332pub enum FfnComponent {
333 Moe(MoeLayer),
334 Dense(DenseFfnLayer),
335 /// No FFN — used by Nemotron-H standalone attention layers.
336 None,
337}
338
339impl FfnComponent {
340 pub fn is_none(&self) -> bool {
341 matches!(self, Self::None)
342 }
343
344 /// True for a plain dense (SwiGLU) FFN. Wide-batch verify paths gate their
345 /// `forward_prefill` fast path on this: batching reads dense weights once
346 /// (big win at N=17), but on a 256-expert MoE the grouped-GEMM is a net
347 /// loss at small batch (per-expert M~1 + sort/permute overhead), so MoE
348 /// keeps its per-token loop.
349 pub fn is_dense(&self) -> bool {
350 matches!(self, Self::Dense(_))
351 }
352
353 /// True when this MoE FFN can serve DECODE through the grouped read-once
354 /// GEMM (forward_prefill) instead of the pairwise per-slot loop. The
355 /// is_dense() comment above asserts grouped is "a net loss at small batch"
356 /// on a 256-expert MoE, but that was never measured for decode CONCURRENCY
357 /// (n=4) where the pairwise path re-reads ~14-20 distinct experts as 40
358 /// per-slot CTAs. Native-NVFP4-routed only (forward_prefill's unconditional
359 /// grouped path); dense/none are false.
360 pub fn moe_grouped_decode_ok(&self) -> bool {
361 match self {
362 Self::Moe(m) => m.grouped_decode_ok(),
363 _ => false,
364 }
365 }
366
367 /// AVAROK_FP32_ROUTING active for this FFN (MoE only; false otherwise).
368 pub fn fp32_routing_active(&self, levers: &ops::ModelLevers) -> bool {
369 match self {
370 Self::Moe(m) => m.fp32_routing_active(levers),
371 _ => false,
372 }
373 }
374
375 pub fn forward(
376 &self,
377 input: DevicePtr,
378 ctx: &ForwardContext,
379 stream: u64,
380 ) -> Result<DevicePtr> {
381 match self {
382 Self::Moe(m) => m.forward(input, ctx, stream),
383 Self::Dense(d) => d.forward(input, ctx, stream),
384 Self::None => Ok(input),
385 }
386 }
387
388 pub fn forward_k2(&self, input: DevicePtr, ctx: &ForwardContext, stream: u64) -> Result<()> {
389 match self {
390 Self::Moe(m) => m.forward_k2(input, ctx, stream),
391 Self::Dense(d) => d.forward_k2(input, ctx, stream),
392 Self::None => Ok(()),
393 }
394 }
395
396 pub fn forward_k3(&self, input: DevicePtr, ctx: &ForwardContext, stream: u64) -> Result<()> {
397 match self {
398 Self::Moe(m) => m.forward_k3(input, ctx, stream),
399 Self::Dense(d) => d.forward_k3(input, ctx, stream),
400 Self::None => Ok(()),
401 }
402 }
403
404 /// Whether the K=m (m<=8) batched-GEMV verify FFN is available (dense
405 /// only — MoE / missing batch4/batch8 kernel / non-NVFP4 weights →
406 /// false). Lets callers gate branch entry BEFORE computing the pre-FFN
407 /// norm, so there is no half-done fallthrough to `forward_prefill`.
408 pub fn can_forward_km(&self, m: u32) -> bool {
409 matches!(self, Self::Dense(d) if d.can_forward_km(m))
410 }
411
412 /// K=m (m=4..8) verify FFN via batched GEMV (dense only). Returns
413 /// `false` when the path is unavailable (MoE / missing batchm kernel /
414 /// non-NVFP4 weights) so the caller can fall back to `forward_prefill`.
415 pub fn try_forward_km(
416 &self,
417 input: DevicePtr,
418 m: u32,
419 ctx: &ForwardContext,
420 stream: u64,
421 ) -> Result<bool> {
422 match self {
423 Self::Dense(d) if d.can_forward_km(m) => {
424 d.forward_km(input, m, ctx, stream)?;
425 Ok(true)
426 }
427 _ => Ok(false),
428 }
429 }
430
431 pub fn forward_prefill(
432 &self,
433 input: DevicePtr,
434 num_tokens: usize,
435 ctx: &ForwardContext,
436 stream: u64,
437 ) -> Result<()> {
438 match self {
439 Self::Moe(m) => m.forward_prefill(input, num_tokens, ctx, stream),
440 Self::Dense(d) => d.forward_prefill(input, num_tokens, ctx, stream),
441 Self::None => {
442 let _ = (input, num_tokens);
443 Ok(())
444 }
445 }
446 }
447
448 pub fn forward_batched(
449 &self,
450 input: DevicePtr,
451 num_tokens: usize,
452 ctx: &ForwardContext,
453 stream: u64,
454 ) -> Result<()> {
455 match self {
456 Self::Moe(m) => m.forward_batched(input, num_tokens, ctx, stream),
457 Self::Dense(d) => d.forward_batched(input, num_tokens, ctx, stream),
458 Self::None => {
459 let _ = (input, num_tokens);
460 Ok(())
461 }
462 }
463 }
464
465 pub fn forward_token_major_decode(
466 &self,
467 input: DevicePtr,
468 num_tokens: usize,
469 ctx: &ForwardContext,
470 stream: u64,
471 ) -> Result<()> {
472 match self {
473 Self::Moe(m) => m.forward_token_major_decode(input, num_tokens, ctx, stream),
474 Self::Dense(d) => d.forward_batched(input, num_tokens, ctx, stream),
475 Self::None => {
476 let _ = (input, num_tokens);
477 Ok(())
478 }
479 }
480 }
481
482 pub fn forward_atomic_c4_decode(
483 &self,
484 input: DevicePtr,
485 num_tokens: usize,
486 ctx: &ForwardContext,
487 stream: u64,
488 ) -> Result<()> {
489 match self {
490 Self::Moe(m) => m.forward_atomic_c4_decode(input, num_tokens, ctx, stream),
491 Self::Dense(d) => d.forward_batched(input, num_tokens, ctx, stream),
492 Self::None => {
493 let _ = (input, num_tokens);
494 Ok(())
495 }
496 }
497 }
498}
499
500pub(crate) use gemv_tier::batch8_kernel;