spark_model/layers/
dense_ffn.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Dense SwiGLU FFN component for non-MoE models.
4//!
5//! Forward: gate = gate_proj(x), up = up_proj(x), out = down_proj(SiLU(gate) * up)
6//! 2 fused kernel launches per decode token (dual GEMV + SiLU-fused down GEMV).
7
8use anyhow::Result;
9use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
10
11use crate::layer::ForwardContext;
12use crate::layers::ops;
13use crate::layers::w4a16_gemv_tiers::W4a16BatchmTiers;
14use crate::weight_map::{
15    DenseWeight, Fp8Weight, Fp8WeightTransposed, PackedQ2Weight, QuantizedWeight,
16};
17
18pub struct DenseFfnWeights {
19    pub gate_proj: QuantizedWeight,
20    pub up_proj: QuantizedWeight,
21    pub down_proj: QuantizedWeight,
22    /// Transposed ([K/2, N]) copies for the fast `w4a16_gemm_t_m128` prefill
23    /// kernel. `None` → prefill falls back to the slow M64xN64 base kernel.
24    /// The non-transposed copies above are kept for the decode gemv path.
25    pub gate_proj_t: Option<QuantizedWeight>,
26    pub up_proj_t: Option<QuantizedWeight>,
27    pub down_proj_t: Option<QuantizedWeight>,
28}
29
30/// BF16 dense MLP weights — alternative to NVFP4 for precision-sensitive
31/// models (Gemma-4-31B). Each is `[N, K]` row-major BF16. When installed
32/// on a `DenseFfnLayer` via `set_bf16_weights`, the forward paths
33/// dispatch to `dense_gemv_bf16` / `dense_gemm_bf16` instead of the
34/// w4a16 NVFP4 kernels. Costs ~3.4 GB extra GPU memory on Gemma-4-31B
35/// (3 × hidden×intermediate × 2 bytes) vs NVFP4's 0.5 bytes/weight.
36pub struct DenseFfnWeightsBf16 {
37    pub gate_proj: DenseWeight,
38    pub up_proj: DenseWeight,
39    pub down_proj: DenseWeight,
40}
41
42/// Native block-scaled FP8 dense MLP weights — loaded directly from an FP8
43/// checkpoint (no NVFP4 requant). When installed via `set_fp8_weights`, decode
44/// dispatches `w8a16_gemv` and prefill `w8a16_gemm` per projection (BF16 act ×
45/// FP8 E4M3 weight with 2D block scales), mirroring the SSM/attention FP8 path.
46pub struct DenseFfnWeightsFp8 {
47    pub gate_proj: Fp8Weight,
48    pub up_proj: Fp8Weight,
49    pub down_proj: Fp8Weight,
50}
51
52/// Native keep-packed ternary Q2_0 dense MLP weights — loaded directly from a
53/// PrismML Q2_0 GGUF (`ATLAS_GGUF_NATIVE_Q2=1`) with NO dequant / NVFP4 requant.
54/// Each projection is a raw `block_q2_0` buffer (2-bit codes + inline fp16 scale
55/// per group). When installed via `set_q2_weights`, decode dispatches
56/// `q2_0_gemv` (BF16 act × 2-bit weight, dequant-in-dot-product), mirroring the
57/// FP8 path but with the weights ~4× smaller resident.
58pub struct DenseFfnWeightsQ2 {
59    pub gate_proj: PackedQ2Weight,
60    pub up_proj: PackedQ2Weight,
61    pub down_proj: PackedQ2Weight,
62}
63
64/// Activation function for gated FFN (SiLU for Qwen/Llama, GELU for Gemma-4).
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum FfnActivation {
67    SiLU,
68    GeLU,
69}
70
71/// A per-projection int8 W4A8 weight, built lazily from the NVFP4 weight on the
72/// first `ATLAS_INT8_PREFILL` prefill (see `DenseFfnLayer::ensure_int8_weight`).
73/// `w_i8` is `[N, K]` signed int8; `w_scale` is `[N, K/32]` F32. Cached for the
74/// process lifetime in a `OnceLock`, so the requant kernel runs once per weight.
75#[derive(Debug, Clone, Copy)]
76struct Int8Weight {
77    w_i8: DevicePtr,
78    w_scale: DevicePtr,
79}
80
81/// Q4_K-quantized FFN weight (GGML block_q4_K layout), materialized once at first
82/// `ATLAS_FFN_MMQ` prefill and cached for process lifetime in a `OnceLock`.
83#[derive(Debug, Clone, Copy)]
84struct Q4kWeight {
85    w_q4k: DevicePtr,
86}
87
88/// block_nvfp4-repacked FFN weight for the `ATLAS_FFN_NVFP4_MMQ` W4A4 prefill arm.
89/// Raw bit shuffle of the checkpoint's NVFP4 (same e2m1 codes + e4m3 scale bytes,
90/// same total bytes) — materialized once and cached for process lifetime.
91#[derive(Debug, Clone, Copy)]
92struct Fp4MmqWeight {
93    w: DevicePtr,
94}
95
96pub struct DenseFfnLayer {
97    pub weights: DenseFfnWeights,
98    activation: FfnActivation,
99    w4a16_gemv: KernelHandle,
100    /// Single-warp `w4a16_gemv_sw`. `KernelHandle(0)` on miss → base GEMV.
101    w4a16_gemv_sw: KernelHandle,
102    w4a16_gemv_dual: KernelHandle,
103    w4a16_gemv_silu_input: KernelHandle,
104    // LOSSLESS single-warp-per-output decode variants (8 outputs/block, no smem
105    // cross-warp reduce). Bit-identical to the 64-thread kernels (proven by the
106    // w4a16_gemv_sw microtest). Default ON via `ModelLevers::gemv_sw`;
107    // `ATLAS_NO_GEMV_SW=1` restores the 64-thread kernels. KernelHandle(0) on
108    // miss → fall back to base kernels.
109    w4a16_gemv_dual_sw: KernelHandle,
110    w4a16_gemv_silu_input_sw: KernelHandle,
111    w4a16_gemv_dual_batch2: KernelHandle,
112    w4a16_gemv_dual_batch3: KernelHandle,
113    w4a16_gemv_batch2: KernelHandle,
114    w4a16_gemv_batch3: KernelHandle,
115    /// Narrow `w4a16_gemv_batch{M}` family (M=4..8) for the K=4 verify FFN and
116    /// the K=5..8 chain verify. SSOT for the M -> tier decision; individual
117    /// tiers are 0-handles when the target did not load them.
118    w4a16_batchm: W4a16BatchmTiers,
119    w4a16_gemm: KernelHandle,
120    // 128x128 2-stage cp.async pipelined w4a16 GEMM — the fast prefill kernel
121    // attention/SSM already use. The base `w4a16_gemm` (M64xN64) only hits
122    // ~10 TFLOPS at M=8k and was the flat ~155 tok/s dense-FFN prefill
123    // bottleneck on Qwen3.6-27B. KernelHandle(0) on miss → scalar-tile fallback.
124    w4a16_gemm_t_m128_k: KernelHandle,
125    // v2: 8-warp (256-thread) variant of t_m128 — parallel chunk MMAs, 3 CTAs/SM.
126    // Preferred over t_m128 for dense-FFN prefill when present. KernelHandle(0) → use t_m128.
127    w4a16_gemm_t_m128_v2_k: KernelHandle,
128    // LOSSLESS BF16 variant of t_m128: same 128x128 cp.async tiling, but FP4→BF16
129    // dequant + BF16 m16n8k16 MMA (FP32 accum) instead of the FP8-E4M3 crush the
130    // default NVIDIA t_m128 uses. The FP8 path perturbs generation (measured
131    // length-truncations / accuracy risk on Qwen3.6-27B); this kernel keeps prefill
132    // outputs bit-for-bit vs the base `w4a16_gemm`. OPT-IN only, gated by
133    // ATLAS_BF16_TC_PREFILL (default off → dispatch unchanged). KernelHandle(0) on miss.
134    w4a16_gemm_t_m128_bf16_k: KernelHandle,
135    // v2 of the LOSSLESS BF16 128x128 prefill kernel: same MMA instruction order
136    // (so BIT-IDENTICAL to bf16_k, proven by w4a16_bf16_v2_microtest) but a
137    // smaller A-tile smem pad lifts occupancy from 2→3 CTAs/SM (~+50% resident
138    // warps), giving a measured ~3-8% faster prefill GEMM on this latency-bound
139    // kernel. Preferred over bf16_k when present. KernelHandle(0) on miss → bf16_k.
140    w4a16_gemm_t_m128_bf16_v2_k: KernelHandle,
141    // FP8 M64 prefill (w4a16_gemm_t): m16n8k32 e4m3 MMA + M_TILE=64. Packed 1-byte
142    // operands cut shared-memory load instructions ~4x (the v2 BF16 path is
143    // smem-bandwidth-bound, L1/TEX 90% per ncu), and M64's lower register pressure
144    // lifts occupancy → measured ~44 TFLOP/s vs ~30 for v2 (~1.47x prefill) on dgx1.
145    // LOSSY (FP8 E4M3, cosine ~0.9997) — OPT-IN via ATLAS_FP8_M64_PREFILL, gated on
146    // quality. KernelHandle(0) on miss → dispatch unchanged.
147    w4a16_gemm_t_k: KernelHandle,
148    // int8 W4A8 prefill (ATLAS_INT8_PREFILL): the validated requant→faith2
149    // pipeline (cosine 0.999978). `int8_gemm_faith2` is an int8×int8 MMA with
150    // per-32 block scales, so BOTH operands must be int8 — unlike the FP8 path
151    // (mixed BF16×FP8). At first int8 prefill we requant the NVFP4 gate/up/down
152    // weights to int8 once (`requant_w_nvfp4_int8`, cached in the OnceLocks
153    // below) and requant the BF16 activations every call (`requant_a_bf16_int8`,
154    // into `int8_a_scratch`). KernelHandle(0) on miss → arm never taken.
155    int8_faith2_k: KernelHandle,
156    // faith5: int32 per-sb accumulation (breaks the MMA→scale dependency chain).
157    // Opt-in via ATLAS_INT8_FAITH5=1 (replaces faith2 for int8 prefill GEMMs).
158    int8_faith5_k: KernelHandle,
159    requant_w_int8_k: KernelHandle,
160    requant_a_int8_k: KernelHandle,
161    // Lazily-built, process-lifetime int8 weight copies (one per projection),
162    // requanted from `self.weights.{gate,up,down}_proj`. Only ever touched when
163    // ATLAS_INT8_PREFILL is set → default-off path is byte-identical.
164    int8_gate: std::sync::OnceLock<Int8Weight>,
165    int8_up: std::sync::OnceLock<Int8Weight>,
166    int8_down: std::sync::OnceLock<Int8Weight>,
167    // Activation-requant scratch for the int8/NVFP4/Q4_K prefill GEMMs is now
168    // shared, arena-owned (BufferArena::ffn_act_{q8,a,scale}), sized once for
169    // max_batch_tokens × max(h, inter) — no per-layer allocation.
170    // W4A4 native-FP4 prefill (ATLAS_FP4_PREFILL): NVFP4 weights consumed directly
171    // (no requant), BF16 activations quantized to NVFP4 each call into ffn_act_a/scale.
172    // KernelHandle(0) on miss → arm never taken (default-off byte-identical).
173    w4a4_gemm_k: KernelHandle,
174    quantize_nvfp4_k: KernelHandle,
175    // Q4_K MMQ prefill (ATLAS_FFN_MMQ): vendored llama Q4_K W4A8 GEMM. Weights
176    // materialized NVFP4→bf16→Q4_K once (lazy, cached in the OnceLocks); activations
177    // quantized to q8_1_mmq each call into ffn_act_q8. KernelHandle(0) → arm skipped.
178    q4k_mmq_nc_k: KernelHandle,
179    q4k_mmq_wc_k: KernelHandle,
180    q4k_quant_act_k: KernelHandle,
181    q4k_quant_w_k: KernelHandle,
182    dequant_nvfp4_bf16_k: KernelHandle,
183    q4k_gate: std::sync::OnceLock<Q4kWeight>,
184    q4k_up: std::sync::OnceLock<Q4kWeight>,
185    q4k_down: std::sync::OnceLock<Q4kWeight>,
186    // NVFP4 W4A4 MMQ prefill (ATLAS_FFN_NVFP4_MMQ): vendored llama Blackwell block-scale
187    // FP4 MMA (80 TFLOP/s vs t_m128 ~51 on GB10). Gate/up weights repacked ONCE at load
188    // (raw bit shuffle, checkpoint layout → block_nvfp4, zero requantization); activations
189    // quantized per call into the shared ffn_act_q8 scratch; the per-tensor scale2 is
190    // folded in the scaled SiLU-mul. KernelHandle(0) → arm skipped.
191    nvfp4_mmq_nc_k: KernelHandle,
192    nvfp4_mmq_wc_k: KernelHandle,
193    /// M-sized MMQ tiles for DECODE. The 128 tile issues MMAs for all 128 columns
194    /// regardless of m, so at m=16 it discards 112 of them; these size the tile to
195    /// the batch. try_kernel: 0-handle -> dispatch keeps the 128 tile.
196    nvfp4_mmq16_nc_k: KernelHandle,
197    nvfp4_mmq16_wc_k: KernelHandle,
198    nvfp4_mmq32_nc_k: KernelHandle,
199    nvfp4_mmq32_wc_k: KernelHandle,
200    nvfp4_mmq64_nc_k: KernelHandle,
201    nvfp4_mmq64_wc_k: KernelHandle,
202    nvfp4_quant_act_k: KernelHandle,
203    nvfp4_repack_k: KernelHandle,
204    nvfp4_silu_scaled_k: KernelHandle,
205    nvfp4_silu_quant_k: KernelHandle,
206    nvfp4_scale_k: KernelHandle,
207    fp4mmq_gate: std::sync::OnceLock<Fp4MmqWeight>,
208    fp4mmq_up: std::sync::OnceLock<Fp4MmqWeight>,
209    fp4mmq_down: std::sync::OnceLock<Fp4MmqWeight>,
210    // Small-M (DFlash verify M=17) routing companion to `w4a16_gemm_t_k`
211    // (declared above): deep-K variant. w4a16_m17_bench: `w4a16_gemm_t_k64`
212    // wins deep-K down_proj (554 vs 810us at K=17408); the M64-tile
213    // `w4a16_gemm_t` beats M128 tiles at M<=64 (283 vs 324us on gate/up).
214    // KernelHandle(0) → m128 dispatch.
215    w4a16_gemm_t_k64_k: KernelHandle,
216    /// SiLU(gate)*up or GELU(gate)*up depending on activation.
217    act_mul: KernelHandle,
218    /// BF16 dense MLP weights — when `Some`, all forward paths use the
219    /// `dense_gemv_bf16` / `dense_gemm_bf16` kernels instead of w4a16
220    /// NVFP4. Falls back to the NVFP4 weights when `None`. Set via
221    /// `set_bf16_weights`. Used by Gemma-4 dense to avoid the structural
222    /// NVFP4 attention drift on greedy code generation (the fib test's
223    /// broken-indentation pattern).
224    bf16_weights: Option<DenseFfnWeightsBf16>,
225    dense_gemv_bf16_k: KernelHandle,
226    dense_gemm_bf16_k: KernelHandle,
227    // Tensor-core BF16 GEMM (m16n8k16 MMA) for the dense-FFN PREFILL path.
228    // The scalar `dense_gemm_bf16` is ~10x too slow on long prefills (it was
229    // the flat ~155 tok/s prefill bottleneck on Qwen3.6-27B dense NVFP4).
230    // KernelHandle(0) on miss → forward_prefill falls back to the scalar path.
231    // Decode (gemv, M=1) is untouched, so TPOT is unaffected.
232    dense_gemm_tc_k: KernelHandle,
233    /// Native FP8 dense MLP weights — when `Some`, decode/prefill dispatch the
234    /// block-scaled FP8 kernels (`w8a16_gemv` / `w8a16_gemm`) instead of w4a16
235    /// NVFP4. Set via `set_fp8_weights` for native FP8 checkpoints (Qwythos /
236    /// Ornith-FP8). Spec-decode batched paths fall back to dequant — dense
237    /// qwen3_5 has no MTP, so they're never reached.
238    fp8_weights: Option<DenseFfnWeightsFp8>,
239    w8a16_gemv_k: KernelHandle,
240    w8a16_gemm_k: KernelHandle,
241    w8a16_gemv_batch4_k: KernelHandle,
242    w8a16_gemm_pipelined_k: KernelHandle,
243    // Fused FP8 decode GEMVs (gate+up in one launch / silu+down in one launch),
244    // mirroring the NVFP4 w4a16_gemv_dual / w4a16_gemv_silu_input. KernelHandle(0)
245    // on miss → fall back to the 3-launch w8a16_gemv path. Module = .cu file stem.
246    w8a16_gemv_dual_k: KernelHandle,
247    w8a16_gemv_silu_input_k: KernelHandle,
248    // Fast transposed FP8 prefill GEMM (128x128 / 8-warp / two-level FP32 fold).
249    // Preferred over w8a16_gemm when a transposed FP8 weight copy is present.
250    // KernelHandle(0) → fall back to non-transposed w8a16_gemm.
251    w8a16_gemm_t_m128_k: KernelHandle,
252    /// v0 LoRA overlay for gate/up/down. `set_lora_weights` REJECTS layers
253    /// where `fp8_weights`, `bf16_weights` or `q2_weights` are installed (v0
254    /// supports the NVFP4 dispatch path only — those branches early-return
255    /// before the NVFP4 tail where the deltas land; holo is NVFP4 so it is
256    /// unaffected).
257    ///
258    /// M1 (2026-08-19): the deltas are APPLIED. `apply_lora_gate_up` runs
259    /// after the gate/up projection and before `silu_mul`; `apply_lora_down`
260    /// runs after the down projection. Every NVFP4 dispatch this layer can
261    /// take — decode `forward`, `forward_k2`/`k3`/`km`, `forward_prefill`,
262    /// `forward_batched` — calls both, because an adapter that applies on one
263    /// path and not another produces a model that contradicts itself between
264    /// prefill and decode.
265    ///
266    /// Until M1 this field was written by `set_lora_weights` and never read,
267    /// so an adapter targeting gate/up/down loaded successfully and changed
268    /// nothing. On a hybrid like Qwen3.8-27B that is most of the adapter:
269    /// community LoRAs for it put 67-78% of their parameter mass in the FFN.
270    lora: Option<ops::lora_delta::LoraFfnWeights>,
271
272    /// Native keep-packed ternary Q2_0 dense MLP weights (`ATLAS_GGUF_NATIVE_Q2`).
273    /// When installed via `set_q2_weights`, decode dispatches `q2_0_gemv_vec`
274    /// (BF16 activation × packed 2-bit weight, dequant-in-dot-product) — the
275    /// weights stay 2-bit resident (no NVFP4 requant). Highest-priority forward
276    /// branch. Prefill/batched paths for packed-Q2 are a deferred (Tier-2) phase
277    /// and currently bail — dense qwen35 has no MTP so k2/k3 are never reached.
278    q2_weights: Option<DenseFfnWeightsQ2>,
279    q2_0_gemv_k: KernelHandle,
280    // Batched (M=1..8) packed-Q2 decode GEMV handle. The kernel + wrapper are
281    // built and validated (CPU math test), but the batched-decode call site
282    // (spec-decode verify rows) is deferred to the same Tier-2 phase as prefill;
283    // dense qwen35 has no MTP so no batched decode reaches the FFN today.
284    #[allow(dead_code)]
285    q2_0_gemv_batchm_k: KernelHandle,
286    // Load-time packed-Q2 → BF16 dequant kernel (`dequant_gguf_bf16` module).
287    // Used by packed-Q2 PREFILL: dequant each proj into a TRANSIENT BF16 scratch
288    // buffer, run the normal BF16 GEMM, free the scratch — the resident weight
289    // stays 2-bit. Decode uses the native `q2_0_gemv` (no dequant). Tier-1 path.
290    dequant_q2_0_gn_k: KernelHandle,
291    // Native Q2_0 MMQ prefill (Tier-2, `ATLAS_GGUF_NATIVE_Q2_MMQ=1`): keeps the
292    // 2-bit weight packed and runs a tensor-core int8 MMA (dequant-in-register)
293    // against a q8_1 activation — no BF16 weight scratch, no dequant tax, no race.
294    // The q8_1 activation quantizer is SHARED with Q4_K (`q4k_quant_act_k`).
295    // KernelHandle(0) when absent → falls back to the transient-dequant path.
296    q2_0_mmq_nc_k: KernelHandle,
297    q2_0_mmq_wc_k: KernelHandle,
298}
299
300/// M-sized MMQ tiles: **ON by default**, disabled by `ATLAS_NO_MMQ_SMALL_TILE=1`.
301/// Strict `== "1"` on an `ATLAS_NO_*` name — presence flags here are enabled by `=0`.
302fn mmq_small_tile_enabled() -> bool {
303    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
304    *ON.get_or_init(|| std::env::var("ATLAS_NO_MMQ_SMALL_TILE").as_deref() != Ok("1"))
305}
306
307/// The m=64 MMQ tile: **ON by default**, disabled by `ATLAS_NO_MMQ_TILE64=1`. Separate
308/// from `ATLAS_NO_MMQ_SMALL_TILE` so this arm can be A/B'd without also reverting the
309/// already-shipped 16/32 tiles. Strict `== "1"`, matching the sibling above.
310fn mmq_tile64_enabled() -> bool {
311    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
312    *ON.get_or_init(|| std::env::var("ATLAS_NO_MMQ_TILE64").as_deref() != Ok("1"))
313}
314impl DenseFfnLayer {
315    pub fn new(weights: DenseFfnWeights, gpu: &dyn GpuBackend) -> Result<Self> {
316        Self::new_with_activation(weights, FfnActivation::SiLU, gpu)
317    }
318
319    pub fn new_with_activation(
320        weights: DenseFfnWeights,
321        activation: FfnActivation,
322        gpu: &dyn GpuBackend,
323    ) -> Result<Self> {
324        let act_mul = match activation {
325            FfnActivation::SiLU => gpu.kernel("moe_silu_mul", "moe_silu_mul")?,
326            FfnActivation::GeLU => gpu.kernel("gelu", "gelu_mul")?,
327        };
328        // BF16 path kernels — optional (only loaded if available; gemma4
329        // is the only consumer today). `try_kernel` returns
330        // `KernelHandle(0)` on miss so we don't break NVFP4-only models
331        // that were built without these kernels. Module names per
332        // `kernels/gb10/{target}/nvfp4/KERNEL.toml`:
333        //   `dense_gemv_bf16 = "gemv"`, `dense_gemm_bf16 = "gemm"`.
334        let dense_gemv_bf16_k = super::try_kernel(gpu, "gemv", "dense_gemv_bf16");
335        let dense_gemm_bf16_k = super::try_kernel(gpu, "gemm", "dense_gemm_bf16");
336        let dense_gemm_tc_k = super::try_kernel(gpu, "gemm_tc", "dense_gemm_tc");
337
338        let layer = Self {
339            weights,
340            activation,
341            w4a16_gemv: gpu.kernel("w4a16_gemv", "w4a16_gemv")?,
342            w4a16_gemv_sw: super::try_kernel(gpu, "w4a16_gemv", "w4a16_gemv_sw"),
343            w4a16_gemv_dual: gpu.kernel("w4a16_gemv_fused", "w4a16_gemv_dual")?,
344            w4a16_gemv_silu_input: gpu.kernel("w4a16_gemv_fused", "w4a16_gemv_silu_input")?,
345            w4a16_gemv_dual_sw: super::try_kernel(gpu, "w4a16_gemv_fused", "w4a16_gemv_dual_sw"),
346            w4a16_gemv_silu_input_sw: super::try_kernel(
347                gpu,
348                "w4a16_gemv_fused",
349                "w4a16_gemv_silu_input_sw",
350            ),
351            w4a16_gemv_dual_batch2: gpu.kernel("w4a16_gemv", "w4a16_gemv_dual_batch2")?,
352            w4a16_gemv_dual_batch3: gpu.kernel("w4a16_gemv", "w4a16_gemv_dual_batch3")?,
353            w4a16_gemv_batch2: gpu.kernel("w4a16_gemv", "w4a16_gemv_batch2")?,
354            w4a16_gemv_batch3: gpu.kernel("w4a16_gemv", "w4a16_gemv_batch3")?,
355            w4a16_batchm: W4a16BatchmTiers::resolve(gpu),
356            w4a16_gemm: gpu.kernel("w4a16", "w4a16_gemm")?,
357            w4a16_gemm_t_m128_k: super::try_kernel(gpu, "w4a16", "w4a16_gemm_t_m128"),
358            w4a16_gemm_t_m128_v2_k: super::w4a16_v2_kernel(gpu),
359            w4a16_gemm_t_m128_bf16_k: super::try_kernel(gpu, "w4a16", "w4a16_gemm_t_m128_bf16"),
360            w4a16_gemm_t_m128_bf16_v2_k: super::try_kernel(
361                gpu,
362                "w4a16",
363                "w4a16_gemm_t_m128_bf16_v2",
364            ),
365            w4a16_gemm_t_k: super::tgemm_kernel(gpu),
366            int8_faith2_k: super::try_kernel(gpu, "w4a16", "int8_gemm_faith2"),
367            int8_faith5_k: super::try_kernel(gpu, "w4a16", "int8_gemm_i32acc"),
368            requant_w_int8_k: super::try_kernel(gpu, "w4a16", "requant_w_nvfp4_int8"),
369            requant_a_int8_k: super::try_kernel(gpu, "w4a16", "requant_a_bf16_int8"),
370            int8_gate: std::sync::OnceLock::new(),
371            int8_up: std::sync::OnceLock::new(),
372            int8_down: std::sync::OnceLock::new(),
373            w4a4_gemm_k: super::try_kernel(gpu, "w4a4", "w4a4_gemm"),
374            quantize_nvfp4_k: super::try_kernel(gpu, "quantize_nvfp4", "quantize_bf16_to_nvfp4"),
375            q4k_mmq_nc_k: super::try_kernel(gpu, "q4k_mmq", "atlas_q4k_mmq128_nc"),
376            q4k_mmq_wc_k: super::try_kernel(gpu, "q4k_mmq", "atlas_q4k_mmq128_wc"),
377            q4k_quant_act_k: super::try_kernel(gpu, "q4k_mmq", "atlas_q8_1_quantize_ds4_bf16"),
378            q4k_quant_w_k: super::try_kernel(gpu, "q4k_quantize", "q4k_quantize"),
379            dequant_nvfp4_bf16_k: super::try_kernel(
380                gpu,
381                "dequant_nvfp4_bf16",
382                "dequant_nvfp4_to_bf16",
383            ),
384            q4k_gate: std::sync::OnceLock::new(),
385            q4k_up: std::sync::OnceLock::new(),
386            q4k_down: std::sync::OnceLock::new(),
387            nvfp4_mmq_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq128_nc"),
388            nvfp4_mmq_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq128_wc"),
389            nvfp4_mmq16_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq16_nc"),
390            nvfp4_mmq16_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq16_wc"),
391            nvfp4_mmq32_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq32_nc"),
392            nvfp4_mmq32_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq32_wc"),
393            nvfp4_mmq64_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq64_nc"),
394            nvfp4_mmq64_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq64_wc"),
395            nvfp4_quant_act_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_quantize_bf16"),
396            nvfp4_repack_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_repack"),
397            nvfp4_silu_scaled_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_silu_mul_scaled"),
398            nvfp4_silu_quant_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_silu_mul_quant"),
399            nvfp4_scale_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_scale_bf16"),
400            fp4mmq_gate: std::sync::OnceLock::new(),
401            fp4mmq_up: std::sync::OnceLock::new(),
402            fp4mmq_down: std::sync::OnceLock::new(),
403            w4a16_gemm_t_k64_k: super::k64_kernel(gpu).unwrap_or(KernelHandle(0)),
404            act_mul,
405            bf16_weights: None,
406            dense_gemv_bf16_k,
407            dense_gemm_bf16_k,
408            dense_gemm_tc_k,
409            fp8_weights: None,
410            w8a16_gemv_k: super::try_kernel(gpu, "w8a16_gemv", "w8a16_gemv"),
411            w8a16_gemm_k: super::try_kernel(gpu, "w8a16_gemm", "w8a16_gemm"),
412            w8a16_gemv_batch4_k: super::try_kernel(gpu, "w8a16_gemv_batch4", "w8a16_gemv_batch4"),
413            w8a16_gemm_pipelined_k: super::try_kernel(
414                gpu,
415                "w8a16_gemm_pipelined",
416                "w8a16_gemm_pipelined",
417            ),
418            w8a16_gemv_dual_k: super::try_kernel(gpu, "w8a16_gemv_fused", "w8a16_gemv_dual"),
419            w8a16_gemv_silu_input_k: super::try_kernel(
420                gpu,
421                "w8a16_gemv_fused",
422                "w8a16_gemv_silu_input",
423            ),
424            w8a16_gemm_t_m128_k: super::try_kernel(gpu, "w8a16_gemm_t_m128", "w8a16_gemm_t_m128"),
425            lora: None,
426            q2_weights: None,
427            // Winner of the decode-GEMV bench: candidate B (vectorized code loads
428            // + smem A-stage, 1 warp/row × 8 rows/CTA). ~268 GB/s (98% of the
429            // 273 GB/s LPDDR5X peak) at gate/up M=1 — 9.5× the original
430            // whole-block-strided `q2_0_gemv`. Same `(code-1)*d` FP32 numerics.
431            q2_0_gemv_k: super::try_kernel(gpu, "q2_0_gemv_vec", "q2_0_gemv_vec"),
432            q2_0_gemv_batchm_k: super::try_kernel(gpu, "q2_0_gemv_vec", "q2_0_gemv_vec_batchm"),
433            dequant_q2_0_gn_k: super::try_kernel(
434                gpu,
435                "dequant_gguf_bf16",
436                "dequant_q2_0_gn_to_bf16",
437            ),
438            // Resolved by `set_q2_weights`, never here: q2_0_mmq ships only
439            // in GGUF-serving targets, and an unconditional probe fails the
440            // boot audit on every dense-FFN model that never installs
441            // packed-Q2 weights.
442            q2_0_mmq_nc_k: KernelHandle(0),
443            q2_0_mmq_wc_k: KernelHandle(0),
444        };
445        Ok(layer)
446    }
447
448    /// Load-time finalize for the Q4_K MMQ prefill path (`ATLAS_FFN_MMQ`). MUST run at
449    /// load, BEFORE the KV cache is sized, so the net FFN footprint is correct when the KV
450    /// cache claims free memory. Order is critical: (1) eagerly materialize the Q4_K weights
451    /// (+9.63 GB) so they are accounted for now rather than lazily on first prefill (which
452    /// would over-subscribe AFTER the KV cache already grabbed the freed `_t` space → decode
453    /// OOM-throttle); (2) free the transposed `_proj_t` copies (−9.63 GB, dead under Q4_K
454    /// prefill — only the unreachable `Some(wt)` arms read them). Net FFN = baseline; decode
455    /// untouched (NVFP4 gemv on the non-`_t` copies). No-op unless Q4_K is active.
456    pub fn finalize_q4k_load(
457        &mut self,
458        gpu: &dyn GpuBackend,
459        h: u32,
460        inter: u32,
461        stream: u64,
462    ) -> Result<()> {
463        // Packed-Q2 (ATLAS_GGUF_NATIVE_Q2) FFN keeps its NVFP4 source weights
464        // NULL — the Q4_K prefill copy is built by dequant-ing those (NULL) NVFP4
465        // blocks, so running it here is a null-ptr kernel launch (CUDA 700).
466        // Packed-Q2 has its own prefill path (transient dequant), so skip.
467        if self.q2_weights.is_some() {
468            return Ok(());
469        }
470        let q4k_active = self.q4k_mmq_nc_k.0 != 0
471            && self.q4k_quant_act_k.0 != 0
472            && self.q4k_quant_w_k.0 != 0
473            && self.dequant_nvfp4_bf16_k.0 != 0
474            && std::env::var_os("ATLAS_FFN_MMQ").is_some();
475        if !q4k_active {
476            return Ok(());
477        }
478        // (1) eagerly materialize the prefill weights BEFORE freeing `_t`, so the KV cache
479        // (sized after load) can't claim the freed space before the weights exist.
480        // gate/up: Q4_K (N=inter,K=h). down: HYBRID → int8 faith2 (N=h,K=inter) for accuracy,
481        // else Q4_K. ensure_int8_weight reads the non-`_t` NVFP4 down_proj (kept for decode gemv).
482        self.ensure_q4k_weight(
483            &self.q4k_gate,
484            gpu,
485            &self.weights.gate_proj,
486            inter,
487            h,
488            stream,
489        )?;
490        self.ensure_q4k_weight(&self.q4k_up, gpu, &self.weights.up_proj, inter, h, stream)?;
491        let down_faith2 = self.int8_faith2_k.0 != 0
492            && self.requant_a_int8_k.0 != 0
493            && std::env::var_os("ATLAS_FFN_MMQ_DOWN_Q4K").is_none();
494        if down_faith2 {
495            self.ensure_int8_weight(
496                &self.int8_down,
497                gpu,
498                &self.weights.down_proj,
499                h,
500                inter,
501                stream,
502            )?;
503        } else {
504            self.ensure_q4k_weight(
505                &self.q4k_down,
506                gpu,
507                &self.weights.down_proj,
508                h,
509                inter,
510                stream,
511            )?;
512        }
513        gpu.synchronize(stream)?;
514        // (2) free the dead transposed copies
515        let mut freed = 0usize;
516        for wt in [
517            &mut self.weights.gate_proj_t,
518            &mut self.weights.up_proj_t,
519            &mut self.weights.down_proj_t,
520        ] {
521            if let Some(w) = wt.as_ref()
522                && !w.weight.is_null()
523            {
524                gpu.free(w.weight)?;
525                gpu.free(w.weight_scale)?;
526                freed += 1;
527            }
528            *wt = None;
529        }
530        if freed > 0 {
531            // Log-once latch (see `atlas_core::scope`). It holds no model-derived
532            // value — the message is rebuilt from the arguments every call — so a
533            // stale entry cannot produce a wrong answer, only a suppressed duplicate
534            // line after a model swap. Scoping it would thread a logging concern
535            // through the call path to prevent one repeated INFO line.
536            // Latched on the BACKEND (`OpCache::once`), which exists by load
537            // time: `finalize_q4k_load` takes the `gpu` it is loading onto. A
538            // static meant only the first model in the process reported the
539            // decision.
540            if gpu.op_cache().once("log:ffn_mmq_freed_twins") {
541                tracing::info!(
542                    "[atlas] ATLAS_FFN_MMQ: freed transposed FFN `_t` copies (dead under Q4_K prefill) — Q4_K weights net to ~0 vs NVFP4 baseline"
543                );
544            }
545        }
546        Ok(())
547    }
548
549    /// Eagerly materialize the block_nvfp4 gate/up copies for the `ATLAS_FFN_NVFP4_MMQ`
550    /// W4A4 prefill arm at LOAD time (before KV sizing), then free the now-dead gate/up
551    /// transposed `_t` copies so net FFN footprint stays at the NVFP4 baseline. Down is
552    /// untouched (hybrid: it stays on the default t_m128 path for accuracy → keeps its
553    /// `_t` copy). No-op unless the env + kernels are present.
554    pub fn finalize_nvfp4_mmq_load(
555        &mut self,
556        gpu: &dyn GpuBackend,
557        h: u32,
558        inter: u32,
559        stream: u64,
560    ) -> Result<()> {
561        // Packed-Q2 (ATLAS_GGUF_NATIVE_Q2) FFN keeps its NVFP4 source weights
562        // NULL. This W4A4-MMQ finalize is active by DEFAULT (SiLU + kernels
563        // present) and repacks the NVFP4 gate/up — over NULL pointers that's a
564        // CUDA-700 illegal access. Packed-Q2 uses its own decode/prefill path.
565        if self.q2_weights.is_some() {
566            return Ok(());
567        }
568        let active = self.nvfp4_mmq_nc_k.0 != 0
569            && self.nvfp4_quant_act_k.0 != 0
570            && self.nvfp4_repack_k.0 != 0
571            && self.nvfp4_silu_scaled_k.0 != 0
572            && matches!(self.activation, FfnActivation::SiLU)
573            && std::env::var_os("ATLAS_NO_FFN_NVFP4_MMQ").is_none();
574        if !active {
575            return Ok(());
576        }
577        self.ensure_nvfp4_mmq_weight(
578            &self.fp4mmq_gate,
579            gpu,
580            &self.weights.gate_proj,
581            inter,
582            h,
583            stream,
584        )?;
585        self.ensure_nvfp4_mmq_weight(
586            &self.fp4mmq_up,
587            gpu,
588            &self.weights.up_proj,
589            inter,
590            h,
591            stream,
592        )?;
593        let down_mmq = std::env::var_os("ATLAS_NO_FFN_NVFP4_MMQ_DOWN").is_none();
594        if down_mmq {
595            self.ensure_nvfp4_mmq_weight(
596                &self.fp4mmq_down,
597                gpu,
598                &self.weights.down_proj,
599                h,
600                inter,
601                stream,
602            )?;
603        }
604        gpu.synchronize(stream)?;
605        // Free the dead transposed copies (prefill for those projections now runs on the
606        // MMQ arm; decode reads the non-transposed originals). down_proj_t is freed only
607        // when the down A/B gate is on.
608        let mut down_t = if down_mmq {
609            Some(&mut self.weights.down_proj_t)
610        } else {
611            None
612        };
613        let mut freed = 0usize;
614        for wt in [&mut self.weights.gate_proj_t, &mut self.weights.up_proj_t]
615            .into_iter()
616            .chain(down_t.take())
617        {
618            if let Some(w) = wt.as_ref()
619                && !w.weight.is_null()
620            {
621                gpu.free(w.weight)?;
622                gpu.free(w.weight_scale)?;
623                freed += 1;
624            }
625            *wt = None;
626        }
627        if freed > 0 {
628            // Log-once latch (see `atlas_core::scope`). It holds no model-derived
629            // value — the message is rebuilt from the arguments every call — so a
630            // stale entry cannot produce a wrong answer, only a suppressed duplicate
631            // line after a model swap. Scoping it would thread a logging concern
632            // through the call path to prevent one repeated INFO line.
633            // Latched on the BACKEND (`OpCache::once`), which exists by load
634            // time: `finalize_q4k_load` takes the `gpu` it is loading onto. A
635            // static meant only the first model in the process reported the
636            // decision.
637            if gpu.op_cache().once("log:ffn_fp4mmq_freed_twins") {
638                tracing::info!(
639                    "[atlas] ATLAS_FFN_NVFP4_MMQ: freed gate/up `_t` copies (dead under FP4-MMQ prefill) — block_nvfp4 copies net to ~0 vs NVFP4 baseline"
640                );
641            }
642        }
643        Ok(())
644    }
645
646    /// Ensure the block_nvfp4 copy of one NVFP4 projection exists (raw repack of the
647    /// checkpoint's packed E2M1 `[N, K/2]` + E4M3 `[N, K/16]` scales — zero numerics;
648    /// scale2 folded at the SiLU-mul). Cached in `cell` for process lifetime.
649    fn ensure_nvfp4_mmq_weight(
650        &self,
651        cell: &std::sync::OnceLock<Fp4MmqWeight>,
652        gpu: &dyn GpuBackend,
653        src: &QuantizedWeight,
654        n: u32,
655        k: u32,
656        stream: u64,
657    ) -> Result<Fp4MmqWeight> {
658        if let Some(w) = cell.get() {
659            return Ok(*w);
660        }
661        let w = gpu.alloc(ops::nvfp4_mmq_weight_bytes(n, k))?;
662        ops::nvfp4_mmq_repack(
663            gpu,
664            self.nvfp4_repack_k,
665            src.weight,
666            src.weight_scale,
667            w,
668            n,
669            k,
670            stream,
671        )?;
672        let built = Fp4MmqWeight { w };
673        if let Err(dup) = cell.set(built) {
674            gpu.synchronize(stream)?;
675            let _ = gpu.free(dup.w);
676        }
677        Ok(*cell.get().expect("fp4mmq weight cell set above"))
678    }
679
680    /// Install native block-scaled FP8 dense MLP weights. After this call the
681    /// forward paths dispatch `w8a16_gemv` (decode) / `w8a16_gemm` (prefill)
682    /// instead of w4a16 NVFP4. Caller must ensure those kernels are present in
683    /// the target (they are for the qwen3_5/ornith nvfp4 bundle).
684    pub fn set_fp8_weights(&mut self, gate: Fp8Weight, up: Fp8Weight, down: Fp8Weight) {
685        self.fp8_weights = Some(DenseFfnWeightsFp8 {
686            gate_proj: gate,
687            up_proj: up,
688            down_proj: down,
689        });
690    }
691
692    /// Install the startup-static LoRA FFN overlay (gate/up/down deltas).
693    /// Hard-rejects when FP8/BF16 weight overlays are installed — those
694    /// decode branches early-return before the NVFP4 tail where the M1
695    /// delta insertions land, so a permissive install would silently skip
696    /// deltas. holo is NVFP4, so it is unaffected.
697    pub fn set_lora_weights(&mut self, w: ops::lora_delta::LoraFfnWeights) -> Result<()> {
698        anyhow::ensure!(
699            self.fp8_weights.is_none() && self.bf16_weights.is_none(),
700            "LoRA v0 supports only the NVFP4 dense-FFN path (FP8/BF16 weight \
701             overlays installed on this layer)"
702        );
703        // Packed-Q2 has its own gemv/batchm branches that early-return before
704        // the NVFP4 tail where the deltas land, exactly like FP8/BF16. Refusing
705        // here keeps the invariant the M1 apply relies on: if `self.lora` is
706        // Some, EVERY dispatch this layer can take applies it. A silently
707        // skipping path is worse than a refused load — it makes the adapter
708        // active in prefill and absent in decode, which reads as model
709        // weirdness rather than as a missing feature.
710        anyhow::ensure!(
711            self.q2_weights.is_none(),
712            "LoRA v0 supports only the NVFP4 dense-FFN path (packed-Q2 weights \
713             installed on this layer)"
714        );
715        // The decode down delta contracts over silu(gate)*up, which only the
716        // split-SiLU path materialises; `forward` pins that path whenever an
717        // adapter is installed. Refuse here if the layer cannot take it, so
718        // the pin is a guarantee rather than a hope.
719        anyhow::ensure!(
720            self.activation == FfnActivation::SiLU && self.act_mul.0 != 0 && self.w4a16_gemv.0 != 0,
721            "LoRA v0 needs the split-SiLU decode path (SiLU activation + \
722             act_mul + w4a16_gemv kernels); this layer resolved activation \
723             {:?}, act_mul={}, w4a16_gemv={}",
724            self.activation,
725            self.act_mul.0,
726            self.w4a16_gemv.0,
727        );
728        self.lora = Some(w);
729        Ok(())
730    }
731
732    /// M1 gate/up delta: `gate_out += ΔW_gate · x`, `up_out += ΔW_up · x`.
733    ///
734    /// Call AFTER the gate/up projection and BEFORE `silu_mul` — the deltas
735    /// belong to the projections, so they must land while gate/up are still
736    /// separate. Both buffers are the arena's dedicated `expert_gate_out` /
737    /// `expert_up_out` regions, contiguous with row stride `inter*2`, which is
738    /// what `apply_lora_delta`'s contiguity contract requires.
739    ///
740    /// No-op (and no launches) when the layer carries no adapter, so the
741    /// non-LoRA path stays byte-identical.
742    fn apply_lora_gate_up(
743        &self,
744        ctx: &ForwardContext,
745        input: DevicePtr,
746        gate_out: DevicePtr,
747        up_out: DevicePtr,
748        m: u32,
749        stream: u64,
750    ) -> Result<()> {
751        if ops::lora_delta::lora_no_ffn() {
752            return Ok(());
753        }
754        let Some(ref lw) = self.lora else {
755            return Ok(());
756        };
757        for (pair, base) in [(&lw.gate, gate_out), (&lw.up, up_out)] {
758            if let Some(pair) = pair.as_ref() {
759                ops::lora_delta::apply_lora_delta(
760                    ctx.gpu,
761                    &lw.kernels,
762                    pair,
763                    input,
764                    base,
765                    m,
766                    ctx.buffers.lora_xa(),
767                    ctx.buffers.lora_delta(),
768                    stream,
769                )?;
770            }
771        }
772        Ok(())
773    }
774
775    /// M1 down delta: `output += ΔW_down · act`.
776    ///
777    /// Call AFTER the down projection. `act` is the SiLU(gate)*up activation —
778    /// the same tensor the base down projection contracted over, NOT the
779    /// layer input. Every dense path leaves it in the `expert_gate_out`
780    /// region (silu_mul writes in place over gate), contiguous at row stride
781    /// `inter*2`.
782    fn apply_lora_down(
783        &self,
784        ctx: &ForwardContext,
785        act: DevicePtr,
786        output: DevicePtr,
787        m: u32,
788        stream: u64,
789    ) -> Result<()> {
790        if ops::lora_delta::lora_no_ffn() {
791            return Ok(());
792        }
793        let Some(ref lw) = self.lora else {
794            return Ok(());
795        };
796        let Some(ref pair) = lw.down else {
797            return Ok(());
798        };
799        ops::lora_delta::apply_lora_delta(
800            ctx.gpu,
801            &lw.kernels,
802            pair,
803            act,
804            output,
805            m,
806            ctx.buffers.lora_xa(),
807            ctx.buffers.lora_delta(),
808            stream,
809        )
810    }
811
812    /// Install native keep-packed ternary Q2_0 dense MLP weights. After this
813    /// call, decode `forward` dispatches `q2_0_gemv` per projection (weights
814    /// stay 2-bit resident, no NVFP4 requant) as the highest-priority path.
815    /// Caller must ensure the `q2_0_gemv` kernel is present in the target
816    /// (checked at forward time; falls through to a clear error otherwise).
817    /// Prefill for packed-Q2 is a deferred phase — see `forward_prefill`.
818    pub fn set_q2_weights(
819        &mut self,
820        gate: PackedQ2Weight,
821        up: PackedQ2Weight,
822        down: PackedQ2Weight,
823        gpu: &dyn GpuBackend,
824    ) {
825        self.q2_weights = Some(DenseFfnWeightsQ2 {
826            gate_proj: gate,
827            up_proj: up,
828            down_proj: down,
829        });
830        // Resolved here, not in the constructor: these ship only in
831        // GGUF-serving targets and the boot audit fails closed on an
832        // unconditional probe everywhere else.
833        self.q2_0_mmq_nc_k = super::try_kernel(gpu, "q2_0_mmq", "atlas_q2_0_mmq128_nc");
834        self.q2_0_mmq_wc_k = super::try_kernel(gpu, "q2_0_mmq", "atlas_q2_0_mmq128_wc");
835    }
836
837    /// Install BF16 dense MLP weights. After this call, the forward paths
838    /// dispatch to the BF16 GEMV/GEMM kernels instead of w4a16. The
839    /// caller must ensure the BF16 kernels are loaded (see
840    /// `dense_gemv_bf16_k` / `dense_gemm_bf16_k` checks). Small-batch
841    /// paths reuse `forward_prefill` so they cannot enter NVFP4 kernels
842    /// with the null placeholder weights used by BF16-native layers.
843    pub fn set_bf16_weights(&mut self, gate: DenseWeight, up: DenseWeight, down: DenseWeight) {
844        self.bf16_weights = Some(DenseFfnWeightsBf16 {
845            gate_proj: gate,
846            up_proj: up,
847            down_proj: down,
848        });
849    }
850
851    /// Ensure the int8 W4A8 copy of one NVFP4 projection weight exists, building
852    /// it once via `requant_w_nvfp4_int8` and caching it in `cell`. Reads the
853    /// NON-transposed NVFP4 layout (`weight` = packed E2M1 `[N, K/2]`,
854    /// `weight_scale` = per-16 E4M3 `[N, K/16]`, `weight_scale_2` = per-tensor
855    /// F32) — so it is independent of the `*_proj_t` transposed copies. The
856    /// requant launches on `stream`; the subsequent faith2 read is stream-ordered
857    /// after it, so no host sync is needed.
858    fn ensure_int8_weight(
859        &self,
860        cell: &std::sync::OnceLock<Int8Weight>,
861        gpu: &dyn GpuBackend,
862        src: &QuantizedWeight,
863        n: u32,
864        k: u32,
865        stream: u64,
866    ) -> Result<Int8Weight> {
867        if let Some(w) = cell.get() {
868            return Ok(*w);
869        }
870        let (nn, kk) = (n as usize, k as usize);
871        let w_i8 = gpu.alloc(nn * kk)?; // [N, K] int8
872        let w_scale = gpu.alloc(nn * (kk / 32) * 4)?; // [N, K/32] F32
873        ops::requant_w_nvfp4_int8(
874            gpu,
875            self.requant_w_int8_k,
876            src.weight,
877            src.weight_scale,
878            src.weight_scale_2,
879            w_i8,
880            w_scale,
881            n,
882            k,
883            stream,
884        )?;
885        let built = Int8Weight { w_i8, w_scale };
886        // Lost a race (another thread built first): free our duplicate buffers.
887        if let Err(dup) = cell.set(built) {
888            let _ = gpu.free(dup.w_i8);
889            let _ = gpu.free(dup.w_scale);
890        }
891        Ok(*cell.get().expect("int8 weight cell set above"))
892    }
893
894    /// Lazily materialize a Q4_K FFN weight from the NVFP4 source: dequant NVFP4→bf16
895    /// (transient buffer, freed) then quantize bf16→GGML block_q4_K (cached for the
896    /// process lifetime). `src` is the non-transposed NVFP4 weight `[n, k]`.
897    fn ensure_q4k_weight(
898        &self,
899        cell: &std::sync::OnceLock<Q4kWeight>,
900        gpu: &dyn GpuBackend,
901        src: &QuantizedWeight,
902        n: u32,
903        k: u32,
904        stream: u64,
905    ) -> Result<Q4kWeight> {
906        if let Some(w) = cell.get() {
907            return Ok(*w);
908        }
909        // transient bf16 [n, k] (freed after quantize); persistent Q4_K bytes.
910        let bf16_tmp = gpu.alloc((n as usize) * (k as usize) * 2)?;
911        ops::dequant_nvfp4_to_bf16(
912            gpu,
913            self.dequant_nvfp4_bf16_k,
914            src.weight,
915            src.weight_scale,
916            bf16_tmp,
917            src.weight_scale_2,
918            n,
919            k,
920            stream,
921        )?;
922        let w_q4k = gpu.alloc(ops::q4k_weight_bytes(n, k))?;
923        ops::quantize_weight_q4k(gpu, self.q4k_quant_w_k, bf16_tmp, w_q4k, n, k, stream)?;
924        // bf16_tmp consumed by the quantize on `stream`; sync before freeing it.
925        gpu.synchronize(stream)?;
926        let _ = gpu.free(bf16_tmp);
927        let built = Q4kWeight { w_q4k };
928        if let Err(dup) = cell.set(built) {
929            let _ = gpu.free(dup.w_q4k);
930        }
931        Ok(*cell.get().expect("q4k weight cell set above"))
932    }
933
934    /// Single-token decode: 2-3 kernel launches depending on activation.
935    /// SiLU: dual GEMV + SiLU-fused down GEMV (2 launches).
936    /// GELU: dual GEMV + gelu_mul + down GEMV (3 launches, no fused GELU down kernel).
937    pub fn forward(
938        &self,
939        input: DevicePtr,
940        ctx: &ForwardContext,
941        stream: u64,
942    ) -> Result<DevicePtr> {
943        let h = ctx.config.hidden_size as u32;
944        let inter = ctx.config.intermediate_size as u32;
945
946        let gate_out = ctx.buffers.expert_gate_out();
947        let up_out = ctx.buffers.expert_up_out();
948
949        // Native keep-packed Q2_0 dispatch (highest priority). Per-projection
950        // `q2_0_gemv`: BF16 activation × packed 2-bit weight, dequant in the
951        // dot-product — weights never expand to BF16/NVFP4. No fused dual/silu
952        // kernel yet, so this is gate + up + silu_mul + down (4 launches),
953        // mirroring the FP8 non-fused fallback. SiLU only (Ternary-Bonsai is a
954        // Qwen-family SwiGLU); GeLU packed-Q2 is a follow-up.
955        if let Some(ref q2w) = self.q2_weights {
956            if self.q2_0_gemv_k.0 == 0 {
957                anyhow::bail!(
958                    "q2_0_gemv kernel missing in this target build — packed-Q2 decode \
959                     (ATLAS_GGUF_NATIVE_Q2) is unavailable"
960                );
961            }
962            if self.activation != FfnActivation::SiLU {
963                anyhow::bail!(
964                    "packed-Q2 FFN decode supports SiLU only (got {:?})",
965                    self.activation
966                );
967            }
968            let output = ctx.buffers.moe_output();
969            ops::q2_0_gemv_vec(
970                ctx.gpu,
971                self.q2_0_gemv_k,
972                input,
973                &q2w.gate_proj,
974                gate_out,
975                stream,
976            )?;
977            ops::q2_0_gemv_vec(
978                ctx.gpu,
979                self.q2_0_gemv_k,
980                input,
981                &q2w.up_proj,
982                up_out,
983                stream,
984            )?;
985            ops::silu_mul(
986                ctx.gpu,
987                self.act_mul,
988                gate_out,
989                up_out,
990                gate_out,
991                inter,
992                stream,
993            )?;
994            ops::q2_0_gemv_vec(
995                ctx.gpu,
996                self.q2_0_gemv_k,
997                gate_out,
998                &q2w.down_proj,
999                output,
1000                stream,
1001            )?;
1002            return Ok(output);
1003        }
1004
1005        // FP8 dispatch: prefer the fused FP8 dual-GEMV (gate+up in one launch) +
1006        // SiLU-fused down GEMV, mirroring the NVFP4 path. Collapses gate+up+
1007        // silu_mul+down (4 launches) to dual+silu (2). Falls back to the
1008        // 3-launch per-projection `w8a16_gemv` path when the fused kernels or a
1009        // non-SiLU activation make the fast path unavailable.
1010        if let Some(ref fp8w) = self.fp8_weights {
1011            let output = ctx.buffers.moe_output();
1012            if self.activation == FfnActivation::SiLU
1013                && self.w8a16_gemv_dual_k.0 != 0
1014                && self.w8a16_gemv_silu_input_k.0 != 0
1015            {
1016                ops::w8a16_gemv_dual(
1017                    ctx.gpu,
1018                    self.w8a16_gemv_dual_k,
1019                    input,
1020                    fp8w.gate_proj.weight,
1021                    fp8w.gate_proj.row_scale,
1022                    gate_out,
1023                    fp8w.up_proj.weight,
1024                    fp8w.up_proj.row_scale,
1025                    up_out,
1026                    inter,
1027                    h,
1028                    stream,
1029                )?;
1030                ops::w8a16_gemv_silu_input(
1031                    ctx.gpu,
1032                    self.w8a16_gemv_silu_input_k,
1033                    gate_out,
1034                    up_out,
1035                    fp8w.down_proj.weight,
1036                    fp8w.down_proj.row_scale,
1037                    output,
1038                    h,
1039                    inter,
1040                    stream,
1041                )?;
1042                return Ok(output);
1043            }
1044            ops::w8a16_gemv(
1045                ctx.gpu,
1046                self.w8a16_gemv_k,
1047                input,
1048                fp8w.gate_proj.weight,
1049                fp8w.gate_proj.row_scale,
1050                gate_out,
1051                inter,
1052                h,
1053                stream,
1054            )?;
1055            ops::w8a16_gemv(
1056                ctx.gpu,
1057                self.w8a16_gemv_k,
1058                input,
1059                fp8w.up_proj.weight,
1060                fp8w.up_proj.row_scale,
1061                up_out,
1062                inter,
1063                h,
1064                stream,
1065            )?;
1066            ops::silu_mul(
1067                ctx.gpu,
1068                self.act_mul,
1069                gate_out,
1070                up_out,
1071                gate_out,
1072                inter,
1073                stream,
1074            )?;
1075            ops::w8a16_gemv(
1076                ctx.gpu,
1077                self.w8a16_gemv_k,
1078                gate_out,
1079                fp8w.down_proj.weight,
1080                fp8w.down_proj.row_scale,
1081                output,
1082                h,
1083                inter,
1084                stream,
1085            )?;
1086            return Ok(output);
1087        }
1088
1089        // BF16 dispatch: per-projection GEMV via `dense_gemv_bf16`. We
1090        // don't have a fused dual-BF16-GEMV kernel today; two sequential
1091        // launches are still BF16-precision-correct and only ~10% slower
1092        // than the fused w4a16 path on Gemma-4-31B (the cost is dominated
1093        // by the bigger BF16 weight reads, not launch overhead).
1094        if let Some(ref bf16w) = self.bf16_weights {
1095            ops::dense_gemv(
1096                ctx.gpu,
1097                self.dense_gemv_bf16_k,
1098                input,
1099                &bf16w.gate_proj,
1100                gate_out,
1101                inter,
1102                h,
1103                stream,
1104            )?;
1105            ops::dense_gemv(
1106                ctx.gpu,
1107                self.dense_gemv_bf16_k,
1108                input,
1109                &bf16w.up_proj,
1110                up_out,
1111                inter,
1112                h,
1113                stream,
1114            )?;
1115            ops::silu_mul(
1116                ctx.gpu,
1117                self.act_mul,
1118                gate_out,
1119                up_out,
1120                gate_out,
1121                inter,
1122                stream,
1123            )?;
1124            let output = ctx.buffers.moe_output();
1125            ops::dense_gemv(
1126                ctx.gpu,
1127                self.dense_gemv_bf16_k,
1128                gate_out,
1129                &bf16w.down_proj,
1130                output,
1131                h,
1132                inter,
1133                stream,
1134            )?;
1135            return Ok(output);
1136        }
1137
1138        // ATLAS_DECODE_FFN_VIA_GEMM=1: route decode's M=1 FFN projections
1139        // through the SAME transposed-weight GEMM kernels the DFlash verify
1140        // path uses (`w4a16_prefill_gemm` → w4a16_gemm_t / _t_k64), instead
1141        // of the dedicated GEMV kernels. Purpose: bit-identical FFN numerics
1142        // between serial decode and batched verify — the batch-K vs batch-1
1143        // divergence #218's bisect isolated ("FFN non-associativity") and the
1144        // root cause of the T=0 spec trajectory flips (2026-07-07 session).
1145        // Split SiLU staging already matches prefill SiLU numerics (swiglu
1146        // clamp), so with this arm the whole FFN block is kernel-identical to
1147        // a verify row. Requires the *_proj_t transposed copies (the NVFP4-MMQ
1148        // prefill arm FREES them — disable it if the warn below fires).
1149        // The `OnceLock<bool>` static that lived here is now a field on
1150        // `layers::ops::ModelLevers` — resolved when the model is built and carried
1151        // on `ForwardContext`, because a static outlives the model whose flags it
1152        // encodes.
1153        if ctx.levers.decode_ffn_via_gemm
1154            && self.activation == FfnActivation::SiLU
1155            && self.act_mul.0 != 0
1156        {
1157            let wt_alive =
1158                |w: &Option<QuantizedWeight>| w.as_ref().is_some_and(|w| !w.weight.is_null());
1159            if wt_alive(&self.weights.gate_proj_t) && wt_alive(&self.weights.up_proj_t) {
1160                // Log-once latch (see `atlas_core::scope`). It holds no model-derived
1161                // value — the message is rebuilt from the arguments every call — so a
1162                // stale entry cannot produce a wrong answer, only a suppressed duplicate
1163                // line after a model swap. Scoping it would thread a logging concern
1164                // through the call path to prevent one repeated INFO line.
1165                if ctx.stats.once("log:decode_ffn_via_gemm") {
1166                    tracing::info!(
1167                        "decode FFN via verify GEMM path (ATLAS_DECODE_FFN_VIA_GEMM=1): \
1168                         gate/up/down through w4a16_prefill_gemm at M=1"
1169                    );
1170                }
1171                self.w4a16_prefill_gemm(
1172                    ctx,
1173                    &self.weights.gate_proj,
1174                    self.weights.gate_proj_t.as_ref(),
1175                    input,
1176                    gate_out,
1177                    1,
1178                    inter,
1179                    h,
1180                    stream,
1181                )?;
1182                self.w4a16_prefill_gemm(
1183                    ctx,
1184                    &self.weights.up_proj,
1185                    self.weights.up_proj_t.as_ref(),
1186                    input,
1187                    up_out,
1188                    1,
1189                    inter,
1190                    h,
1191                    stream,
1192                )?;
1193                ops::silu_mul(
1194                    ctx.gpu,
1195                    self.act_mul,
1196                    gate_out,
1197                    up_out,
1198                    gate_out,
1199                    inter,
1200                    stream,
1201                )?;
1202                let output = ctx.buffers.moe_output();
1203                self.w4a16_prefill_gemm(
1204                    ctx,
1205                    &self.weights.down_proj,
1206                    self.weights.down_proj_t.as_ref(),
1207                    gate_out,
1208                    output,
1209                    1,
1210                    h,
1211                    inter,
1212                    stream,
1213                )?;
1214                return Ok(output);
1215            }
1216            // Log-once latch (see `atlas_core::scope`). It holds no model-derived
1217            // value — the message is rebuilt from the arguments every call — so a
1218            // stale entry cannot produce a wrong answer, only a suppressed duplicate
1219            // line after a model swap. Scoping it would thread a logging concern
1220            // through the call path to prevent one repeated INFO line.
1221            if ctx.stats.once("log:decode_ffn_no_twins") {
1222                tracing::warn!(
1223                    "ATLAS_DECODE_FFN_VIA_GEMM=1 requested but transposed FFN copies \
1224                     are freed/absent (NVFP4-MMQ prefill arm?) — falling back to GEMV; \
1225                     the unification experiment is NOT active"
1226                );
1227            }
1228        }
1229
1230        // Fused gate_proj + up_proj: [1, H] → [1, inter] × 2.
1231        // Single-warp variant (lossless) when the lever is on and the kernel
1232        // resolved; otherwise the 64-thread kernel. Dual and silu-input SW
1233        // are independent — missing silu_input_sw must not skip dual_sw on
1234        // the default split-SiLU path.
1235        let use_dual_sw = ops::use_gemv_sw(ctx.levers.gemv_sw, self.w4a16_gemv_dual_sw);
1236        let use_silu_sw = ops::use_gemv_sw(ctx.levers.gemv_sw, self.w4a16_gemv_silu_input_sw);
1237        if use_dual_sw {
1238            ops::w4a16_gemv_dual_sw(
1239                ctx.gpu,
1240                self.w4a16_gemv_dual_sw,
1241                input,
1242                &self.weights.gate_proj,
1243                gate_out,
1244                &self.weights.up_proj,
1245                up_out,
1246                inter,
1247                h,
1248                stream,
1249            )?;
1250        } else {
1251            ops::w4a16_gemv_dual(
1252                ctx.gpu,
1253                self.w4a16_gemv_dual,
1254                input,
1255                &self.weights.gate_proj,
1256                gate_out,
1257                &self.weights.up_proj,
1258                up_out,
1259                inter,
1260                h,
1261                stream,
1262            )?;
1263        }
1264
1265        let output = ctx.buffers.moe_output();
1266        // Split SiLU+down (DEFAULT; kill-switch ATLAS_NO_DECODE_SPLIT_SILU): the fused
1267        // silu_input kernel recomputes the SiLU transcendentals per OUTPUT ROW (N/4
1268        // blocks × redundant __expf) and measures COMPUTE-bound — ncu: SM 57% vs
1269        // memory 23%, 186 GB/s vs the dual GEMV's 266. Staging silu(gate)*up once
1270        // (one elementwise launch, CUDA graphs amortize it) lets the down GEMV run
1271        // memory-bound like the dual. Also aligns decode with the prefill SiLU
1272        // numerics (swiglu clamp), which the fused kernel lacked.
1273        //
1274        // An installed LoRA adapter PINS this path. The fused `silu_input`
1275        // alternative never materialises silu(gate)*up — it consumes gate and
1276        // up straight into the down GEMV — and the down delta has to contract
1277        // over exactly that activation. Reproducing it into `lora_hact` just
1278        // to feed the delta would compute the SiLU twice for a path that is
1279        // already the default and already the numerically preferred one, so
1280        // the adapter pins it instead. `set_lora_weights` refuses an adapter
1281        // when this path is unavailable on the layer, which makes
1282        // `lora.is_some()` imply the three conditions above.
1283        let split_silu = self.activation == FfnActivation::SiLU
1284            && self.act_mul.0 != 0
1285            && self.w4a16_gemv.0 != 0
1286            && (std::env::var_os("ATLAS_NO_DECODE_SPLIT_SILU").is_none() || self.lora.is_some());
1287        if split_silu {
1288            self.apply_lora_gate_up(ctx, input, gate_out, up_out, 1, stream)?;
1289            ops::silu_mul(
1290                ctx.gpu,
1291                self.act_mul,
1292                gate_out,
1293                up_out,
1294                gate_out,
1295                inter,
1296                stream,
1297            )?;
1298            ops::w4a16_decode_gemv(
1299                ctx.gpu,
1300                self.w4a16_gemv,
1301                self.w4a16_gemv_sw,
1302                ctx.levers.gemv_sw,
1303                gate_out,
1304                &self.weights.down_proj,
1305                output,
1306                h,
1307                inter,
1308                stream,
1309            )?;
1310            self.apply_lora_down(ctx, gate_out, output, 1, stream)?;
1311            return Ok(output);
1312        }
1313        debug_assert!(
1314            self.lora.is_none(),
1315            "LoRA installed but decode took the fused silu_input path, which \
1316             never materialises the activation the down delta contracts over; \
1317             set_lora_weights is supposed to make this unreachable"
1318        );
1319        match self.activation {
1320            FfnActivation::SiLU => {
1321                // Fused SiLU(gate)*up + down_proj: [1, inter] → [1, H]
1322                if use_silu_sw {
1323                    ops::w4a16_gemv_silu_input_sw(
1324                        ctx.gpu,
1325                        self.w4a16_gemv_silu_input_sw,
1326                        gate_out,
1327                        up_out,
1328                        &self.weights.down_proj,
1329                        output,
1330                        h,
1331                        inter,
1332                        stream,
1333                    )?;
1334                } else {
1335                    ops::w4a16_gemv_silu_input(
1336                        ctx.gpu,
1337                        self.w4a16_gemv_silu_input,
1338                        gate_out,
1339                        up_out,
1340                        &self.weights.down_proj,
1341                        output,
1342                        h,
1343                        inter,
1344                        stream,
1345                    )?;
1346                }
1347            }
1348            FfnActivation::GeLU => {
1349                // GELU(gate)*up → gate_out, then down_proj GEMV
1350                ops::silu_mul(
1351                    ctx.gpu,
1352                    self.act_mul,
1353                    gate_out,
1354                    up_out,
1355                    gate_out,
1356                    inter,
1357                    stream,
1358                )?;
1359                ops::w4a16_decode_gemv(
1360                    ctx.gpu,
1361                    self.w4a16_gemv,
1362                    self.w4a16_gemv_sw,
1363                    ctx.levers.gemv_sw,
1364                    gate_out,
1365                    &self.weights.down_proj,
1366                    output,
1367                    h,
1368                    inter,
1369                    stream,
1370                )?;
1371            }
1372        }
1373
1374        Ok(output)
1375    }
1376
1377    /// Packed-Q2 batched decode FFN for `m` concurrent rows (`m >= 2`). Mirrors
1378    /// the single-token `forward` packed-Q2 arm — per-projection keep-packed
1379    /// `q2_0_gemv_vec_batchm` (BF16 `[m,·]` activation × 2-bit weight, dequant in
1380    /// the dot-product, no BF16/NVFP4 expansion), SiLU-mul between gate/up, then
1381    /// down — but with `m` activation rows staged per weight read. This is the
1382    /// correctness path for concurrent decode (C>=2): the NVFP4 `forward_k2/k3`
1383    /// GEMVs read the NULL NVFP4 fallback weights, so packed-Q2 must route here.
1384    /// The wrapper chunks internally for `m > 8`. SiLU only (Ternary-Bonsai is a
1385    /// SwiGLU); output lands in `moe_output` as `[m, h]` row-major.
1386    fn forward_km_q2(
1387        &self,
1388        q2w: &DenseFfnWeightsQ2,
1389        input: DevicePtr,
1390        ctx: &ForwardContext,
1391        m: u32,
1392        stream: u64,
1393    ) -> Result<()> {
1394        if self.q2_0_gemv_batchm_k.0 == 0 {
1395            anyhow::bail!(
1396                "q2_0_gemv_vec_batchm kernel missing in this target build — packed-Q2 \
1397                 batched decode (ATLAS_GGUF_NATIVE_Q2, C>=2) is unavailable"
1398            );
1399        }
1400        if self.activation != FfnActivation::SiLU {
1401            anyhow::bail!(
1402                "packed-Q2 FFN batched decode supports SiLU only (got {:?})",
1403                self.activation
1404            );
1405        }
1406        let inter = ctx.config.intermediate_size as u32;
1407        let gate_out = ctx.buffers.expert_gate_out();
1408        let up_out = ctx.buffers.expert_up_out();
1409        let batchm = |w: &PackedQ2Weight, inp: DevicePtr, out: DevicePtr| -> Result<()> {
1410            ops::q2_0_gemv_vec_batchm(ctx.gpu, self.q2_0_gemv_batchm_k, inp, w, out, m, stream)
1411        };
1412        batchm(&q2w.gate_proj, input, gate_out)?;
1413        batchm(&q2w.up_proj, input, up_out)?;
1414        ops::silu_mul(
1415            ctx.gpu,
1416            self.act_mul,
1417            gate_out,
1418            up_out,
1419            gate_out,
1420            m * inter,
1421            stream,
1422        )?;
1423        let output = ctx.buffers.moe_output();
1424        batchm(&q2w.down_proj, gate_out, output)?;
1425        Ok(())
1426    }
1427
1428    /// K=2 speculative: batched GEMV for 2 tokens.
1429    /// 3 launches: dual batch2 (gate+up) + silu_mul + batch2 (down).
1430    pub fn forward_k2(&self, input: DevicePtr, ctx: &ForwardContext, stream: u64) -> Result<()> {
1431        // Packed-Q2: NVFP4 fallback weights are NULL, so the NVFP4 batch2 GEMVs
1432        // below would fault. Route to the keep-packed batchm FFN (m=2).
1433        if let Some(ref q2w) = self.q2_weights {
1434            return self.forward_km_q2(q2w, input, ctx, 2, stream);
1435        }
1436        if native_small_batch_uses_prefill(self.bf16_weights.is_some(), self.fp8_weights.is_some())
1437        {
1438            return self.forward_prefill(input, 2, ctx, stream);
1439        }
1440
1441        let h = ctx.config.hidden_size as u32;
1442        let inter = ctx.config.intermediate_size as u32;
1443
1444        let gate_out = ctx.buffers.expert_gate_out();
1445        let up_out = ctx.buffers.expert_up_out();
1446
1447        // Fused gate+up for 2 tokens
1448        ops::w4a16_gemv_dual_batch2(
1449            ctx.gpu,
1450            self.w4a16_gemv_dual_batch2,
1451            input,
1452            &self.weights.gate_proj,
1453            gate_out,
1454            &self.weights.up_proj,
1455            up_out,
1456            inter,
1457            h,
1458            stream,
1459        )?;
1460        self.apply_lora_gate_up(ctx, input, gate_out, up_out, 2, stream)?;
1461        ops::silu_mul(
1462            ctx.gpu,
1463            self.act_mul,
1464            gate_out,
1465            up_out,
1466            gate_out,
1467            2 * inter,
1468            stream,
1469        )?;
1470        let output = ctx.buffers.moe_output();
1471        ops::w4a16_gemv_batch2(
1472            ctx.gpu,
1473            self.w4a16_gemv_batch2,
1474            gate_out,
1475            &self.weights.down_proj,
1476            output,
1477            h,
1478            inter,
1479            stream,
1480        )?;
1481        self.apply_lora_down(ctx, gate_out, output, 2, stream)?;
1482
1483        Ok(())
1484    }
1485
1486    /// K=3 speculative: batched GEMV for 3 tokens.
1487    /// 3 launches: dual batch3 (gate+up) + silu_mul + batch3 (down).
1488    pub fn forward_k3(&self, input: DevicePtr, ctx: &ForwardContext, stream: u64) -> Result<()> {
1489        // Packed-Q2: route to the keep-packed batchm FFN (m=3); NVFP4 weights null.
1490        if let Some(ref q2w) = self.q2_weights {
1491            return self.forward_km_q2(q2w, input, ctx, 3, stream);
1492        }
1493        if native_small_batch_uses_prefill(self.bf16_weights.is_some(), self.fp8_weights.is_some())
1494        {
1495            return self.forward_prefill(input, 3, ctx, stream);
1496        }
1497
1498        let h = ctx.config.hidden_size as u32;
1499        let inter = ctx.config.intermediate_size as u32;
1500
1501        let gate_out = ctx.buffers.expert_gate_out();
1502        let up_out = ctx.buffers.expert_up_out();
1503
1504        // Fused gate+up for 3 tokens
1505        ops::w4a16_gemv_dual_batch3(
1506            ctx.gpu,
1507            self.w4a16_gemv_dual_batch3,
1508            input,
1509            &self.weights.gate_proj,
1510            gate_out,
1511            &self.weights.up_proj,
1512            up_out,
1513            inter,
1514            h,
1515            stream,
1516        )?;
1517        self.apply_lora_gate_up(ctx, input, gate_out, up_out, 3, stream)?;
1518        ops::silu_mul(
1519            ctx.gpu,
1520            self.act_mul,
1521            gate_out,
1522            up_out,
1523            gate_out,
1524            3 * inter,
1525            stream,
1526        )?;
1527        let output = ctx.buffers.moe_output();
1528        ops::w4a16_gemv_batch3(
1529            ctx.gpu,
1530            self.w4a16_gemv_batch3,
1531            gate_out,
1532            &self.weights.down_proj,
1533            output,
1534            h,
1535            inter,
1536            stream,
1537        )?;
1538        self.apply_lora_down(ctx, gate_out, output, 3, stream)?;
1539
1540        Ok(())
1541    }
1542
1543    /// Batchm-GEMV kernel for `m` verify rows: the narrowest resolved tier in
1544    /// `w4a16_gemv_batch{4,5,6,7,8}` that covers `m`. 0-handle when out of
1545    /// range or absent. See `layers::w4a16_gemv_tiers` for the decision and
1546    /// the `ATLAS_NO_GEMV_EXACT_M_TIERS=1` kill switch.
1547    fn batchm_kernel(&self, m: u32) -> KernelHandle {
1548        self.w4a16_batchm.kernel(m)
1549    }
1550
1551    /// Whether the M-row batched-GEMV verify path is available for `m` rows
1552    /// (batchm kernel present AND NVFP4 weights loaded — the batchm GEMV
1553    /// reads the non-transposed NVFP4 layout).
1554    pub fn can_forward_km(&self, m: u32) -> bool {
1555        self.batchm_kernel(m).0 != 0 && !self.weights.gate_proj.weight.is_null()
1556    }
1557
1558    /// K=m (m<=8) speculative verify: batched GEMV for m tokens.
1559    /// 4 launches: batchm gate + batchm up + silu_mul + batchm down — each
1560    /// projection weight is read ONCE for all m rows at near-peak stream
1561    /// bandwidth. nsys (2026-07-18, M=4): the `forward_prefill` MMQ arm this
1562    /// replaces for the K=4 verify cost 54.8 ms/step across the 64-layer
1563    /// dense FFN stack (~156 GB/s effective at M=4); the batch GEMV family
1564    /// measures ~290 GB/s on the same shapes (w8a16_gemv_batch4 sibling),
1565    /// putting this path at the ~31 ms weight-traffic floor. m=5..8 uses
1566    /// `w4a16_gemv_batch8` (batchm_bench: same weight-streaming bandwidth,
1567    /// removing the M>4 tile-GEMM cliff for chain-verify K=5..8).
1568    pub fn forward_km(
1569        &self,
1570        input: DevicePtr,
1571        m: u32,
1572        ctx: &ForwardContext,
1573        stream: u64,
1574    ) -> Result<()> {
1575        // As in k2/k3, concurrent decode must preserve an installed native
1576        // overlay even when valid, lower-precision NVFP4 fallbacks coexist.
1577        if native_small_batch_uses_prefill(self.bf16_weights.is_some(), self.fp8_weights.is_some())
1578        {
1579            return self.forward_prefill(input, m as usize, ctx, stream);
1580        }
1581        let h = ctx.config.hidden_size as u32;
1582        let inter = ctx.config.intermediate_size as u32;
1583        let kh = self.batchm_kernel(m);
1584
1585        let gate_out = ctx.buffers.expert_gate_out();
1586        let up_out = ctx.buffers.expert_up_out();
1587
1588        ops::w4a16_gemv_batchm(
1589            ctx.gpu,
1590            kh,
1591            input,
1592            &self.weights.gate_proj,
1593            gate_out,
1594            m,
1595            inter,
1596            h,
1597            stream,
1598        )?;
1599        ops::w4a16_gemv_batchm(
1600            ctx.gpu,
1601            kh,
1602            input,
1603            &self.weights.up_proj,
1604            up_out,
1605            m,
1606            inter,
1607            h,
1608            stream,
1609        )?;
1610        self.apply_lora_gate_up(ctx, input, gate_out, up_out, m, stream)?;
1611        ops::silu_mul(
1612            ctx.gpu,
1613            self.act_mul,
1614            gate_out,
1615            up_out,
1616            gate_out,
1617            m * inter,
1618            stream,
1619        )?;
1620        let output = ctx.buffers.moe_output();
1621        ops::w4a16_gemv_batchm(
1622            ctx.gpu,
1623            kh,
1624            gate_out,
1625            &self.weights.down_proj,
1626            output,
1627            m,
1628            h,
1629            inter,
1630            stream,
1631        )?;
1632        self.apply_lora_down(ctx, gate_out, output, m, stream)?;
1633
1634        Ok(())
1635    }
1636
1637    /// N-token prefill: GEMM for all projections.
1638    /// W4A16 prefill/verify GEMM dispatch, routed by (M, K) per
1639    /// w4a16_m17_bench measurements on GB10:
1640    ///   - M<=64 (DFlash verify M=17): the M64-tile `w4a16_gemm_t` beats the
1641    ///     M128-tile kernels (283 vs 324us on gate/up — 87% of an M128 tile
1642    ///     is padding at M=17), and `w4a16_gemm_t_k64` wins deep-K down_proj
1643    ///     (554 vs 810us at K=17408, where N/128 CTAs can't fill the GPU and
1644    ///     the halved K-loop matters).
1645    ///   - M>64 (real prefill): v2 (8-warp) > t_m128 (4-warp), unchanged.
1646    ///   - No transposed copy: base `w4a16_gemm` (9-12x the bandwidth floor —
1647    ///     last resort).
1648    ///
1649    /// Kill-switch: ATLAS_FFN_SMALLM=0 restores the m128-only dispatch for A/B.
1650    #[allow(clippy::too_many_arguments)]
1651    fn w4a16_prefill_gemm(
1652        &self,
1653        ctx: &ForwardContext,
1654        w: &QuantizedWeight,
1655        wt: Option<&QuantizedWeight>,
1656        input: DevicePtr,
1657        output: DevicePtr,
1658        m: u32,
1659        n: u32,
1660        k: u32,
1661        stream: u64,
1662    ) -> Result<()> {
1663        // The `OnceLock<bool>` static that lived here is now a field on
1664        // `layers::ops::ModelLevers` — resolved when the model is built and carried
1665        // on `ForwardContext`, because a static outlives the model whose flags it
1666        // encodes.
1667        if let Some(wt) = wt {
1668            if m <= 64 && k.is_multiple_of(32) && ctx.levers.ffn_small_m {
1669                if k >= crate::layers::w4a16_k64_min_k()
1670                    && k.is_multiple_of(64)
1671                    && self.w4a16_gemm_t_k64_k.0 != 0
1672                {
1673                    return ops::w4a16_gemm_n128(
1674                        ctx.gpu,
1675                        self.w4a16_gemm_t_k64_k,
1676                        input,
1677                        wt,
1678                        output,
1679                        m,
1680                        n,
1681                        k,
1682                        stream,
1683                    );
1684                }
1685                if self.w4a16_gemm_t_k.0 != 0 {
1686                    return ops::w4a16_gemm_n128(
1687                        ctx.gpu,
1688                        self.w4a16_gemm_t_k,
1689                        input,
1690                        wt,
1691                        output,
1692                        m,
1693                        n,
1694                        k,
1695                        stream,
1696                    );
1697                }
1698            }
1699            if self.w4a16_gemm_t_m128_v2_k.0 != 0 {
1700                return ops::w4a16_gemm_n128_m128_v2(
1701                    ctx.gpu,
1702                    self.w4a16_gemm_t_m128_v2_k,
1703                    input,
1704                    wt,
1705                    output,
1706                    m,
1707                    n,
1708                    k,
1709                    stream,
1710                );
1711            }
1712            if self.w4a16_gemm_t_m128_k.0 != 0 {
1713                return ops::w4a16_gemm_n128_m128(
1714                    ctx.gpu,
1715                    self.w4a16_gemm_t_m128_k,
1716                    input,
1717                    wt,
1718                    output,
1719                    m,
1720                    n,
1721                    k,
1722                    stream,
1723                );
1724            }
1725        }
1726        ops::w4a16_gemm(ctx.gpu, self.w4a16_gemm, input, w, output, m, n, k, stream)
1727    }
1728
1729    /// Timed wrapper around the dense-FFN prefill.
1730    ///
1731    /// ★ THIS PATH HAD NO TIMERS AT ALL, and that hid the largest unexplained
1732    /// number on the board. Profiling nvidia/Gemma-4-31B-IT-NVFP4 at a
1733    /// 4096-token prompt: wall 28,180 ms, while EVERY profiled phase across
1734    /// `ATTN prefill [...]` and `MoE prefill [...]` summed to 3,269.8 ms. 88% of
1735    /// the prefill was invisible — not attributed to something slow, simply not
1736    /// instrumented. `forward_prefill` dispatches ~20 quantization arms and none
1737    /// of them reported elapsed time; only one-shot "which arm was chosen" INFO
1738    /// lines existed.
1739    ///
1740    /// One coarse timer first, deliberately: it answers whether the missing time
1741    /// is here at all before anyone threads timers through twenty arms. Same
1742    /// `<AREA> prefill [phase] N=<n>: <us>µs` shape the attention and MoE paths
1743    /// already emit, so the existing log-summing one-liners pick it up unchanged.
1744    pub fn forward_prefill(
1745        &self,
1746        input: DevicePtr,
1747        num_tokens: usize,
1748        ctx: &ForwardContext,
1749        stream: u64,
1750    ) -> Result<()> {
1751        if !ctx.profile {
1752            return self.forward_prefill_inner(input, num_tokens, ctx, stream);
1753        }
1754        let t0 = std::time::Instant::now();
1755        let r = self.forward_prefill_inner(input, num_tokens, ctx, stream);
1756        // Sync so the figure is the kernel's, not the launch queue's — the
1757        // attention and MoE timers do the same under `ctx.profile`.
1758        ctx.gpu.synchronize(stream)?;
1759        tracing::info!(
1760            "  FFN prefill [dense_total] N={}: {}µs",
1761            num_tokens,
1762            t0.elapsed().as_micros()
1763        );
1764        r
1765    }
1766
1767    fn forward_prefill_inner(
1768        &self,
1769        input: DevicePtr,
1770        num_tokens: usize,
1771        ctx: &ForwardContext,
1772        stream: u64,
1773    ) -> Result<()> {
1774        let h = ctx.config.hidden_size as u32;
1775        let inter = ctx.config.intermediate_size as u32;
1776        let m = num_tokens as u32;
1777
1778        let gate_out = ctx.buffers.expert_gate_out();
1779        let up_out = ctx.buffers.expert_up_out();
1780
1781        // Native keep-packed Q2_0 prefill (Tier-1): the resident weight stays
1782        // 2-bit, but prefill has no packed-MMQ kernel yet (that's Tier-2). So we
1783        // dequant each projection into a TRANSIENT BF16 scratch `[N, K]` via the
1784        // load-time `dequant_q2_0_gn_to_bf16` kernel, run the normal BF16
1785        // prefill GEMM (tensor-core when present), then free the scratch. Only a
1786        // per-matmul scratch is BF16 — the WeightStore blocks stay 2-bit. Decode
1787        // still uses the native `q2_0_gemv` (no dequant). SiLU only.
1788        if let Some(ref q2w) = self.q2_weights {
1789            if self.activation != FfnActivation::SiLU {
1790                anyhow::bail!(
1791                    "packed-Q2 FFN prefill supports SiLU only (got {:?})",
1792                    self.activation
1793                );
1794            }
1795
1796            // Tier-2 native MMQ prefill (ATLAS_GGUF_NATIVE_Q2_MMQ=1): quantize the
1797            // activation to q8_1 ONCE per projection-input (gate/up share `input`;
1798            // down re-quantizes `gate_out`), then run the packed 2-bit MMQ GEMM —
1799            // no BF16 weight dequant, no shared `q2_dequant_scratch`. Requires the
1800            // MMQ kernel + the shared q8_1 quantizer + group-128 weights.
1801            let q2_mmq = self.q2_0_mmq_nc_k.0 != 0
1802                && self.q4k_quant_act_k.0 != 0
1803                && ops::native_q2_mmq_enabled()
1804                && q2w.gate_proj.group == 128
1805                && q2w.up_proj.group == 128
1806                && q2w.down_proj.group == 128;
1807            if q2_mmq {
1808                static Q2MMQ_LOG: std::sync::Once = std::sync::Once::new();
1809                Q2MMQ_LOG.call_once(|| {
1810                    eprintln!(
1811                        "[atlas] ATLAS_GGUF_NATIVE_Q2_MMQ=1: dense-FFN prefill via native packed Q2_0 MMQ (W2A8, keep-packed)"
1812                    );
1813                });
1814                let a_q8 = ctx.buffers.q2_act_q8();
1815                let mmq = |w: &PackedQ2Weight, out: DevicePtr| -> Result<()> {
1816                    ops::q2_0_mmq_gemm(
1817                        ctx.gpu,
1818                        self.q2_0_mmq_nc_k,
1819                        self.q2_0_mmq_wc_k,
1820                        a_q8,
1821                        w.weight,
1822                        out,
1823                        m,
1824                        w.n,
1825                        w.k,
1826                        stream,
1827                    )
1828                };
1829                // gate/up: quantize `input` [m,h] once, feed both.
1830                ops::quantize_act_q8_1(ctx.gpu, self.q4k_quant_act_k, input, a_q8, m, h, stream)?;
1831                mmq(&q2w.gate_proj, gate_out)?;
1832                mmq(&q2w.up_proj, up_out)?;
1833                ops::silu_mul(
1834                    ctx.gpu,
1835                    self.act_mul,
1836                    gate_out,
1837                    up_out,
1838                    gate_out,
1839                    m * inter,
1840                    stream,
1841                )?;
1842                // down: quantize `gate_out` [m,inter] (same-stream after silu_mul).
1843                let output = ctx.buffers.moe_output();
1844                ops::quantize_act_q8_1(
1845                    ctx.gpu,
1846                    self.q4k_quant_act_k,
1847                    gate_out,
1848                    a_q8,
1849                    m,
1850                    inter,
1851                    stream,
1852                )?;
1853                mmq(&q2w.down_proj, output)?;
1854                return Ok(());
1855            }
1856
1857            // Transient-dequant stopgap (Tier-1): requires the load-time dequant kernel.
1858            if self.dequant_q2_0_gn_k.0 == 0 {
1859                anyhow::bail!(
1860                    "dequant_q2_0_gn_to_bf16 kernel missing in this target build — \
1861                     packed-Q2 (ATLAS_GGUF_NATIVE_Q2) prefill is unavailable"
1862                );
1863            }
1864            let tc = self.dense_gemm_tc_k.0 != 0;
1865            // Dequant one packed-Q2 projection into the PERSISTENT arena BF16
1866            // scratch `[n, k]`, run the BF16 GEMM (A=`in` [m,k] → out [m,n]). No
1867            // per-matmul alloc/sync/free: the arena buffer is sized to the
1868            // largest packed projection and reused. Gate → up → down run
1869            // sequentially on `stream`, so each GEMM consumes the scratch before
1870            // the next projection's dequant overwrites it (same-stream order).
1871            let scratch = ctx.buffers.q2_dequant_scratch();
1872            let q2_gemm = |w: &PackedQ2Weight, input: DevicePtr, out: DevicePtr| -> Result<()> {
1873                let (n, k) = (w.n, w.k);
1874                debug_assert!(
1875                    (n as usize) * (k as usize) * 2 <= ctx.buffers.q2_dequant_scratch_bytes(),
1876                    "packed-Q2 FFN dequant scratch too small for [{n},{k}] BF16"
1877                );
1878                ops::dequant_q2_0_gn_to_bf16(
1879                    ctx.gpu,
1880                    self.dequant_q2_0_gn_k,
1881                    w.weight,
1882                    scratch,
1883                    n,
1884                    k,
1885                    w.group as u32,
1886                    stream,
1887                )?;
1888                let dw = DenseWeight { weight: scratch };
1889                if tc {
1890                    ops::dense_gemm_tc(
1891                        ctx.gpu,
1892                        self.dense_gemm_tc_k,
1893                        input,
1894                        &dw,
1895                        out,
1896                        m,
1897                        n,
1898                        k,
1899                        stream,
1900                    )?;
1901                } else {
1902                    ops::dense_gemm(
1903                        ctx.gpu,
1904                        self.dense_gemm_bf16_k,
1905                        input,
1906                        &dw,
1907                        out,
1908                        m,
1909                        n,
1910                        k,
1911                        stream,
1912                    )?;
1913                }
1914                Ok(())
1915            };
1916            q2_gemm(&q2w.gate_proj, input, gate_out)?;
1917            q2_gemm(&q2w.up_proj, input, up_out)?;
1918            ops::silu_mul(
1919                ctx.gpu,
1920                self.act_mul,
1921                gate_out,
1922                up_out,
1923                gate_out,
1924                m * inter,
1925                stream,
1926            )?;
1927            let output = ctx.buffers.moe_output();
1928            q2_gemm(&q2w.down_proj, gate_out, output)?;
1929            return Ok(());
1930        }
1931
1932        // Native FP8: small batches stream each weight once via the existing
1933        // M<=4 GEMV, avoiding padded MMA tiles. Larger prefills prefer a
1934        // transposed copy when available, then the same-format pipelined GEMM.
1935        // Every fallback retains the original E4M3 bytes and FP32 block scales.
1936        if let Some(ref fp8w) = self.fp8_weights {
1937            macro_rules! w8_gemm {
1938                ($w:expr, $wt:expr, $in:expr, $out:expr, $n:expr, $k:expr) => {
1939                    match $wt {
1940                        _ if (1..=4).contains(&m) && self.w8a16_gemv_batch4_k.0 != 0 => {
1941                            ops::w8a16_gemv_batch4(
1942                                ctx.gpu,
1943                                self.w8a16_gemv_batch4_k,
1944                                $in,
1945                                $w.weight,
1946                                $w.row_scale,
1947                                $out,
1948                                m,
1949                                $n,
1950                                $k,
1951                                stream,
1952                            )?
1953                        }
1954                        Some(wt) if self.w8a16_gemm_t_m128_k.0 != 0 => {
1955                            let wt: Fp8WeightTransposed = wt;
1956                            ops::w8a16_gemm_n128_m128(
1957                                ctx.gpu,
1958                                self.w8a16_gemm_t_m128_k,
1959                                $in,
1960                                wt.weight_t,
1961                                wt.scale_t,
1962                                $out,
1963                                m,
1964                                $n,
1965                                $k,
1966                                stream,
1967                            )?
1968                        }
1969                        _ if self.w8a16_gemm_pipelined_k.0 != 0 => ops::w8a16_gemm_pipelined(
1970                            ctx.gpu,
1971                            self.w8a16_gemm_pipelined_k,
1972                            $in,
1973                            $w.weight,
1974                            $w.row_scale,
1975                            $out,
1976                            m,
1977                            $n,
1978                            $k,
1979                            stream,
1980                        )?,
1981                        _ => ops::w8a16_gemm(
1982                            ctx.gpu,
1983                            self.w8a16_gemm_k,
1984                            $in,
1985                            $w.weight,
1986                            $w.row_scale,
1987                            $out,
1988                            m,
1989                            $n,
1990                            $k,
1991                            stream,
1992                        )?,
1993                    }
1994                };
1995            }
1996            let gate_t: Option<Fp8WeightTransposed> = None;
1997            let up_t: Option<Fp8WeightTransposed> = None;
1998            let down_t: Option<Fp8WeightTransposed> = None;
1999            w8_gemm!(fp8w.gate_proj, gate_t, input, gate_out, inter, h);
2000            w8_gemm!(fp8w.up_proj, up_t, input, up_out, inter, h);
2001            ops::silu_mul(
2002                ctx.gpu,
2003                self.act_mul,
2004                gate_out,
2005                up_out,
2006                gate_out,
2007                m * inter,
2008                stream,
2009            )?;
2010            let output = ctx.buffers.moe_output();
2011            w8_gemm!(fp8w.down_proj, down_t, gate_out, output, h, inter);
2012            return Ok(());
2013        }
2014
2015        // BF16 prefill dispatch. Prefer the tensor-core m16n8k16 MMA kernel
2016        // (`dense_gemm_tc`, 3-5x+ over scalar) — the scalar `dense_gemm_bf16`
2017        // was the flat ~155 tok/s prefill bottleneck on Qwen3.6-27B dense
2018        // NVFP4 (FFN = ~83% of prefill). Falls back to scalar if the TC
2019        // kernel isn't loaded for this target. Decode (gemv, M=1) is a
2020        // separate path, so TPOT is unaffected; BF16 MMA preserves coherence.
2021        if let Some(ref bf16w) = self.bf16_weights {
2022            let tc = self.dense_gemm_tc_k.0 != 0;
2023            // helper: cuBLASLt when enabled (the big win at prefill M), else the
2024            // tensor-core MMA kernel, else scalar. dense_gemm_tc is ~1.4 TFLOP/s
2025            // on the large dense-FFN shapes (e.g. Laguna layer-0 gate/up/down at
2026            // N=12288/3072, K=3072) — nsys measured its 3 launches at ~100 ms
2027            // EACH = 33% of the whole C=1 prefill. cuBLASLt runs the identical
2028            // BF16×BF16→FP32 GEMM at 90+ TFLOP/s (~65× faster), the same path
2029            // q/k/v/o and the head-gate already use. Gated on ATLAS_CUBLAS_GEMM.
2030            macro_rules! ffn_gemm {
2031                ($a:expr, $b:expr, $c:expr, $n:expr, $k:expr) => {
2032                    if ctx.dispatch.cublas_gemm {
2033                        ops::cublas_bf16_proj_dense($a, $b.weight, $c, m, $n, $k, stream)?;
2034                    } else if tc {
2035                        ops::dense_gemm_tc(
2036                            ctx.gpu,
2037                            self.dense_gemm_tc_k,
2038                            $a,
2039                            $b,
2040                            $c,
2041                            m,
2042                            $n,
2043                            $k,
2044                            stream,
2045                        )?;
2046                    } else {
2047                        ops::dense_gemm(
2048                            ctx.gpu,
2049                            self.dense_gemm_bf16_k,
2050                            $a,
2051                            $b,
2052                            $c,
2053                            m,
2054                            $n,
2055                            $k,
2056                            stream,
2057                        )?;
2058                    }
2059                };
2060            }
2061            ffn_gemm!(input, &bf16w.gate_proj, gate_out, inter, h);
2062            ffn_gemm!(input, &bf16w.up_proj, up_out, inter, h);
2063            ops::silu_mul(
2064                ctx.gpu,
2065                self.act_mul,
2066                gate_out,
2067                up_out,
2068                gate_out,
2069                m * inter,
2070                stream,
2071            )?;
2072            let output = ctx.buffers.moe_output();
2073            ffn_gemm!(gate_out, &bf16w.down_proj, output, h, inter);
2074            return Ok(());
2075        }
2076
2077        // Prefill: prefer the 128x128 cp.async-pipelined `w4a16_gemm_t_m128`
2078        // (the kernel attention/SSM use) over the M64xN64 base `w4a16_gemm`
2079        // (~10 TFLOPS, the flat ~155 tok/s bottleneck). That kernel needs the
2080        // TRANSPOSED weight layout, so we use the `*_proj_t` copies built at
2081        // load (decode keeps the non-transposed weights via gemv → TPOT/
2082        // coherence unaffected). Falls back to base when no transposed copy /
2083        // kernel is present.
2084        // LOSSLESS prefill opt-in: when ATLAS_BF16_TC_PREFILL is set AND the
2085        // BF16 128x128 kernel is present, route prefill GEMMs through the
2086        // bit-equivalent BF16 tensor-core path instead of the default FP8-E4M3
2087        // `t_m128`. The FP8 crush is fast but perturbs generation (measured
2088        // length-truncations / accuracy risk on Qwen3.6-27B); the BF16 variant
2089        // keeps the same 128x128 cp.async speed at base-kernel precision.
2090        // Unset (default) → every arm below is byte-for-byte the prior behavior
2091        // (PCND: explicit opt-in, no silent default change). Read once per call.
2092        // Env read only here; the usable gate (`bf16_tc_prefill`) is derived
2093        // below AFTER v1/v2 selection, from the handle actually launched.
2094        // Gating on v1's handle while dispatching v2 admitted launches of a
2095        // kernel this target may not carry.
2096        let bf16_tc_env = std::env::var_os("ATLAS_BF16_TC_PREFILL").is_some();
2097        // FP8 M64 fast-prefill opt-in: route prefill GEMMs through the m16n8k32
2098        // e4m3 M64 kernel (~1.47x vs v2 BF16, smem-relieved). Lossy (cosine 0.9997)
2099        // → highest priority when set, so it overrides the BF16/FP8 t_m128 arms.
2100        // PCND: explicit opt-in, default off = byte-for-byte prior behavior.
2101        let fp8_m64_prefill =
2102            self.w4a16_gemm_t_k.0 != 0 && std::env::var_os("ATLAS_FP8_M64_PREFILL").is_some();
2103        // int8 W4A8 fast-prefill opt-in (ATLAS_INT8_PREFILL): route prefill GEMMs
2104        // through the validated requant→`int8_gemm_faith2` pipeline (cosine
2105        // 0.999978 vs the host full-precision dequant GEMM). HIGHEST priority when
2106        // set, so it overrides every other prefill arm. Needs both operands int8:
2107        // the NVFP4 weights are requanted to int8 once (cached, see
2108        // `ensure_int8_weight`) and the BF16 activations are requanted every call
2109        // into the shared scratch (`ensure_int8_scratch`). LOSSY (perf gate, not
2110        // bit-identical) — the _2.5h IoU gate is the final arbiter.
2111        // PCND: explicit opt-in, default off = byte-for-byte prior behavior; the
2112        // arm is a no-op (and no buffers are built) unless the kernels are loaded.
2113        let int8_prefill =
2114            self.int8_faith2_k.0 != 0 && std::env::var_os("ATLAS_INT8_PREFILL").is_some();
2115        if int8_prefill {
2116            // Log-once latch (see `atlas_core::scope`). It holds no model-derived
2117            // value — the message is rebuilt from the arguments every call — so a
2118            // stale entry cannot produce a wrong answer, only a suppressed duplicate
2119            // line after a model swap. Scoping it would thread a logging concern
2120            // through the call path to prevent one repeated INFO line.
2121            if ctx.stats.once("log:ffn_int8_prefill") {
2122                tracing::info!(
2123                    "[atlas] ATLAS_INT8_PREFILL=1: dense-FFN prefill via int8_gemm_faith2 (W4A8 requant→int8 MMA, lossy ~0.99998 cosine)"
2124                );
2125            }
2126        }
2127        // NVFP4 W4A4 MMQ prefill (ATLAS_FFN_NVFP4_MMQ) — vendored llama Blackwell
2128        // block-scale FP4 MMA, gate/up ONLY (hybrid: down stays on the default t_m128
2129        // path — SiLU(gate)*up is heavy-tailed and accuracy-critical). SiLU models only
2130        // (the scale2 fold lives in the scaled SiLU-mul). Mutually exclusive with
2131        // ATLAS_FFN_MMQ (both use the shared ffn_act_q8 scratch); this arm wins.
2132        //
2133        // An installed LoRA adapter turns this arm OFF. The MMQ path leaves
2134        // gate_out/up_out holding UNSCALED products and folds each
2135        // projection's `weight_scale_2` later, inside the SiLU-mul kernel. A
2136        // LoRA delta is a true-valued quantity, so adding it to those buffers
2137        // would put it through a scale2 multiply that does not belong to it —
2138        // silently wrong output rather than a failure. Folding scale2 into the
2139        // delta instead would mean reproducing the quant layout's arithmetic
2140        // in the adapter path, which is a much larger commitment than the
2141        // ~8% prefill this arm is worth (measured 831 vs 767 tok/s at 8K).
2142        // Correctness first; making LoRA and MMQ coexist is its own change.
2143        let fp4mmq_prefill = self.nvfp4_mmq_nc_k.0 != 0
2144            && self.nvfp4_quant_act_k.0 != 0
2145            && self.nvfp4_silu_scaled_k.0 != 0
2146            && matches!(self.activation, FfnActivation::SiLU)
2147            && self.lora.is_none()
2148            && std::env::var_os("ATLAS_NO_FFN_NVFP4_MMQ").is_none();
2149        if fp4mmq_prefill {
2150            // Log-once latch (see `atlas_core::scope`). It holds no model-derived
2151            // value — the message is rebuilt from the arguments every call — so a
2152            // stale entry cannot produce a wrong answer, only a suppressed duplicate
2153            // line after a model swap. Scoping it would thread a logging concern
2154            // through the call path to prevent one repeated INFO line.
2155            if ctx.stats.once("log:ffn_fp4_mmq_prefill") {
2156                tracing::info!(
2157                    "[atlas] ATLAS_FFN_NVFP4_MMQ=1: dense-FFN gate/up prefill via vendored llama NVFP4 W4A4 MMQ (block-scale FP4 MMA, ~80 TFLOP/s vs t_m128 ~51)"
2158                );
2159            }
2160        }
2161        // Down-projection MMQ arm (DEFAULT ON; kill-switch ATLAS_NO_FFN_NVFP4_MMQ_DOWN=1): route down through
2162        // the same MMQ arm (t_m128 runs the narrow-N down at only ~34 TFLOP/s in-model).
2163        // Accuracy note: down W4A4 cosine 0.9961 (random) — better than the previously
2164        // coherence-validated all-W4A4 config (0.991) — but still the heavy-tailed
2165        // projection, so it stays a SEPARATE opt-in gate.
2166        let fp4mmq_down = fp4mmq_prefill
2167            && self.nvfp4_scale_k.0 != 0
2168            && std::env::var_os("ATLAS_NO_FFN_NVFP4_MMQ_DOWN").is_none();
2169        // HYBRID: route the accuracy-critical down_proj OFF Q4_K onto the near-lossless faith2
2170        // NVFP4 path (W4A8 requant, cos 0.99998). down=SiLU(gate)*up is heavy-tailed; Q4_K
2171        // superblock scaling clips it (BFCL `multiple` -4.0%; llama promotes only down→Q6_K for
2172        // this reason). gate/up stay on Q4_K. Default ON when MMQ active; ATLAS_FFN_MMQ_DOWN_Q4K=1
2173        // = lossy all-Q4_K (A/B only). Defined here (self-fields+env, no q4k_prefill var dep) so the
2174        // int8 scratch below can size for the hybrid down.
2175        let down_faith2 = self.q4k_mmq_nc_k.0 != 0
2176            && self.q4k_quant_act_k.0 != 0
2177            && self.q4k_quant_w_k.0 != 0
2178            && self.dequant_nvfp4_bf16_k.0 != 0
2179            && self.int8_faith2_k.0 != 0
2180            && self.requant_a_int8_k.0 != 0
2181            && !fp4mmq_prefill
2182            && std::env::var_os("ATLAS_FFN_MMQ").is_some()
2183            && std::env::var_os("ATLAS_FFN_MMQ_DOWN_Q4K").is_none();
2184        // Pre-allocate (or reuse) the activation-requant scratch once per call,
2185        // sized to the largest projection K (= max(h, inter)) so the per-GEMM
2186        // arms never trigger a mid-call grow/sync. NULL when the int8 path is off.
2187        // Shared, arena-owned activation-requant scratch (sized once for
2188        // max_batch_tokens × max(h, inter) in BufferSizes::from_config). Replaces
2189        // the former per-DenseFfnLayer grow-on-demand allocator that leaked
2190        // ~286MB × 64 layers on the MMQ prefill path.
2191        let (int8_a_i8, int8_a_scale) = if int8_prefill || down_faith2 {
2192            (ctx.buffers.ffn_act_a(), ctx.buffers.ffn_act_scale())
2193        } else {
2194            (DevicePtr::NULL, DevicePtr::NULL)
2195        };
2196        // W4A4 native-FP4 prefill (ATLAS_FP4_PREFILL) — HIGHEST priority. NVFP4 weights
2197        // used directly (no requant); BF16 activations quantized to NVFP4 each GEMM into
2198        // the shared scratch. Native FP4 tensor cores (sm_121a). Lossy (cos ~0.99 vs fp32).
2199        let fp4_prefill = self.w4a4_gemm_k.0 != 0
2200            && self.quantize_nvfp4_k.0 != 0
2201            && std::env::var_os("ATLAS_FP4_PREFILL").is_some();
2202        if fp4_prefill {
2203            // Log-once latch (see `atlas_core::scope`). It holds no model-derived
2204            // value — the message is rebuilt from the arguments every call — so a
2205            // stale entry cannot produce a wrong answer, only a suppressed duplicate
2206            // line after a model swap. Scoping it would thread a logging concern
2207            // through the call path to prevent one repeated INFO line.
2208            if ctx.stats.once("log:ffn_fp4_prefill") {
2209                tracing::info!(
2210                    "[atlas] ATLAS_FP4_PREFILL=1: dense-FFN prefill via w4a4_gemm (native FP4 MMA sm_121a, W4A4)"
2211                );
2212            }
2213        }
2214        // NVFP4 packed [m,K/2] + scale [m,K/16] both fit within the shared int8
2215        // buffers (a_i8 [m,K] ⊇ packed; a_scale [m,(K/32)*4] ⊇ scale). FP4-prefill
2216        // is a standalone A/B flag, never co-active with the int8/Q4_K down path.
2217        let (nvfp4_a_packed, nvfp4_a_scale) = if fp4_prefill {
2218            (ctx.buffers.ffn_act_a(), ctx.buffers.ffn_act_scale())
2219        } else {
2220            (DevicePtr::NULL, DevicePtr::NULL)
2221        };
2222        // Q4_K MMQ prefill (ATLAS_FFN_MMQ) — vendored llama Q4_K W4A8 GEMM. Highest priority
2223        // when enabled. Lossy (Q4_K weight format ≠ NVFP4); gate via BFCL before relying on it.
2224        let q4k_prefill = self.q4k_mmq_nc_k.0 != 0
2225            && self.q4k_quant_act_k.0 != 0
2226            && self.q4k_quant_w_k.0 != 0
2227            && self.dequant_nvfp4_bf16_k.0 != 0
2228            && !fp4mmq_prefill
2229            && std::env::var_os("ATLAS_FFN_MMQ").is_some();
2230        if q4k_prefill {
2231            // Log-once latch (see `atlas_core::scope`). It holds no model-derived
2232            // value — the message is rebuilt from the arguments every call — so a
2233            // stale entry cannot produce a wrong answer, only a suppressed duplicate
2234            // line after a model swap. Scoping it would thread a logging concern
2235            // through the call path to prevent one repeated INFO line.
2236            if ctx.stats.once("log:ffn_q4k_prefill") {
2237                tracing::info!(
2238                    "[atlas] ATLAS_FFN_MMQ=1: dense-FFN prefill via vendored llama Q4_K MMQ (W4A8, +25%/+10% gate·down vs faith2)"
2239                );
2240            }
2241        }
2242        let q4k_a = if q4k_prefill {
2243            ctx.buffers.ffn_act_q8()
2244        } else {
2245            DevicePtr::NULL
2246        };
2247        // FP4-MMQ y scratch: block_fp4_mmq activations, in the SAME shared arena buffer
2248        // (fp4_act_scratch_bytes ≤ q8_1_scratch_bytes; mutually exclusive with q4k_prefill).
2249        let fp4_y = if fp4mmq_prefill {
2250            ctx.buffers.ffn_act_q8()
2251        } else {
2252            DevicePtr::NULL
2253        };
2254        // A/B escape hatch (benchmark only): force the proven v1 BF16 kernel even
2255        // when v2 is loaded, so v1-vs-v2 prefill TTFT can be compared in one
2256        // binary. Default unset → prefer v2 (the faster, bit-identical variant).
2257        let use_v2 = self.w4a16_gemm_t_m128_bf16_v2_k.0 != 0
2258            && std::env::var_os("ATLAS_DISABLE_PREFILL_V2").is_none();
2259        let bf16_kernel = if use_v2 {
2260            self.w4a16_gemm_t_m128_bf16_v2_k
2261        } else {
2262            self.w4a16_gemm_t_m128_bf16_k
2263        };
2264        // Final gate: the flag is honored only when the SELECTED kernel is
2265        // loaded (v2 when preferred, else v1) — not v1's handle unconditionally.
2266        let bf16_tc_prefill = bf16_kernel.0 != 0 && bf16_tc_env;
2267
2268        macro_rules! w4_gemm {
2269            ($w:expr, $wt:expr, $cell:expr, $qcell:expr, $fp4cell:expr, $allow_fp4:expr, $in:expr, $out:expr, $n:expr, $k:expr, $allow_q4k:expr) => {
2270                match $wt {
2271                    // NVFP4 W4A4 MMQ prefill (ATLAS_FFN_NVFP4_MMQ) — HIGHEST priority.
2272                    // `$allow_fp4` = fp4mmq_prefill for gate/up, fp4mmq_down for down.
2273                    // Activation pre-quantized into `fp4_y` by the caller; the output is
2274                    // missing ×scale2, folded downstream (scaled SiLU-mul / scale_bf16).
2275                    _ if $allow_fp4 => {
2276                        let _ = $in;
2277                        let qw =
2278                            self.ensure_nvfp4_mmq_weight($fp4cell, ctx.gpu, $w, $n, $k, stream)?;
2279                        // Size the M tile to the batch when the batch is small and
2280                        // the small-tile entries are present. m must be <= mmq_x or
2281                        // grid.y>1 re-streams the weights per tile.
2282                        let (tk_nc, tk_wc, tile) = if m <= 16
2283                            && self.nvfp4_mmq16_nc_k.0 != 0
2284                            && mmq_small_tile_enabled()
2285                        {
2286                            (self.nvfp4_mmq16_nc_k, self.nvfp4_mmq16_wc_k, 16u32)
2287                        } else if m <= 32
2288                            && self.nvfp4_mmq32_nc_k.0 != 0
2289                            && mmq_small_tile_enabled()
2290                        {
2291                            (self.nvfp4_mmq32_nc_k, self.nvfp4_mmq32_wc_k, 32u32)
2292                        } else if m <= 64
2293                            && self.nvfp4_mmq64_nc_k.0 != 0
2294                            && mmq_small_tile_enabled()
2295                            && mmq_tile64_enabled()
2296                        {
2297                            (self.nvfp4_mmq64_nc_k, self.nvfp4_mmq64_wc_k, 64u32)
2298                        } else {
2299                            (self.nvfp4_mmq_nc_k, self.nvfp4_mmq_wc_k, 128u32)
2300                        };
2301                        ops::nvfp4_mmq_gemm_tiled(
2302                            ctx.gpu, tk_nc, tk_wc, tile, fp4_y, qw.w, $out, m, $n, $k, stream,
2303                        )?;
2304                    }
2305                    // Q4_K MMQ prefill (ATLAS_FFN_MMQ) — next priority, gated per-GEMM by
2306                    // `$allow_q4k` (false for down in the hybrid → falls to the faith2 arm).
2307                    // Activation `$in` is pre-quantized to q8_1 in `q4k_a` by the caller.
2308                    _ if q4k_prefill && $allow_q4k => {
2309                        let qw = self.ensure_q4k_weight($qcell, ctx.gpu, $w, $n, $k, stream)?;
2310                        ops::q4k_mmq_gemm(
2311                            ctx.gpu,
2312                            self.q4k_mmq_nc_k,
2313                            self.q4k_mmq_wc_k,
2314                            q4k_a,
2315                            qw.w_q4k,
2316                            $out,
2317                            m,
2318                            $n,
2319                            $k,
2320                            stream,
2321                        )?;
2322                    }
2323                    // W4A4 native-FP4 prefill (ATLAS_FP4_PREFILL) — HIGHEST priority.
2324                    // The activation is PRE-quantized into the NVFP4 scratch by the caller
2325                    // (`input` once for gate+up which share it; `gate_out` for down) — opt #1,
2326                    // avoids the redundant re-quant. This arm just runs w4a4_gemm against the
2327                    // native NVFP4 weight `$w` (no requant). sm_121a FP4 MMA.
2328                    _ if fp4_prefill => {
2329                        let _ = $in;
2330                        ops::w4a4_gemm(
2331                            ctx.gpu,
2332                            self.w4a4_gemm_k,
2333                            nvfp4_a_packed,
2334                            nvfp4_a_scale,
2335                            $w,
2336                            $out,
2337                            m,
2338                            $n,
2339                            $k,
2340                            stream,
2341                        )?;
2342                    }
2343                    // int8 W4A8 fast prefill (ATLAS_INT8_PREFILL) — next priority.
2344                    // Independent of `$wt`/the transposed copies: requant reads the
2345                    // non-transposed NVFP4 `$w` directly. Builds (once) + caches the
2346                    // int8 weight in `$cell`, then requant_a + faith2 via the shared
2347                    // scratch. Lossy (cosine ~0.99998). Also the HYBRID down path
2348                    // (down_faith2 && !$allow_q4k): down falls here instead of Q4_K.
2349                    _ if int8_prefill || (down_faith2 && !$allow_q4k) => {
2350                        let iw = self.ensure_int8_weight($cell, ctx.gpu, $w, $n, $k, stream)?;
2351                        // faith5 (ATLAS_INT8_FAITH5=1): int32 per-sb accumulation
2352                        // breaks the MMA→scale dependency chain. Same kernel signature
2353                        // + grid/block as faith2 — just a different KernelHandle.
2354                        let int8_kernel = if self.int8_faith5_k.0 != 0
2355                            && std::env::var_os("ATLAS_INT8_FAITH5").is_some()
2356                        {
2357                            self.int8_faith5_k
2358                        } else {
2359                            self.int8_faith2_k
2360                        };
2361                        ops::int8_gemm_faith2_prefill(
2362                            ctx.gpu,
2363                            int8_kernel,
2364                            self.requant_a_int8_k,
2365                            $in,
2366                            iw.w_i8,
2367                            iw.w_scale,
2368                            int8_a_i8,
2369                            int8_a_scale,
2370                            $out,
2371                            m,
2372                            $n,
2373                            $k,
2374                            stream,
2375                        )?;
2376                    }
2377                    // Lossless opt-in: BF16 128x128 tensor-core prefill (bit-equivalent
2378                    // to base `w4a16_gemm`). Preferred over the FP8 t_m128/v2 paths only
2379                    // when ATLAS_BF16_TC_PREFILL is set and the kernel is loaded. Within
2380                    // the lossless path, prefer the higher-occupancy v2 kernel (3 CTAs/SM,
2381                    // bit-identical to v1) when it is loaded; else the proven v1 kernel.
2382                    // Both go through the same launch helper (identical grid/block/args).
2383                    // FP8 M64 fast prefill (ATLAS_FP8_M64_PREFILL) — highest priority,
2384                    // M64 grid via the w4a16_gemm_n128 launcher.
2385                    Some(wt) if fp8_m64_prefill => ops::w4a16_gemm_n128(
2386                        ctx.gpu,
2387                        self.w4a16_gemm_t_k,
2388                        $in,
2389                        &wt,
2390                        $out,
2391                        m,
2392                        $n,
2393                        $k,
2394                        stream,
2395                    )?,
2396                    // v2's COMPILED signature carries a 9th param, `ldb`
2397                    // (transposed-B row stride; == N for the FFN twins, which
2398                    // are built unpadded). It MUST go through the `_ldb`
2399                    // launcher: the 8-arg helper leaves cuLaunchKernel reading
2400                    // one-past-the-end of the param array for `ldb` —
2401                    // CUDA_ERROR_INVALID_VALUE or a host SIGSEGV depending on
2402                    // the neighboring heap word. v1 takes exactly 8 params and
2403                    // stays on the 8-arg helper.
2404                    Some(wt) if bf16_tc_prefill && use_v2 => ops::w4a16_gemm_n128_m128_bf16_ldb(
2405                        ctx.gpu,
2406                        bf16_kernel,
2407                        $in,
2408                        &wt,
2409                        $out,
2410                        m,
2411                        $n,
2412                        $k,
2413                        $n,
2414                        stream,
2415                    )?,
2416                    Some(wt) if bf16_tc_prefill => ops::w4a16_gemm_n128_m128_bf16(
2417                        ctx.gpu,
2418                        bf16_kernel,
2419                        $in,
2420                        &wt,
2421                        $out,
2422                        m,
2423                        $n,
2424                        $k,
2425                        stream,
2426                    )?,
2427                    // Small-M routing (DFlash verify, M<=64): delegate to
2428                    // `w4a16_prefill_gemm`, which picks `w4a16_gemm_t` /
2429                    // `w4a16_gemm_t_k64` per the w4a16_m17_bench numbers and
2430                    // falls back to the same v2/m128 kernels below.
2431                    // ATLAS_FFN_SMALLM=0 disables. Sits after the opt-in
2432                    // quant arms so explicit MMQ/int8/FP8 experiments keep
2433                    // priority.
2434                    Some(wt) if m <= 64 => {
2435                        self.w4a16_prefill_gemm(ctx, $w, Some(&wt), $in, $out, m, $n, $k, stream)?
2436                    }
2437                    // Prefer v2 (8-warp) > t_m128 (4-warp) > scalar-tile base.
2438                    Some(wt) if self.w4a16_gemm_t_m128_v2_k.0 != 0 => ops::w4a16_gemm_n128_m128_v2(
2439                        ctx.gpu,
2440                        self.w4a16_gemm_t_m128_v2_k,
2441                        $in,
2442                        &wt,
2443                        $out,
2444                        m,
2445                        $n,
2446                        $k,
2447                        stream,
2448                    )?,
2449                    Some(wt) if self.w4a16_gemm_t_m128_k.0 != 0 => ops::w4a16_gemm_n128_m128(
2450                        ctx.gpu,
2451                        self.w4a16_gemm_t_m128_k,
2452                        $in,
2453                        &wt,
2454                        $out,
2455                        m,
2456                        $n,
2457                        $k,
2458                        stream,
2459                    )?,
2460                    _ => {
2461                        ops::w4a16_gemm(ctx.gpu, self.w4a16_gemm, $in, $w, $out, m, $n, $k, stream)?
2462                    }
2463                }
2464            };
2465        }
2466
2467        // W4A4 opt #1: quantize the gate/up SHARED input `[M, H]` to NVFP4 ONCE
2468        // (gate and up both read it) instead of per-GEMM. Reused by both arms below.
2469        if fp4_prefill {
2470            ops::quantize_bf16_to_nvfp4(
2471                ctx.gpu,
2472                self.quantize_nvfp4_k,
2473                input,
2474                nvfp4_a_packed,
2475                nvfp4_a_scale,
2476                m,
2477                h,
2478                stream,
2479            )?;
2480        }
2481        // Q4_K opt: quantize the gate/up SHARED input `[M, H]` to q8_1 ONCE (both read it).
2482        if q4k_prefill {
2483            ops::quantize_act_q8_1(ctx.gpu, self.q4k_quant_act_k, input, q4k_a, m, h, stream)?;
2484        }
2485        // FP4-MMQ: quantize the gate/up SHARED input `[M, H]` to block_fp4_mmq ONCE.
2486        if fp4mmq_prefill {
2487            ops::nvfp4_mmq_quantize_act(
2488                ctx.gpu,
2489                self.nvfp4_quant_act_k,
2490                input,
2491                fp4_y,
2492                m,
2493                h,
2494                stream,
2495            )?;
2496        }
2497        // Per-projection timers. `dense_total` localised 23.7 s of a 28.2 s
2498        // Gemma-4-31B prefill to this function; these say WHICH of the three
2499        // projections it is. Roofline for that shape: 3 x 5376 x 21504 x 4012 tok
2500        // x 60 layers = 167 TFLOP, so 23.7 s is ~7 TFLOP/s against a bf16
2501        // tensor-core peak two orders higher — a hypothesis these numbers test
2502        // rather than assume.
2503        // ★ PER-STEP, NOT CUMULATIVE. The first version of this timer measured
2504        // elapsed-since-one-start at each of the three call sites, so `up_proj`
2505        // included `gate_proj` and `down_proj` included both — and the summed
2506        // "total profiled" then exceeded the wall clock, which is the tell.
2507        macro_rules! ffn_step {
2508            ($label:expr, $t0:expr) => {
2509                if ctx.profile {
2510                    ctx.gpu.synchronize(stream)?;
2511                    tracing::info!(
2512                        "  FFN prefill [{}] N={}: {}µs",
2513                        $label,
2514                        num_tokens,
2515                        $t0.elapsed().as_micros()
2516                    );
2517                    #[allow(unused_assignments)]
2518                    {
2519                        $t0 = std::time::Instant::now();
2520                    }
2521                }
2522            };
2523        }
2524        #[allow(unused_mut, unused_assignments)]
2525        let mut t_ffn = std::time::Instant::now();
2526        // gate_proj GEMM: [M, H] → [M, inter]
2527        w4_gemm!(
2528            &self.weights.gate_proj,
2529            self.weights.gate_proj_t,
2530            &self.int8_gate,
2531            &self.q4k_gate,
2532            &self.fp4mmq_gate,
2533            fp4mmq_prefill,
2534            input,
2535            gate_out,
2536            inter,
2537            h,
2538            true
2539        );
2540        ffn_step!("gate_proj", t_ffn);
2541        // up_proj GEMM: [M, H] → [M, inter]
2542        w4_gemm!(
2543            &self.weights.up_proj,
2544            self.weights.up_proj_t,
2545            &self.int8_up,
2546            &self.q4k_up,
2547            &self.fp4mmq_up,
2548            fp4mmq_prefill,
2549            input,
2550            up_out,
2551            inter,
2552            h,
2553            true
2554        );
2555        ffn_step!("up_proj", t_ffn);
2556
2557        // LoRA gate/up deltas land here: the projections are complete and, with
2558        // the MMQ arm disabled above, gate_out/up_out hold true-valued BF16 —
2559        // so the delta adds in the same units it was trained in.
2560        self.apply_lora_gate_up(ctx, input, gate_out, up_out, m, stream)?;
2561        // activation(gate) * up for all M tokens (SiLU or GELU)
2562        let fused_down_quant = fp4mmq_down && self.nvfp4_silu_quant_k.0 != 0;
2563        if fused_down_quant {
2564            // Fused SiLU-mul + quantize straight into the down MMQ's y-format: the
2565            // [M, inter] bf16 intermediate is never written or re-read (that round-trip
2566            // is why the unfused down arm measured neutral). scale2 folds happen inside,
2567            // pre-clamp — identical math to the two-step path below.
2568            ops::nvfp4_silu_mul_quant(
2569                ctx.gpu,
2570                self.nvfp4_silu_quant_k,
2571                gate_out,
2572                up_out,
2573                fp4_y,
2574                self.weights.gate_proj.weight_scale_2,
2575                self.weights.up_proj.weight_scale_2,
2576                m,
2577                inter,
2578                stream,
2579            )?;
2580        } else if fp4mmq_prefill {
2581            // FP4-MMQ outputs are missing the per-tensor FP32 scale2 (the hardware MMA
2582            // applies only the per-16 e4m3 scales) — fold it here, before the nonlinearity.
2583            ops::nvfp4_silu_mul_scaled(
2584                ctx.gpu,
2585                self.nvfp4_silu_scaled_k,
2586                gate_out,
2587                up_out,
2588                gate_out,
2589                self.weights.gate_proj.weight_scale_2,
2590                self.weights.up_proj.weight_scale_2,
2591                m * inter,
2592                stream,
2593            )?;
2594        } else {
2595            ops::silu_mul(
2596                ctx.gpu,
2597                self.act_mul,
2598                gate_out,
2599                up_out,
2600                gate_out,
2601                m * inter,
2602                stream,
2603            )?;
2604        }
2605
2606        // W4A4 opt #1: quantize the down input (SiLU(gate)*up, `[M, inter]`) to NVFP4.
2607        if fp4_prefill {
2608            ops::quantize_bf16_to_nvfp4(
2609                ctx.gpu,
2610                self.quantize_nvfp4_k,
2611                gate_out,
2612                nvfp4_a_packed,
2613                nvfp4_a_scale,
2614                m,
2615                inter,
2616                stream,
2617            )?;
2618        }
2619        // Q4_K opt: quantize the down input (SiLU(gate)*up, `[M, inter]`) to q8_1.
2620        // Skip when the hybrid routes down to faith2 (it does its own int8 requant).
2621        if q4k_prefill && !down_faith2 {
2622            ops::quantize_act_q8_1(
2623                ctx.gpu,
2624                self.q4k_quant_act_k,
2625                gate_out,
2626                q4k_a,
2627                m,
2628                inter,
2629                stream,
2630            )?;
2631        }
2632        // FP4-MMQ down (two-step fallback, only when the fused kernel is absent):
2633        // quantize the down input (SiLU(gate)*up, `[M, inter]`) to block_fp4_mmq.
2634        if fp4mmq_down && !fused_down_quant {
2635            ops::nvfp4_mmq_quantize_act(
2636                ctx.gpu,
2637                self.nvfp4_quant_act_k,
2638                gate_out,
2639                fp4_y,
2640                m,
2641                inter,
2642                stream,
2643            )?;
2644        }
2645        // down_proj GEMM: [M, inter] → [M, H]
2646        // ($fp4cell is a placeholder — the FP4-MMQ arm is gated off by `false` below;
2647        // down stays on the default path in the FP4-MMQ hybrid.)
2648        let output = ctx.buffers.moe_output();
2649        w4_gemm!(
2650            &self.weights.down_proj,
2651            self.weights.down_proj_t,
2652            &self.int8_down,
2653            &self.q4k_down,
2654            &self.fp4mmq_down,
2655            fp4mmq_down,
2656            gate_out,
2657            output,
2658            h,
2659            inter,
2660            false
2661        );
2662        ffn_step!("down_proj", t_ffn);
2663        // FP4-MMQ down: fold the down-projection's per-tensor scale2 (no SiLU-mul here;
2664        // the consumer is the residual add).
2665        if fp4mmq_down {
2666            ops::nvfp4_scale_bf16(
2667                ctx.gpu,
2668                self.nvfp4_scale_k,
2669                output,
2670                self.weights.down_proj.weight_scale_2,
2671                m * h,
2672                stream,
2673            )?;
2674        }
2675        // AFTER the scale2 fold, not before: that fold scales the base
2676        // projection's output, and the delta is not part of that product.
2677        // `gate_out` holds silu(gate)*up — the activation the base down GEMM
2678        // just contracted over — because the fused silu+quant arm is off
2679        // whenever an adapter is installed.
2680        self.apply_lora_down(ctx, gate_out, output, m, stream)?;
2681
2682        Ok(())
2683    }
2684
2685    /// Batched forward (per-token loop). Used by forward_batched in model loop.
2686    pub fn forward_batched(
2687        &self,
2688        input: DevicePtr,
2689        num_tokens: usize,
2690        ctx: &ForwardContext,
2691        stream: u64,
2692    ) -> Result<()> {
2693        self.forward_prefill(input, num_tokens, ctx, stream)
2694    }
2695}
2696
2697/// Native BF16/FP8 overlays take precedence over any NVFP4 fallback weights.
2698/// Small batches must use the same format-aware dispatcher as prefill.
2699fn native_small_batch_uses_prefill(has_bf16: bool, has_fp8: bool) -> bool {
2700    has_bf16 || has_fp8
2701}
2702
2703#[cfg(test)]
2704#[path = "dense_ffn_mmq_tests.rs"]
2705mod mmq_tests;
2706
2707#[cfg(test)]
2708#[path = "dense_ffn_native_batch_tests.rs"]
2709mod native_batch_tests;
2710
2711#[cfg(test)]
2712#[path = "dense_ffn_kernel_tests.rs"]
2713mod kernel_tests;
2714
2715#[cfg(test)]
2716mod tests {
2717    use super::native_small_batch_uses_prefill;
2718
2719    #[test]
2720    fn native_weight_presence_requires_prefill_dispatch() {
2721        assert!(native_small_batch_uses_prefill(true, false));
2722        assert!(native_small_batch_uses_prefill(false, true));
2723        assert!(native_small_batch_uses_prefill(true, true));
2724        assert!(!native_small_batch_uses_prefill(false, false));
2725    }
2726}