spark_model/layers/moe/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! MoE (Mixture of Experts) FFN component.
4//!
5//! Batched expert dispatch: top-K experts run in 2 fused kernel launches
6//! (gate+up, silu+down) instead of 10 × 5 individual launches. Expert indices
7//! and weights stay on device — zero D2H synchronization.
8
9use anyhow::Result;
10use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
11
12use crate::layer::ForwardContext;
13use crate::layers::ops;
14use crate::weight_map::{DenseWeight, Fp8ExpertWeight, MoeWeights, QuantizedWeight};
15
16/// MoE feed-forward network component.
17///
18/// Not a `TransformerLayer` — used as a component inside layers
19/// for the FFN/MoE block after post-attention norm.
20#[allow(dead_code)]
21pub struct MoeLayer {
22    pub weights: MoeWeights,
23    /// Quant format of the ROUTED experts as landed in GPU memory. `Nvfp4`
24    /// (default) = packed E2M1 + FP8-E4M3 per-16 block scales + f32 per-tensor
25    /// global. Set to `Mxfp4E8m0` by the DeepSeek-V4 native-MXFP4 loader
26    /// (transcode-free: E8M0 per-32 scales, no global) so the Phase-K E8M0
27    /// GEMM variants dispatch on it instead of the NVFP4 kernels. Consumed at
28    /// the grouped/decode GEMM call sites (assert via `WeightQuantFormat::expect`).
29    // Written by the loader (Phase L); READ at the GEMM dispatch sites in Phase K.
30    // Until Phase K wires the read, `deny(warnings)` would flag it never-read.
31    #[allow(dead_code)]
32    pub(crate) experts_scale_kind: crate::weight_map::WeightQuantFormat,
33    /// Quant format of the SHARED expert (ARM-2 Phase-K RIDER A1). The native
34    /// V4 ckpt is heterogeneous: routed experts `Mxfp4E8m0` but the shared
35    /// expert is FP8→`Nvfp4`. Keyed off the weight tag (not `is_shared`
36    /// positionality) so the dual-format decode kernel's `expect` net fires if
37    /// a future ckpt ships a different shared format. Default `Nvfp4`.
38    #[allow(dead_code)]
39    pub(crate) shared_experts_scale_kind: crate::weight_map::WeightQuantFormat,
40    // NVFP4-quantized gate weight (quarters bandwidth for routing)
41    gate_nvfp4: Option<QuantizedWeight>,
42    /// Pre-expert norm: applied to input AFTER routing but BEFORE expert dispatch.
43    /// Gemma-4 26B: router sees raw residual, experts see pre_feedforward_layernorm_2(residual).
44    pub pre_expert_norm: Option<crate::weight_map::DenseWeight>,
45    pre_expert_norm_k: spark_runtime::gpu::KernelHandle,
46    dense_gemv: KernelHandle,
47    w4a16_gemv: KernelHandle,
48    /// Single-warp `w4a16_gemv_sw`. `KernelHandle(0)` on miss → base GEMV.
49    w4a16_gemv_sw: KernelHandle,
50    w4a16_gemm: KernelHandle,
51    dense_gemm: KernelHandle,
52    /// Order-preserving register-blocked router GEMM (`dense_gemm_bf16_router`):
53    /// bit-identical to the scalar `dense_gemm` (same per-output FP32 k-order,
54    /// `--fmad=false` build) at ~2x speed. `KernelHandle(0)` on miss → the
55    /// pinned scalar kernel. Used ONLY by `router_gate_gemm_dense`.
56    dense_gemm_router: KernelHandle,
57    dense_gemm_pipelined: KernelHandle,
58    /// FP32-output router GEMM + FP32-input top-K for the ATLAS_FP32_GATE path.
59    /// Zero (unresolved) when the kernels are absent; dispatch falls back to BF16.
60    dense_gemm_f32out: KernelHandle,
61    /// FP32-in/FP32-out router GEMM for ATLAS_FP32_ROUTING (reads the FP32
62    /// router_in from residual_add_rms_norm_gatef32). Zero if absent.
63    dense_gemm_f32in: KernelHandle,
64    moe_topk_f32: KernelHandle,
65    moe_expert_gate_up_shared: KernelHandle,
66    moe_expert_silu_down_shared: KernelHandle,
67    moe_topk: KernelHandle,
68    moe_weighted_sum_blend: KernelHandle,
69    residual_add: KernelHandle,
70    moe_topk_batched: KernelHandle,
71    // K=2 fused MoE kernel handles
72    moe_expert_gate_up_shared_batch2: KernelHandle,
73    moe_expert_silu_down_shared_batch2: KernelHandle,
74    moe_weighted_sum_blend_batch2: KernelHandle,
75    w4a16_gemv_batch2: KernelHandle,
76    // K=3 fused MoE kernel handles
77    moe_expert_gate_up_shared_batch3: KernelHandle,
78    moe_expert_silu_down_shared_batch3: KernelHandle,
79    moe_weighted_sum_blend_batch3: KernelHandle,
80    w4a16_gemv_batch3: KernelHandle,
81    // Generic token-major NVFP4 MoE kernels. Used as an opt-in decode
82    // concurrency experiment for N>=4 without grouped-GEMM sorting.
83    moe_expert_gate_up_shared_token_major: KernelHandle,
84    moe_expert_silu_down_shared_token_major: KernelHandle,
85    moe_weighted_sum_blend_token_major: KernelHandle,
86    moe_decode_atomic_c4_silu_down_accum_k: KernelHandle,
87    moe_decode_atomic_c4_finalize_k: KernelHandle,
88    // Sorted/grouped prefill path
89    moe_sort_by_expert: KernelHandle,
90    moe_sorted_gate_up: KernelHandle,
91    moe_sorted_silu_down: KernelHandle,
92    moe_grouped_gemm: KernelHandle,
93    /// Wider-K, grouped-dequant twin of `moe_grouped_gemm`, bit-exact with
94    /// it. `try_kernel` — absent on targets whose shadow predates it.
95    /// Opt-in via ATLAS_MOE_GROUPED_K32=1: measured on qwen4_exp only, and
96    /// the win is shape-dependent, so it is not switched on for every model
97    /// that happens to compile it.
98    moe_grouped_gemm_k32: KernelHandle,
99    /// M_TILE=256 twin: ONE pass over the expert weights instead of three
100    /// when rows/expert <= 256. Bit-exact. Opt-in via ATLAS_MOE_GROUPED_M256
101    /// because the win inverts for models with many rows per expert.
102    moe_grouped_gemm_m256: KernelHandle,
103    moe_silu_mul: KernelHandle,
104    /// Activation kernel for sorted/unfused path. SiLU by default, GeGLU for Gemma-4.
105    moe_act_mul: KernelHandle,
106    /// When true, decode uses the sorted prefill path (avoids fused SiLU kernels).
107    gelu_activation: bool,
108    moe_unpermute_reduce: KernelHandle,
109    moe_batched_blend: KernelHandle,
110    /// Pointer tables for batched expert dispatch.
111    gate_ptrs: ExpertPtrTable,
112    up_ptrs: ExpertPtrTable,
113    down_ptrs: ExpertPtrTable,
114    /// Transposed pointer tables for coalesced prefill GEMM.
115    gate_ptrs_t: Option<ExpertPtrTable>,
116    up_ptrs_t: Option<ExpertPtrTable>,
117    down_ptrs_t: Option<ExpertPtrTable>,
118    /// CUTLASS grouped-NVFP4 host tables (`ATLAS_HOLO_MOE_GROUPED_CUTLASS`).
119    /// Per-expert packed/SFB pointer values + scale2, snapshotted ONCE at load
120    /// by `build_cutlass_grouped_sfb` (the SFB swizzle is built there from the
121    /// `gate_ptrs_t`/`up_ptrs_t` `[K/16,N]` scales via `pack_weight_sfb`). The
122    /// grouped C entry consumes these host-side, so the snapshot lives here —
123    /// owned by the layer, dying with the model — rather than in any global
124    /// cache keyed on device addresses, which a model swap's free/realloc
125    /// would turn stale. `None` => the CUTLASS grouped path is unavailable.
126    cutlass_grouped_host: Option<ops::MoeCutlassHostTables>,
127    /// Keeps the per-expert SFB buffers alive.
128    _cutlass_sfb_owned: Vec<DevicePtr>,
129    /// Lazy down_proj transpose scratch — populated at the start of each
130    /// prefill call when the persistent transpose pass couldn't fit
131    /// down_proj. Decode keeps using `down_ptrs` (untransposed); prefill
132    /// uses `down_ptrs_t` pointing into this scratch. Shared across all
133    /// MoE layers (the same scratch is overwritten layer-by-layer during
134    /// the sequential forward).
135    ///
136    /// `down_t_scratch_packed`: contiguous `[num_experts × N × K/2]` bytes.
137    /// `down_t_scratch_scale`:  contiguous `[num_experts × N × K/16]` bytes.
138    /// Both `None` when the persistent transpose pass already covered
139    /// down (full-fits path) or when the layer doesn't need scratch
140    /// transpose (FP8 experts, etc.).
141    down_t_scratch_packed: Option<DevicePtr>,
142    down_t_scratch_scale: Option<DevicePtr>,
143    /// Kernel handle for the batched per-expert uint8 transpose.
144    moe_transpose_u8_batched_k: KernelHandle,
145    // ── Phase 8a transposed-layout decode kernels (unified-layout MoE).
146    // Loaded eagerly at construction. Currently NOT wired into the
147    // dispatch — Phase 8a part 3/3 will route decode through these once
148    // the weight loader produces transposed-only pointer tables.
149    moe_expert_gate_up_shared_t_k: KernelHandle,
150    moe_expert_silu_down_shared_t_k: KernelHandle,
151    // ARM-2 Phase-K: native-MXFP4 (E8M0 routed / NVFP4 shared) dual-format
152    // decode variants. KernelHandle(0) on models that don't ship them.
153    moe_expert_gate_up_shared_t_e8m0_k: KernelHandle,
154    moe_expert_silu_down_shared_t_e8m0_k: KernelHandle,
155    // ── sqrtsoftplus routing (DeepSeek-V4) ──
156    moe_topk_sqrtsoftplus_k: KernelHandle,
157    moe_topk_sqrtsoftplus_batched_k: KernelHandle,
158    // ── hash routing (DeepSeek-V4 first `num_hash_layers` MoE layers) ──
159    moe_hash_route_k: KernelHandle,
160    moe_hash_route_batched_k: KernelHandle,
161    // ── LongCat softmax+bias routing with zero-computation experts ──
162    /// Router logit width = num_experts + zero_expert_num. Equal to
163    /// num_experts on every non-LongCat model (behavior-neutral).
164    pub(crate) router_logits_n: u32,
165    moe_topk_softmax_bias_k: KernelHandle,
166    moe_topk_softmax_bias_batched_k: KernelHandle,
167    moe_zero_expert_add_k: KernelHandle,
168    /// Per-token folded zero-expert weight (f32, written by the softmax+bias
169    /// router kernels). Fixed-size allocation (16K tokens) — graph-safe.
170    zero_accum_dev: DevicePtr,
171    /// Static `tid2eid` table [vocab_size, top_k] i64 — present ONLY for the
172    /// hash-routed layers (the loader supplies it only for those). `Some`
173    /// here is the SSOT that this layer routes via the static hash table
174    /// instead of the learned gate's top-K.
175    tid2eid_dev: Option<DevicePtr>,
176    moe_expert_gate_up_shared_batch2_t_k: KernelHandle,
177    moe_expert_silu_down_shared_batch2_t_k: KernelHandle,
178    moe_expert_gate_up_shared_batch3_t_k: KernelHandle,
179    moe_expert_silu_down_shared_batch3_t_k: KernelHandle,
180    moe_expert_gate_up_shared_fp8_t_k: KernelHandle,
181    moe_expert_silu_down_shared_fp8_t_k: KernelHandle,
182    moe_expert_gate_up_shared_fp8_batch2_t_k: KernelHandle,
183    moe_expert_silu_down_shared_fp8_batch2_t_k: KernelHandle,
184    moe_expert_gate_up_shared_fp8_batch3_t_k: KernelHandle,
185    moe_expert_silu_down_shared_fp8_batch3_t_k: KernelHandle,
186    /// `ATLAS_UNIFIED_MOE_LAYOUT=1` opts in to the unified-layout decode
187    /// path: gate/up/down all use transposed `[K/2, N]` layout, decode
188    /// dispatches to `moe_expert_*_shared_t` kernels. Default off — the
189    /// dispatch falls through to the original `[N, K/2]` kernels.
190    /// Resolved once at construction.
191    unified_layout: bool,
192    /// `ATLAS_NVFP4_GATE_UP_M128=1` opts in to the M=128 fused gate+up
193    /// kernel (Block D #3, Avarok tile-shape rewrite). Halves block count
194    /// at large prefill — better SM amortization on GB10's 25-SM budget.
195    /// Currently only minimax-m2-229b ships the kernel; other models keep
196    /// `moe_fused_gate_up_t_k64_m128 == KernelHandle(0)` and dispatch
197    /// falls through to the M=64 path even when the env var is set.
198    nvfp4_gate_up_m128: bool,
199    /// `ATLAS_HOLO_MOE_GATEUP_FP4=1` opts the prefill fused gate_up onto the
200    /// block-scaled FP4 kernel. Reads the SHARED FAST_MOE=full `gate_ptrs_t`/
201    /// `up_ptrs_t` `[K/2,N]` tables (no extra MoE memory); dispatch also requires
202    /// those tables present + the FP4 kernel handle != 0.
203    gateup_fp4: bool,
204    /// `ATLAS_HOLO_MOE_DOWN_FP4=1` — same, for the prefill down projection over
205    /// the shared `down_ptrs_t` table.
206    down_fp4: bool,
207    /// `ATLAS_HYBRID_MOE_LAYOUT=1` opts in to the hybrid-layout path:
208    /// keep BOTH original `[N, K/2]` weights (for decode + MTP verify) AND
209    /// transposed `[K/2, N]` weights (for prefill). Doubles MoE-weight
210    /// memory but recovers the ~15 % decode regression that pure unified
211    /// layout suffers from. Resolved once at construction; mutually
212    /// exclusive with `unified_layout` at the dispatch level (hybrid wins
213    /// on decode paths since it preserves untransposed warp-reduction
214    /// parallelism).
215    hybrid_layout: bool,
216    /// Transposed shared expert weights for prefill.
217    shared_gate_t: Option<QuantizedWeight>,
218    shared_up_t: Option<QuantizedWeight>,
219    shared_down_t: Option<QuantizedWeight>,
220    moe_grouped_gemm_t: KernelHandle,
221    moe_grouped_gemm_t_k64: KernelHandle,
222    moe_fused_gate_up_t: KernelHandle,
223    moe_fused_gate_up_t_k64: KernelHandle,
224    // ARM-2 Phase-K: native-MXFP4 (E8M0 per-32) prefill variants of the W4A16
225    // routed-expert GEMMs. KernelHandle(0) on models that don't ship them
226    // (only the deepseek-v4-flash target compiles the `_e8m0` entries).
227    moe_grouped_gemm_e8m0: KernelHandle,
228    moe_grouped_gemm_t_e8m0: KernelHandle,
229    moe_grouped_gemm_t_k64_e8m0: KernelHandle,
230    moe_fused_gate_up_t_e8m0: KernelHandle,
231    moe_fused_gate_up_t_k64_e8m0: KernelHandle,
232    /// M=128 variant of the K64 fused gate+up kernel (Block D #3, Avarok
233    /// tile-shape rewrite). Loaded with `try_kernel` — falls back to
234    /// `KernelHandle(0)` on models that don't ship the kernel; dispatch
235    /// gates on `nvfp4_gate_up_m128` AND handle non-zero.
236    moe_fused_gate_up_t_k64_m128: KernelHandle,
237    /// FUSED FP4 (block-scaled e2m1) variant of the K64 fused gate+up kernel
238    /// (`ATLAS_HOLO_MOE_GATEUP_FP4`). Same signature as `moe_fused_gate_up_t_k64`
239    /// but runs one `mma.sync.kind::mxf4nvf4.scale_vec::4X.m16n8k64` per k64
240    /// tile (vs 2× m16n8k32 e4m3). `try_kernel` — `KernelHandle(0)` on images
241    /// lacking it; the dispatch in `forward_prefill_routed` only fires when this
242    /// handle != 0, `gateup_fp4` is set, and the shared `gate_ptrs_t`/`up_ptrs_t`
243    /// tables are present (FAST_MOE=full).
244    moe_fused_gate_up_t_k64_fp4: KernelHandle,
245    moe_fp8_grouped_gemm_t: KernelHandle,
246    w4a16_gemm_t: KernelHandle,
247    bf16_to_fp8_k: KernelHandle,
248    /// Pre-dequanted FP8 weights for zero-overhead prefill GEMMs.
249    gate_fp8: Option<DevicePtr>,
250    shared_gate_fp8: Option<DevicePtr>,
251    shared_up_fp8: Option<DevicePtr>,
252    shared_down_fp8: Option<DevicePtr>,
253    fp8_gemm_k: KernelHandle,
254    /// Secondary CUDA stream for overlapping shared expert with routed experts.
255    prefill_stream: u64,
256    /// Event pair for stream synchronization (input_ready, shared_done).
257    event_a: u64,
258    event_b: u64,
259    // ── Sigmoid + correction-bias routing (DeepSeek-V3 / MiniMax-M2 style) ──
260    /// Device pointer to `[num_experts]` correction bias. Populated from
261    /// `MoeWeights.correction_bias` in `new()` when the loader sets it.
262    /// `None` = Atlas's default softmax path. When `Some`, every top-k
263    /// dispatch site branches to `moe_topk_sigmoid` with this bias arg.
264    correction_bias_dev: Option<DevicePtr>,
265    /// Handle to `moe_topk_sigmoid` kernel. Lazy-loaded in `new()` even
266    /// when bias is `None` (harmless if kernel isn't used).
267    moe_topk_sigmoid_k: KernelHandle,
268    /// Batched variant for prefill / MTP-verify (one block per token).
269    /// Loaded via `try_kernel` — returns KernelHandle(0) on models whose
270    /// KERNEL.toml doesn't register the sigmoid kernels (e.g. Mistral).
271    /// Never dispatched on those paths because `correction_bias_dev` is
272    /// `None` there.
273    moe_topk_sigmoid_batched_k: KernelHandle,
274    // FP8 fused MoE kernels (used when experts are FP8)
275    moe_expert_gate_up_shared_fp8: KernelHandle,
276    moe_expert_silu_down_shared_fp8: KernelHandle,
277    // FP8 batch2/3 fused MoE kernels (for MTP K=2/K=3 verify)
278    moe_expert_gate_up_shared_fp8_batch2: KernelHandle,
279    moe_expert_silu_down_shared_fp8_batch2: KernelHandle,
280    moe_weighted_sum_blend_fp8_batch2: KernelHandle,
281    moe_expert_gate_up_shared_fp8_batch3: KernelHandle,
282    moe_expert_silu_down_shared_fp8_batch3: KernelHandle,
283    moe_weighted_sum_blend_fp8_batch3: KernelHandle,
284    // THE routed-expert FP8 grouped GEMM for sorted MoE prefill: grid-compaction
285    // (persistent 96-CTA grid over a COMPACTED (expert, m_tile, n_tile) work-list
286    // built by `moe_build_tile_worklist`). Handle may be 0 on images that don't
287    // ship the kernel.
288    moe_fp8_grouped_gemm_k: KernelHandle,
289    // Builds the grouped-GEMM work-list (moe_build_tile_worklist, module "moe").
290    // Launched on the SAME stream as the grouped GEMM (read-after-write of
291    // total_tiles). Handle may be 0 on older images.
292    moe_build_tile_worklist_k: KernelHandle,
293    // W8A8 + FP32 epilogue MoE GEMM (vLLM-equivalent). Opt-in via
294    // ATLAS_FP8_W8A8=1. Requires per-token-quanted A_fp8 + a_scale.
295    moe_w8a8_grouped_gemm_k: KernelHandle,
296    // PM4-geometry W8A8 grouped GEMM over the compacted work-list (kernel
297    // `moe_w8a8_grouped_gemm_pm4`, same module). Bit-identical numerics to
298    // the dense kernel; preferred when present (gb10). Handle may be 0 on
299    // targets/images that don't ship it — dispatch falls back to the dense
300    // 3D-grid `moe_w8a8_grouped_gemm_k`.
301    moe_w8a8_grouped_gemm_pm4_k: KernelHandle,
302    per_token_group_quant_fp8_k: KernelHandle,
303    /// Fused SiLU·mul + per-token-group FP8 quant (bit-identical replacement
304    /// for the `silu_mul` → `per_token_group_quant_fp8` pair on the W8A8
305    /// prefill down-path). Optional: handle 0 (e.g. a model shadowing
306    /// moe_silu_mul.cu without this entry point) falls back to the pair.
307    silu_mul_quant_fp8_k: KernelHandle,
308    // Dense W8A8 (same kernel used by attention QKV/O proj) for shared-expert path.
309    fp8_gemm_t_blockscaled_k: KernelHandle,
310    // BF16 grouped GEMM — for FP8-source models dequanted to BF16 at load.
311    // Activates the high-precision MoE path that closes the per-layer
312    // 0.989 FP8 cosine ceiling. Handle may be 0 on images that don't ship
313    // the kernel; dispatch site is gated on Some(bf16_*_weight_ptrs).
314    moe_bf16_grouped_gemm_k: KernelHandle,
315    // Fused BF16 decode kernels (mirror moe_expert_*_shared_fp8 layout).
316    moe_expert_gate_up_shared_bf16_k: KernelHandle,
317    moe_expert_silu_down_shared_bf16_k: KernelHandle,
318    // Fused BF16 K=2 batch kernels for MTP verify (mirror the FP8 batch2 layout).
319    // Handle may be 0 on images that don't ship the kernel; the K=2 BF16
320    // dispatch site is gated on this being non-null and falls back to the
321    // per-token batched path otherwise.
322    moe_expert_gate_up_shared_bf16_batch2_k: KernelHandle,
323    moe_expert_silu_down_shared_bf16_batch2_k: KernelHandle,
324    w8a16_gemm_k: KernelHandle,           // for shared expert FP8 prefill
325    w8a16_gemm_pipelined_k: KernelHandle, // ATLAS_W8A16_PIPELINED shared-expert variant
326    // Fused gate GEMV + topK softmax (saves 1 kernel launch per layer)
327    moe_gate_topk_fused_k: KernelHandle,
328    // FP8 expert pointer tables (None when experts are NVFP4)
329    fp8_gate_weight_ptrs: Option<Fp8ExpertPtrTable>,
330    fp8_up_weight_ptrs: Option<Fp8ExpertPtrTable>,
331    fp8_down_weight_ptrs: Option<Fp8ExpertPtrTable>,
332    // BF16 expert pointer tables — populated by the FP8-dequant-on-load
333    // path. When Some, the routed-expert dispatch in `forward_prefill_fp8`
334    // routes through `moe_bf16_grouped_gemm` instead of the FP8 grouped
335    // GEMM, eliminating the per-layer FP8 quantization ceiling.
336    bf16_gate_weight_ptrs: Option<DevicePtr>,
337    bf16_up_weight_ptrs: Option<DevicePtr>,
338    bf16_down_weight_ptrs: Option<DevicePtr>,
339    // Checkpoint-native BF16 shared expert. Independent of routed-expert
340    // precision so mixed NVFP4-routed/BF16-shared checkpoints stay faithful.
341    bf16_shared_expert: Option<Bf16SharedExpert>,
342    // FP8 shared expert weights (None when shared expert is NVFP4)
343    fp8_shared_expert: Option<Fp8ExpertWeight>,
344    /// FP4 down kernel handle (`moe_w4a16_down_t_k64_fp4`). `try_kernel` =>
345    /// `KernelHandle(0)` on images lacking it; the FP4-down dispatch checks this
346    /// handle != 0, `down_fp4` is set, and the shared `down_ptrs_t` table is present.
347    pub(crate) moe_down_t_k64_fp4: KernelHandle,
348    /// `moe_permute_tokens` gather kernel — only needed by the FP4 escape-hatch
349    /// (which consumes expert-sorted contiguous rows, unlike the FP8 fused
350    /// kernel that gathers via `sorted_token_ids` internally). `try_kernel`
351    /// (handle may be 0 on images lacking it). Now unused — the CUTLASS grouped
352    /// path fuses the gather into its A-pack — kept for potential reuse.
353    #[allow(dead_code)]
354    pub(crate) moe_permute_tokens_k: KernelHandle,
355    // Phase 2.7 Tier C — Frankenstein dispatch flag.
356    // True when this layer's index is in `config.dflash_capture_layers`.
357    // When the env var `ATLAS_FRANKENSTEIN_DECODE_VIA_PREFILL=1` is set,
358    // `forward()` (single-token decode) will route through `forward_prefill`
359    // (tensor-core grouped GEMM kernel) on this layer only, so the captured
360    // hidden states use a different numerical recipe than the scalar GEMV
361    // path. Used to test whether the kernel choice is the dominant cause
362    // of low DFlash drafter acceptance on FP4/FP8 targets.
363    pub is_dflash_capture_layer: bool,
364    /// Feature-1 (MoE expert + router LoRA): this layer's installed router +
365    /// routed-expert deltas + apply scratch. `None` = no adapter / feature off
366    /// → the base MoE path is byte-identical. Set by
367    /// [`MoeLayer::set_lora_weights`] (`moe/lora.rs`); applied in the prefill
368    /// forward. Decode/verify paths are a phase-1 followup (they REFUSE rather
369    /// than silently drop the delta — see `reject_decode_lora`).
370    pub(crate) lora: Option<MoeLoraWeights>,
371}
372
373impl MoeLayer {
374    /// ARM-2 Phase-K routed-expert kernel-handle select. Returns the E8M0
375    /// variant when the routed experts are native MXFP4 (`Mxfp4E8m0`), else the
376    /// NVFP4 handle. Panics if E8M0 is selected but the `_e8m0` kernel is
377    /// absent from this target (`try_kernel` gave 0) — that means a native
378    /// checkpoint reached a build that never compiled the variant, which must
379    /// be loud, not silent NVFP4-on-E8M0 garbage (the straggler net).
380    #[inline]
381    fn e8m0_or(
382        &self,
383        nvfp4: spark_runtime::gpu::KernelHandle,
384        e8m0: spark_runtime::gpu::KernelHandle,
385        site: &str,
386    ) -> spark_runtime::gpu::KernelHandle {
387        if self.experts_scale_kind == crate::weight_map::WeightQuantFormat::Mxfp4E8m0 {
388            assert!(
389                e8m0.0 != 0,
390                "ARM-2 Phase-K: routed experts tagged Mxfp4E8m0 at {site}, but the \
391                 _e8m0 kernel handle is unresolved (not compiled into this target)."
392            );
393            e8m0
394        } else {
395            nvfp4
396        }
397    }
398}
399
400mod tables;
401// Re-exported so every `moe::ExpertPtrTable`-style path in the sub-files keeps
402// resolving; the split is invisible to them.
403pub(crate) use tables::{Bf16SharedExpert, ExpertPtrTable, Fp8ExpertPtrTable};
404
405// ── Sub-files (split for ≤500 LoC) ────────────────────────────────────────
406mod dump;
407mod forward;
408mod lora;
409mod lora_gateup;
410mod lora_router;
411pub(crate) use lora::MoeLoraWeights;
412mod forward_atomic_c4;
413mod forward_batched;
414mod forward_batched_gate;
415mod forward_ep;
416mod forward_k2;
417mod forward_k3;
418mod forward_phase;
419mod forward_prefill;
420mod forward_prefill_bf16;
421mod forward_prefill_fp8;
422mod forward_prefill_phase;
423mod forward_prefill_routed;
424mod forward_prefill_router;
425mod forward_token_major;
426mod helpers_a;
427mod helpers_b;
428mod helpers_c;
429mod init;
430#[cfg(test)]
431mod mod_tests;
432mod ptr_table_build;
433mod union_stats;
434pub(crate) use ptr_table_build::*;