spark_model/layers/
mod.rs

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