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;