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 (`AVAROK_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 `AVAROK_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/// `AVAROK_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 `AVAROK_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    // `AVAROK_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    // AVAROK_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 AVAROK_FP8_M64_PREFILL, gated on
146    // quality. KernelHandle(0) on miss → dispatch unchanged.
147    w4a16_gemm_t_k: KernelHandle,
148    // int8 W4A8 prefill (AVAROK_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 AVAROK_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    // AVAROK_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 (AVAROK_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 (AVAROK_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 (AVAROK_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    /// MAX_M=16 sibling of `w8a16_gemv_batch4_k` — the 5..=32-row decode tier
243    /// (#927). KernelHandle(0) on a shadow that lacks the entry point, which
244    /// puts those widths back on the tile GEMMs. Rule: `batch16_decode.rs`.
245    w8a16_gemv_batch16_k: KernelHandle,
246    /// Whether that tier is ARMED — `AVAROK_FFN_BATCH16=1`, latched once here
247    /// at construction. DEFAULT FALSE: the tier measured a net LOSS in serving
248    /// on H100 (-5.4% aggregate at C=16, +50 ms TTFT) and has never been
249    /// measured on GB10, where the handle resolves just the same. Receipt and
250    /// the full rule: `batch16_decode.rs`.
251    ///
252    /// A FIELD rather than a call into `ffn_batch16_enabled()` at dispatch
253    /// time, for two reasons: the route is then fixed before any CUDA-graph
254    /// capture, and the dispatch tests can pin BOTH polarities without a
255    /// process-global `OnceLock` that latches on whichever test runs first.
256    batch16_enabled: bool,
257    /// Tensor-core 16-row-M-tile GEMM (#927) — the `AVAROK_FFN_M16_TC` tier
258    /// that sits AHEAD of `w8a16_gemv_batch16_k` at 5..=32 rows when the lever
259    /// is set. KernelHandle(0) on a shadow that lacks the entry point, which
260    /// leaves the ladder exactly as #927 shipped it. Rule: `dense_ffn_m16_tc.rs`.
261    w8a16_gemm_m16_k: KernelHandle,
262    /// The `N_TILE=64` twin (`AVAROK_FFN_M16_TC_NTILE=64`). KernelHandle(0) on a
263    /// shadow built before the wide arm existed, which silently keeps the
264    /// 32-wide kernel. Rule: `m16_tc::m16_tc_kernel`.
265    w8a16_gemm_m16_n64_k: KernelHandle,
266    /// `AVAROK_FFN_M16_TC` (or the `AVAROK_M16_TC` umbrella), cached at
267    /// construction. Since round 6 this lever reaches ONLY the FFN arm — the
268    /// attention tiers have their own, because the H100 measured them moving in
269    /// opposite directions. A FIELD rather than a per-call accessor for two
270    /// reasons: the selector runs per projection per layer per step, and the
271    /// dispatch tests drive both arms without racing the process-global
272    /// `OnceLock` the env read lives in. Grammar: `dense_ffn_m16_tc.rs`.
273    m16_tc: bool,
274    /// The CTA N width this layer asks for, 32 or 64. Same field-not-accessor
275    /// reasoning as `m16_tc`.
276    m16_tc_n_tile: u32,
277    w8a16_gemm_pipelined_k: KernelHandle,
278    // Fused FP8 decode GEMVs (gate+up in one launch / silu+down in one launch),
279    // mirroring the NVFP4 w4a16_gemv_dual / w4a16_gemv_silu_input. KernelHandle(0)
280    // on miss → fall back to the 3-launch w8a16_gemv path. Module = .cu file stem.
281    w8a16_gemv_dual_k: KernelHandle,
282    w8a16_gemv_silu_input_k: KernelHandle,
283    // Fast transposed FP8 prefill GEMM (128x128 / 8-warp / two-level FP32 fold).
284    // Preferred over w8a16_gemm when a transposed FP8 weight copy is present.
285    // KernelHandle(0) → fall back to non-transposed w8a16_gemm.
286    w8a16_gemm_t_m128_k: KernelHandle,
287    // W8A8 block-scaled prefill pair (#917/#928): per-token 1x128 FP8
288    // activation quant + the FP8xFP8 GEMM with both scale sets folded in an
289    // FP32 epilogue. Same two entry points the attention Q/K/V/O prefill
290    // already resolves, so no model shadow needs a new kernel; KernelHandle(0)
291    // on a shadow that lacks them -> the W8A16 branches still run.
292    // Dispatch rule + rationale live in `dense_ffn_w8a8_prefill.rs` (SSOT).
293    per_token_group_quant_fp8_k: ops::Fp8ActQuant,
294    fp8_gemm_t_blockscaled_k: KernelHandle,
295    // VEC128 activation-scale layout adapter for the cuBLASLt arm of the pair
296    // above: cuBLASLt reads those scales with the TOKEN index contiguous, the
297    // quantizer writes them K-group-contiguous. KernelHandle(0) -> the cuBLASLt
298    // arm is not selectable and the in-tree GEMM runs (SSOT for the rule:
299    // `dense_ffn_w8a8_prefill.rs::w8a8_gemm`).
300    fp8_act_scale_kmajor_k: KernelHandle,
301    /// The FUSED `[2*inter, hidden]` block-scaled FP8 gate+up weight (#927),
302    /// installed by `set_fp8_gate_up_fused`. `gate_proj` and `up_proj` above
303    /// are VIEWS inside it whenever it is `Some`, so every un-fused rung reads
304    /// the same bytes and no arm needs a second copy. `None` on every route
305    /// that did not build it — the arm is optional at runtime, not assumed.
306    fp8_gate_up_fused: Option<Fp8Weight>,
307    /// Whether the compiled target ARMS the fused arm (`[defaults]
308    /// ffn_gateup_fused`, overridable with `AVAROK_FFN_GATEUP_FUSED`), cached at
309    /// construction for the two reasons `batch16_tier` is: the selector runs
310    /// per layer per step, and the dispatch tests drive both polarities without
311    /// racing the process-global `OnceLock`.
312    gateup_fused: bool,
313    /// The strided SiLU·mul that reads the fused `[m, 2*inter]` output.
314    /// `silu_mul_strided.cu` is a HOPPER-OWNED source (`[kernels] overrides`),
315    /// so KernelHandle(0) on every other target — and the probe is GATED on the
316    /// resolved lever, like `w8a16_gemm_m16_k`, because an unresolved lookup
317    /// nothing declared fails the boot audit CLOSED.
318    silu_mul_strided_k: KernelHandle,
319    /// v0 LoRA overlay for gate/up/down. `set_lora_weights` REJECTS layers
320    /// where `fp8_weights`, `bf16_weights` or `q2_weights` are installed (v0
321    /// supports the NVFP4 dispatch path only — those branches early-return
322    /// before the NVFP4 tail where the deltas land; holo is NVFP4 so it is
323    /// unaffected).
324    ///
325    /// M1 (2026-08-19): the deltas are APPLIED. `apply_lora_gate_up` runs
326    /// after the gate/up projection and before `silu_mul`; `apply_lora_down`
327    /// runs after the down projection. Every NVFP4 dispatch this layer can
328    /// take — decode `forward`, `forward_k2`/`k3`/`km`, `forward_prefill`,
329    /// `forward_batched` — calls both, because an adapter that applies on one
330    /// path and not another produces a model that contradicts itself between
331    /// prefill and decode.
332    ///
333    /// Until M1 this field was written by `set_lora_weights` and never read,
334    /// so an adapter targeting gate/up/down loaded successfully and changed
335    /// nothing. On a hybrid like Qwen3.8-27B that is most of the adapter:
336    /// community LoRAs for it put 67-78% of their parameter mass in the FFN.
337    lora: Option<ops::lora_delta::LoraFfnWeights>,
338
339    /// Native keep-packed ternary Q2_0 dense MLP weights (`AVAROK_GGUF_NATIVE_Q2`).
340    /// When installed via `set_q2_weights`, decode dispatches `q2_0_gemv_vec`
341    /// (BF16 activation × packed 2-bit weight, dequant-in-dot-product) — the
342    /// weights stay 2-bit resident (no NVFP4 requant). Highest-priority forward
343    /// branch. Prefill/batched paths for packed-Q2 are a deferred (Tier-2) phase
344    /// and currently bail — dense qwen35 has no MTP so k2/k3 are never reached.
345    q2_weights: Option<DenseFfnWeightsQ2>,
346    q2_0_gemv_k: KernelHandle,
347    // Batched (M=1..8) packed-Q2 decode GEMV handle. The kernel + wrapper are
348    // built and validated (CPU math test), but the batched-decode call site
349    // (spec-decode verify rows) is deferred to the same Tier-2 phase as prefill;
350    // dense qwen35 has no MTP so no batched decode reaches the FFN today.
351    #[allow(dead_code)]
352    q2_0_gemv_batchm_k: KernelHandle,
353    // Load-time packed-Q2 → BF16 dequant kernel (`dequant_gguf_bf16` module).
354    // Used by packed-Q2 PREFILL: dequant each proj into a TRANSIENT BF16 scratch
355    // buffer, run the normal BF16 GEMM, free the scratch — the resident weight
356    // stays 2-bit. Decode uses the native `q2_0_gemv` (no dequant). Tier-1 path.
357    dequant_q2_0_gn_k: KernelHandle,
358    // Native Q2_0 MMQ prefill (Tier-2, `AVAROK_GGUF_NATIVE_Q2_MMQ=1`): keeps the
359    // 2-bit weight packed and runs a tensor-core int8 MMA (dequant-in-register)
360    // against a q8_1 activation — no BF16 weight scratch, no dequant tax, no race.
361    // The q8_1 activation quantizer is SHARED with Q4_K (`q4k_quant_act_k`).
362    // KernelHandle(0) when absent → falls back to the transient-dequant path.
363    q2_0_mmq_nc_k: KernelHandle,
364    q2_0_mmq_wc_k: KernelHandle,
365}
366
367/// M-sized MMQ tiles: **ON by default**, disabled by `AVAROK_NO_MMQ_SMALL_TILE=1`.
368/// Strict `== "1"` on an `AVAROK_NO_*` name — presence flags here are enabled by `=0`.
369fn mmq_small_tile_enabled() -> bool {
370    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
371    *ON.get_or_init(|| std::env::var("AVAROK_NO_MMQ_SMALL_TILE").as_deref() != Ok("1"))
372}
373
374/// The m=64 MMQ tile: **ON by default**, disabled by `AVAROK_NO_MMQ_TILE64=1`. Separate
375/// from `AVAROK_NO_MMQ_SMALL_TILE` so this arm can be A/B'd without also reverting the
376/// already-shipped 16/32 tiles. Strict `== "1"`, matching the sibling above.
377fn mmq_tile64_enabled() -> bool {
378    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
379    *ON.get_or_init(|| std::env::var("AVAROK_NO_MMQ_TILE64").as_deref() != Ok("1"))
380}
381impl DenseFfnLayer {
382    pub fn new(weights: DenseFfnWeights, gpu: &dyn GpuBackend) -> Result<Self> {
383        Self::new_with_activation(weights, FfnActivation::SiLU, gpu)
384    }
385
386    pub fn new_with_activation(
387        weights: DenseFfnWeights,
388        activation: FfnActivation,
389        gpu: &dyn GpuBackend,
390    ) -> Result<Self> {
391        let act_mul = match activation {
392            FfnActivation::SiLU => gpu.kernel("moe_silu_mul", "moe_silu_mul")?,
393            FfnActivation::GeLU => gpu.kernel("gelu", "gelu_mul")?,
394        };
395        // Resolved ONCE for the layer: it gates both the kernel probe below
396        // and the per-step selector, and those two must never disagree —
397        // a probe skipped while the selector says yes is a NULL launch.
398        // Rule: `dense_ffn_gateup_fused.rs`.
399        let gateup_fused = gateup_fused::ffn_gateup_fused();
400        // BF16 path kernels — optional (only loaded if available; gemma4
401        // is the only consumer today). `try_kernel` returns
402        // `KernelHandle(0)` on miss so we don't break NVFP4-only models
403        // that were built without these kernels. Module names per
404        // `kernels/gb10/{target}/nvfp4/KERNEL.toml`:
405        //   `dense_gemv_bf16 = "gemv"`, `dense_gemm_bf16 = "gemm"`.
406        let dense_gemv_bf16_k = super::try_kernel(gpu, "gemv", "dense_gemv_bf16");
407        let dense_gemm_bf16_k = super::try_kernel(gpu, "gemm", "dense_gemm_bf16");
408        let dense_gemm_tc_k = super::try_kernel(gpu, "gemm_tc", "dense_gemm_tc");
409
410        let layer = Self {
411            weights,
412            activation,
413            w4a16_gemv: gpu.kernel("w4a16_gemv", "w4a16_gemv")?,
414            w4a16_gemv_sw: super::try_kernel(gpu, "w4a16_gemv", "w4a16_gemv_sw"),
415            w4a16_gemv_dual: gpu.kernel("w4a16_gemv_fused", "w4a16_gemv_dual")?,
416            w4a16_gemv_silu_input: gpu.kernel("w4a16_gemv_fused", "w4a16_gemv_silu_input")?,
417            w4a16_gemv_dual_sw: super::try_kernel(gpu, "w4a16_gemv_fused", "w4a16_gemv_dual_sw"),
418            w4a16_gemv_silu_input_sw: super::try_kernel(
419                gpu,
420                "w4a16_gemv_fused",
421                "w4a16_gemv_silu_input_sw",
422            ),
423            w4a16_gemv_dual_batch2: gpu.kernel("w4a16_gemv", "w4a16_gemv_dual_batch2")?,
424            w4a16_gemv_dual_batch3: gpu.kernel("w4a16_gemv", "w4a16_gemv_dual_batch3")?,
425            w4a16_gemv_batch2: gpu.kernel("w4a16_gemv", "w4a16_gemv_batch2")?,
426            w4a16_gemv_batch3: gpu.kernel("w4a16_gemv", "w4a16_gemv_batch3")?,
427            w4a16_batchm: W4a16BatchmTiers::resolve(gpu),
428            w4a16_gemm: gpu.kernel("w4a16", "w4a16_gemm")?,
429            w4a16_gemm_t_m128_k: super::try_kernel(gpu, "w4a16", "w4a16_gemm_t_m128"),
430            w4a16_gemm_t_m128_v2_k: super::w4a16_v2_kernel(gpu),
431            w4a16_gemm_t_m128_bf16_k: super::try_kernel(gpu, "w4a16", "w4a16_gemm_t_m128_bf16"),
432            w4a16_gemm_t_m128_bf16_v2_k: super::try_kernel(
433                gpu,
434                "w4a16",
435                "w4a16_gemm_t_m128_bf16_v2",
436            ),
437            w4a16_gemm_t_k: super::tgemm_kernel(gpu),
438            int8_faith2_k: super::try_kernel(gpu, "w4a16", "int8_gemm_faith2"),
439            int8_faith5_k: super::try_kernel(gpu, "w4a16", "int8_gemm_i32acc"),
440            requant_w_int8_k: super::try_kernel(gpu, "w4a16", "requant_w_nvfp4_int8"),
441            requant_a_int8_k: super::try_kernel(gpu, "w4a16", "requant_a_bf16_int8"),
442            int8_gate: std::sync::OnceLock::new(),
443            int8_up: std::sync::OnceLock::new(),
444            int8_down: std::sync::OnceLock::new(),
445            w4a4_gemm_k: super::try_kernel(gpu, "w4a4", "w4a4_gemm"),
446            quantize_nvfp4_k: super::try_kernel(gpu, "quantize_nvfp4", "quantize_bf16_to_nvfp4"),
447            q4k_mmq_nc_k: super::try_kernel(gpu, "q4k_mmq", "avarok_q4k_mmq128_nc"),
448            q4k_mmq_wc_k: super::try_kernel(gpu, "q4k_mmq", "avarok_q4k_mmq128_wc"),
449            q4k_quant_act_k: super::try_kernel(gpu, "q4k_mmq", "avarok_q8_1_quantize_ds4_bf16"),
450            q4k_quant_w_k: super::try_kernel(gpu, "q4k_quantize", "q4k_quantize"),
451            dequant_nvfp4_bf16_k: super::try_kernel(
452                gpu,
453                "dequant_nvfp4_bf16",
454                "dequant_nvfp4_to_bf16",
455            ),
456            q4k_gate: std::sync::OnceLock::new(),
457            q4k_up: std::sync::OnceLock::new(),
458            q4k_down: std::sync::OnceLock::new(),
459            nvfp4_mmq_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_mmq128_nc"),
460            nvfp4_mmq_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_mmq128_wc"),
461            nvfp4_mmq16_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_mmq16_nc"),
462            nvfp4_mmq16_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_mmq16_wc"),
463            nvfp4_mmq32_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_mmq32_nc"),
464            nvfp4_mmq32_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_mmq32_wc"),
465            nvfp4_mmq64_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_mmq64_nc"),
466            nvfp4_mmq64_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_mmq64_wc"),
467            nvfp4_quant_act_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_quantize_bf16"),
468            nvfp4_repack_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_repack"),
469            nvfp4_silu_scaled_k: super::try_kernel(
470                gpu,
471                "nvfp4_mmq",
472                "avarok_nvfp4_silu_mul_scaled",
473            ),
474            nvfp4_silu_quant_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_silu_mul_quant"),
475            nvfp4_scale_k: super::try_kernel(gpu, "nvfp4_mmq", "avarok_nvfp4_scale_bf16"),
476            fp4mmq_gate: std::sync::OnceLock::new(),
477            fp4mmq_up: std::sync::OnceLock::new(),
478            fp4mmq_down: std::sync::OnceLock::new(),
479            w4a16_gemm_t_k64_k: super::k64_kernel(gpu).unwrap_or(KernelHandle(0)),
480            act_mul,
481            bf16_weights: None,
482            dense_gemv_bf16_k,
483            dense_gemm_bf16_k,
484            dense_gemm_tc_k,
485            fp8_weights: None,
486            w8a16_gemv_k: super::try_kernel(gpu, "w8a16_gemv", "w8a16_gemv"),
487            w8a16_gemm_k: super::try_kernel(gpu, "w8a16_gemm", "w8a16_gemm"),
488            w8a16_gemv_batch4_k: super::try_kernel(gpu, "w8a16_gemv_batch4", "w8a16_gemv_batch4"),
489            w8a16_gemv_batch16_k: super::try_kernel(gpu, "w8a16_gemv_batch4", "w8a16_gemv_batch16"),
490            batch16_enabled: batch16_decode::ffn_batch16_enabled(),
491            w8a16_gemm_m16_k: super::try_target_kernel(gpu, "w8a16_gemm_m16", "w8a16_gemm_m16"),
492            w8a16_gemm_m16_n64_k: super::try_target_kernel(
493                gpu,
494                "w8a16_gemm_m16",
495                "w8a16_gemm_m16_n64",
496            ),
497            m16_tc: m16_tc::m16_tc_levers().ffn,
498            m16_tc_n_tile: m16_tc::m16_tc_levers().ffn_n_tile,
499            w8a16_gemm_pipelined_k: super::try_kernel(
500                gpu,
501                "w8a16_gemm_pipelined",
502                "w8a16_gemm_pipelined",
503            ),
504            w8a16_gemv_dual_k: super::try_kernel(gpu, "w8a16_gemv_fused", "w8a16_gemv_dual"),
505            w8a16_gemv_silu_input_k: super::try_kernel(
506                gpu,
507                "w8a16_gemv_fused",
508                "w8a16_gemv_silu_input",
509            ),
510            w8a16_gemm_t_m128_k: super::try_kernel(gpu, "w8a16_gemm_t_m128", "w8a16_gemm_t_m128"),
511            per_token_group_quant_fp8_k: ops::Fp8ActQuant::resolve(gpu),
512            fp8_gemm_t_blockscaled_k: super::try_kernel(
513                gpu,
514                "fp8_gemm_t_blockscaled",
515                "fp8_gemm_t_blockscaled",
516            ),
517            fp8_act_scale_kmajor_k: super::try_kernel(
518                gpu,
519                "fp8_scale_transpose",
520                "fp8_act_scale_to_kmajor",
521            ),
522            fp8_gate_up_fused: None,
523            gateup_fused,
524            // ★ PROBED ONLY WHEN THE ARM IS ARMED: an unresolved lookup
525            // nothing declared fails the boot audit CLOSED, and
526            // `silu_mul_strided.cu` is HOPPER-OWNED — no other hardware tree
527            // carries it. Gating the lookup on the resolved lever is the fix
528            // `ptx_set.rs` prescribes.
529            silu_mul_strided_k: if gateup_fused {
530                super::try_target_kernel(gpu, "silu_mul_strided", "silu_mul_strided")
531            } else {
532                KernelHandle(0)
533            },
534            lora: None,
535            q2_weights: None,
536            // Winner of the decode-GEMV bench: candidate B (vectorized code loads
537            // + smem A-stage, 1 warp/row × 8 rows/CTA). ~268 GB/s (98% of the
538            // 273 GB/s LPDDR5X peak) at gate/up M=1 — 9.5× the original
539            // whole-block-strided `q2_0_gemv`. Same `(code-1)*d` FP32 numerics.
540            q2_0_gemv_k: super::try_kernel(gpu, "q2_0_gemv_vec", "q2_0_gemv_vec"),
541            q2_0_gemv_batchm_k: super::try_kernel(gpu, "q2_0_gemv_vec", "q2_0_gemv_vec_batchm"),
542            dequant_q2_0_gn_k: super::try_kernel(
543                gpu,
544                "dequant_gguf_bf16",
545                "dequant_q2_0_gn_to_bf16",
546            ),
547            // Resolved by `set_q2_weights`, never here: q2_0_mmq ships only
548            // in GGUF-serving targets, and an unconditional probe fails the
549            // boot audit on every dense-FFN model that never installs
550            // packed-Q2 weights.
551            q2_0_mmq_nc_k: KernelHandle(0),
552            q2_0_mmq_wc_k: KernelHandle(0),
553        };
554        Ok(layer)
555    }
556
557    /// Load-time finalize for the Q4_K MMQ prefill path (`AVAROK_FFN_MMQ`). MUST run at
558    /// load, BEFORE the KV cache is sized, so the net FFN footprint is correct when the KV
559    /// cache claims free memory. Order is critical: (1) eagerly materialize the Q4_K weights
560    /// (+9.63 GB) so they are accounted for now rather than lazily on first prefill (which
561    /// would over-subscribe AFTER the KV cache already grabbed the freed `_t` space → decode
562    /// OOM-throttle); (2) free the transposed `_proj_t` copies (−9.63 GB, dead under Q4_K
563    /// prefill — only the unreachable `Some(wt)` arms read them). Net FFN = baseline; decode
564    /// untouched (NVFP4 gemv on the non-`_t` copies). No-op unless Q4_K is active.
565    pub fn finalize_q4k_load(
566        &mut self,
567        gpu: &dyn GpuBackend,
568        h: u32,
569        inter: u32,
570        stream: u64,
571    ) -> Result<()> {
572        // Packed-Q2 (AVAROK_GGUF_NATIVE_Q2) FFN keeps its NVFP4 source weights
573        // NULL — the Q4_K prefill copy is built by dequant-ing those (NULL) NVFP4
574        // blocks, so running it here is a null-ptr kernel launch (CUDA 700).
575        // Packed-Q2 has its own prefill path (transient dequant), so skip.
576        if self.q2_weights.is_some() {
577            return Ok(());
578        }
579        // Native FP8 (#915): `forward_prefill_inner` returns from inside its
580        // `self.fp8_weights` arm long before the Q4_K/MMQ arm, so this repack
581        // is dead work — and since #915 the loader installs NULL NVFP4 source
582        // weights on that route, over which `ensure_q4k_weight`'s dequant is a
583        // CUDA-700 illegal access. Same reason as the packed-Q2 guard above.
584        if self.fp8_weights.is_some() {
585            return Ok(());
586        }
587        let q4k_active = self.q4k_mmq_nc_k.0 != 0
588            && self.q4k_quant_act_k.0 != 0
589            && self.q4k_quant_w_k.0 != 0
590            && self.dequant_nvfp4_bf16_k.0 != 0
591            && std::env::var_os("AVAROK_FFN_MMQ").is_some();
592        if !q4k_active {
593            return Ok(());
594        }
595        // (1) eagerly materialize the prefill weights BEFORE freeing `_t`, so the KV cache
596        // (sized after load) can't claim the freed space before the weights exist.
597        // gate/up: Q4_K (N=inter,K=h). down: HYBRID → int8 faith2 (N=h,K=inter) for accuracy,
598        // else Q4_K. ensure_int8_weight reads the non-`_t` NVFP4 down_proj (kept for decode gemv).
599        self.ensure_q4k_weight(
600            &self.q4k_gate,
601            gpu,
602            &self.weights.gate_proj,
603            inter,
604            h,
605            stream,
606        )?;
607        self.ensure_q4k_weight(&self.q4k_up, gpu, &self.weights.up_proj, inter, h, stream)?;
608        let down_faith2 = self.int8_faith2_k.0 != 0
609            && self.requant_a_int8_k.0 != 0
610            && std::env::var_os("AVAROK_FFN_MMQ_DOWN_Q4K").is_none();
611        if down_faith2 {
612            self.ensure_int8_weight(
613                &self.int8_down,
614                gpu,
615                &self.weights.down_proj,
616                h,
617                inter,
618                stream,
619            )?;
620        } else {
621            self.ensure_q4k_weight(
622                &self.q4k_down,
623                gpu,
624                &self.weights.down_proj,
625                h,
626                inter,
627                stream,
628            )?;
629        }
630        gpu.synchronize(stream)?;
631        // (2) free the dead transposed copies
632        let mut freed = 0usize;
633        for wt in [
634            &mut self.weights.gate_proj_t,
635            &mut self.weights.up_proj_t,
636            &mut self.weights.down_proj_t,
637        ] {
638            if let Some(w) = wt.as_ref()
639                && !w.weight.is_null()
640            {
641                gpu.free(w.weight)?;
642                gpu.free(w.weight_scale)?;
643                freed += 1;
644            }
645            *wt = None;
646        }
647        if freed > 0 {
648            // Log-once latch (see `avarok_core::scope`). It holds no model-derived
649            // value — the message is rebuilt from the arguments every call — so a
650            // stale entry cannot produce a wrong answer, only a suppressed duplicate
651            // line after a model swap. Scoping it would thread a logging concern
652            // through the call path to prevent one repeated INFO line.
653            // Latched on the BACKEND (`OpCache::once`), which exists by load
654            // time: `finalize_q4k_load` takes the `gpu` it is loading onto. A
655            // static meant only the first model in the process reported the
656            // decision.
657            if gpu.op_cache().once("log:ffn_mmq_freed_twins") {
658                tracing::info!(
659                    "[avarok] AVAROK_FFN_MMQ: freed transposed FFN `_t` copies (dead under Q4_K prefill) — Q4_K weights net to ~0 vs NVFP4 baseline"
660                );
661            }
662        }
663        Ok(())
664    }
665
666    /// Eagerly materialize the block_nvfp4 gate/up copies for the `AVAROK_FFN_NVFP4_MMQ`
667    /// W4A4 prefill arm at LOAD time (before KV sizing), then free the now-dead gate/up
668    /// transposed `_t` copies so net FFN footprint stays at the NVFP4 baseline. Down is
669    /// untouched (hybrid: it stays on the default t_m128 path for accuracy → keeps its
670    /// `_t` copy). No-op unless the env + kernels are present.
671    pub fn finalize_nvfp4_mmq_load(
672        &mut self,
673        gpu: &dyn GpuBackend,
674        h: u32,
675        inter: u32,
676        stream: u64,
677    ) -> Result<()> {
678        // Packed-Q2 (AVAROK_GGUF_NATIVE_Q2) FFN keeps its NVFP4 source weights
679        // NULL. This W4A4-MMQ finalize is active by DEFAULT (SiLU + kernels
680        // present) and repacks the NVFP4 gate/up — over NULL pointers that's a
681        // CUDA-700 illegal access. Packed-Q2 uses its own decode/prefill path.
682        if self.q2_weights.is_some() {
683            return Ok(());
684        }
685        // Native FP8 (#915): same reasoning as `finalize_q4k_load`, and this
686        // one is active by DEFAULT wherever the W4A4-MMQ kernels exist — so on
687        // a target that ships them it would repack NULL NVFP4 gate/up AND free
688        // `_t` copies for a prefill arm the FP8 early-return never reaches.
689        if self.fp8_weights.is_some() {
690            return Ok(());
691        }
692        let active = self.nvfp4_mmq_nc_k.0 != 0
693            && self.nvfp4_quant_act_k.0 != 0
694            && self.nvfp4_repack_k.0 != 0
695            && self.nvfp4_silu_scaled_k.0 != 0
696            && matches!(self.activation, FfnActivation::SiLU)
697            && std::env::var_os("AVAROK_NO_FFN_NVFP4_MMQ").is_none();
698        if !active {
699            return Ok(());
700        }
701        self.ensure_nvfp4_mmq_weight(
702            &self.fp4mmq_gate,
703            gpu,
704            &self.weights.gate_proj,
705            inter,
706            h,
707            stream,
708        )?;
709        self.ensure_nvfp4_mmq_weight(
710            &self.fp4mmq_up,
711            gpu,
712            &self.weights.up_proj,
713            inter,
714            h,
715            stream,
716        )?;
717        let down_mmq = std::env::var_os("AVAROK_NO_FFN_NVFP4_MMQ_DOWN").is_none();
718        if down_mmq {
719            self.ensure_nvfp4_mmq_weight(
720                &self.fp4mmq_down,
721                gpu,
722                &self.weights.down_proj,
723                h,
724                inter,
725                stream,
726            )?;
727        }
728        gpu.synchronize(stream)?;
729        // Free the dead transposed copies (prefill for those projections now runs on the
730        // MMQ arm; decode reads the non-transposed originals). down_proj_t is freed only
731        // when the down A/B gate is on.
732        let mut down_t = if down_mmq {
733            Some(&mut self.weights.down_proj_t)
734        } else {
735            None
736        };
737        let mut freed = 0usize;
738        for wt in [&mut self.weights.gate_proj_t, &mut self.weights.up_proj_t]
739            .into_iter()
740            .chain(down_t.take())
741        {
742            if let Some(w) = wt.as_ref()
743                && !w.weight.is_null()
744            {
745                gpu.free(w.weight)?;
746                gpu.free(w.weight_scale)?;
747                freed += 1;
748            }
749            *wt = None;
750        }
751        if freed > 0 {
752            // Log-once latch (see `avarok_core::scope`). It holds no model-derived
753            // value — the message is rebuilt from the arguments every call — so a
754            // stale entry cannot produce a wrong answer, only a suppressed duplicate
755            // line after a model swap. Scoping it would thread a logging concern
756            // through the call path to prevent one repeated INFO line.
757            // Latched on the BACKEND (`OpCache::once`), which exists by load
758            // time: `finalize_q4k_load` takes the `gpu` it is loading onto. A
759            // static meant only the first model in the process reported the
760            // decision.
761            if gpu.op_cache().once("log:ffn_fp4mmq_freed_twins") {
762                tracing::info!(
763                    "[avarok] AVAROK_FFN_NVFP4_MMQ: freed gate/up `_t` copies (dead under FP4-MMQ prefill) — block_nvfp4 copies net to ~0 vs NVFP4 baseline"
764                );
765            }
766        }
767        Ok(())
768    }
769
770    /// Ensure the block_nvfp4 copy of one NVFP4 projection exists (raw repack of the
771    /// checkpoint's packed E2M1 `[N, K/2]` + E4M3 `[N, K/16]` scales — zero numerics;
772    /// scale2 folded at the SiLU-mul). Cached in `cell` for process lifetime.
773    fn ensure_nvfp4_mmq_weight(
774        &self,
775        cell: &std::sync::OnceLock<Fp4MmqWeight>,
776        gpu: &dyn GpuBackend,
777        src: &QuantizedWeight,
778        n: u32,
779        k: u32,
780        stream: u64,
781    ) -> Result<Fp4MmqWeight> {
782        if let Some(w) = cell.get() {
783            return Ok(*w);
784        }
785        let w = gpu.alloc(ops::nvfp4_mmq_weight_bytes(n, k))?;
786        ops::nvfp4_mmq_repack(
787            gpu,
788            self.nvfp4_repack_k,
789            src.weight,
790            src.weight_scale,
791            w,
792            n,
793            k,
794            stream,
795        )?;
796        let built = Fp4MmqWeight { w };
797        if let Err(dup) = cell.set(built) {
798            gpu.synchronize(stream)?;
799            let _ = gpu.free(dup.w);
800        }
801        Ok(*cell.get().expect("fp4mmq weight cell set above"))
802    }
803
804    /// Install native block-scaled FP8 dense MLP weights. After this call the
805    /// forward paths dispatch `w8a16_gemv` (decode) / `w8a16_gemm` (prefill)
806    /// instead of w4a16 NVFP4. Caller must ensure those kernels are present in
807    /// the target (they are for the qwen3_5/ornith nvfp4 bundle).
808    pub fn set_fp8_weights(&mut self, gate: Fp8Weight, up: Fp8Weight, down: Fp8Weight) {
809        self.fp8_weights = Some(DenseFfnWeightsFp8 {
810            gate_proj: gate,
811            up_proj: up,
812            down_proj: down,
813        });
814    }
815
816    /// Install the FUSED `[2*inter, hidden]` block-scaled FP8 gate+up weight
817    /// (#927), enabling the one-GEMM decode arm.
818    ///
819    /// 🪤 CONTRACT, and the loader is the only caller: `gate` and `up` as
820    /// passed to [`Self::set_fp8_weights`] must ALREADY be views inside
821    /// `fused` (`gate.weight == fused.weight`, `up.weight ==
822    /// fused.weight + inter*hidden`, and the same for the scale grid). Passing
823    /// a fused weight built from DIFFERENT bytes than the two projections
824    /// would make the fused and un-fused rungs of one layer disagree, silently,
825    /// at whichever `m` moved the dispatch between them. Rule and residency
826    /// argument: `dense_ffn_gateup_fused.rs`.
827    pub fn set_fp8_gate_up_fused(&mut self, fused: Fp8Weight) {
828        debug_assert!(
829            self.fp8_weights
830                .as_ref()
831                .is_some_and(|w| w.gate_proj.weight == fused.weight),
832            "the fused gate+up weight must be the buffer gate_proj is a view into"
833        );
834        self.fp8_gate_up_fused = Some(fused);
835    }
836
837    /// Install the startup-static LoRA FFN overlay (gate/up/down deltas).
838    /// Hard-rejects when FP8/BF16 weight overlays are installed — those
839    /// decode branches early-return before the NVFP4 tail where the M1
840    /// delta insertions land, so a permissive install would silently skip
841    /// deltas. holo is NVFP4, so it is unaffected.
842    pub fn set_lora_weights(&mut self, w: ops::lora_delta::LoraFfnWeights) -> Result<()> {
843        anyhow::ensure!(
844            self.fp8_weights.is_none() && self.bf16_weights.is_none(),
845            "LoRA v0 supports only the NVFP4 dense-FFN path (FP8/BF16 weight \
846             overlays installed on this layer)"
847        );
848        // Packed-Q2 has its own gemv/batchm branches that early-return before
849        // the NVFP4 tail where the deltas land, exactly like FP8/BF16. Refusing
850        // here keeps the invariant the M1 apply relies on: if `self.lora` is
851        // Some, EVERY dispatch this layer can take applies it. A silently
852        // skipping path is worse than a refused load — it makes the adapter
853        // active in prefill and absent in decode, which reads as model
854        // weirdness rather than as a missing feature.
855        anyhow::ensure!(
856            self.q2_weights.is_none(),
857            "LoRA v0 supports only the NVFP4 dense-FFN path (packed-Q2 weights \
858             installed on this layer)"
859        );
860        // The decode down delta contracts over silu(gate)*up, which only the
861        // split-SiLU path materialises; `forward` pins that path whenever an
862        // adapter is installed. Refuse here if the layer cannot take it, so
863        // the pin is a guarantee rather than a hope.
864        anyhow::ensure!(
865            self.activation == FfnActivation::SiLU && self.act_mul.0 != 0 && self.w4a16_gemv.0 != 0,
866            "LoRA v0 needs the split-SiLU decode path (SiLU activation + \
867             act_mul + w4a16_gemv kernels); this layer resolved activation \
868             {:?}, act_mul={}, w4a16_gemv={}",
869            self.activation,
870            self.act_mul.0,
871            self.w4a16_gemv.0,
872        );
873        self.lora = Some(w);
874        Ok(())
875    }
876
877    /// M1 gate/up delta: `gate_out += ΔW_gate · x`, `up_out += ΔW_up · x`.
878    ///
879    /// Call AFTER the gate/up projection and BEFORE `silu_mul` — the deltas
880    /// belong to the projections, so they must land while gate/up are still
881    /// separate. Both buffers are the arena's dedicated `expert_gate_out` /
882    /// `expert_up_out` regions, contiguous with row stride `inter*2`, which is
883    /// what `apply_lora_delta`'s contiguity contract requires.
884    ///
885    /// No-op (and no launches) when the layer carries no adapter, so the
886    /// non-LoRA path stays byte-identical.
887    fn apply_lora_gate_up(
888        &self,
889        ctx: &ForwardContext,
890        input: DevicePtr,
891        gate_out: DevicePtr,
892        up_out: DevicePtr,
893        m: u32,
894        stream: u64,
895    ) -> Result<()> {
896        if ops::lora_delta::lora_no_ffn() {
897            return Ok(());
898        }
899        let Some(ref lw) = self.lora else {
900            return Ok(());
901        };
902        for (pair, base) in [(&lw.gate, gate_out), (&lw.up, up_out)] {
903            if let Some(pair) = pair.as_ref() {
904                ops::lora_delta::apply_lora_delta(
905                    ctx.gpu,
906                    &lw.kernels,
907                    pair,
908                    input,
909                    base,
910                    m,
911                    ctx.buffers.lora_xa(),
912                    ctx.buffers.lora_delta(),
913                    stream,
914                )?;
915            }
916        }
917        Ok(())
918    }
919
920    /// M1 down delta: `output += ΔW_down · act`.
921    ///
922    /// Call AFTER the down projection. `act` is the SiLU(gate)*up activation —
923    /// the same tensor the base down projection contracted over, NOT the
924    /// layer input. Every dense path leaves it in the `expert_gate_out`
925    /// region (silu_mul writes in place over gate), contiguous at row stride
926    /// `inter*2`.
927    fn apply_lora_down(
928        &self,
929        ctx: &ForwardContext,
930        act: DevicePtr,
931        output: DevicePtr,
932        m: u32,
933        stream: u64,
934    ) -> Result<()> {
935        if ops::lora_delta::lora_no_ffn() {
936            return Ok(());
937        }
938        let Some(ref lw) = self.lora else {
939            return Ok(());
940        };
941        let Some(ref pair) = lw.down else {
942            return Ok(());
943        };
944        ops::lora_delta::apply_lora_delta(
945            ctx.gpu,
946            &lw.kernels,
947            pair,
948            act,
949            output,
950            m,
951            ctx.buffers.lora_xa(),
952            ctx.buffers.lora_delta(),
953            stream,
954        )
955    }
956
957    /// Install native keep-packed ternary Q2_0 dense MLP weights. After this
958    /// call, decode `forward` dispatches `q2_0_gemv` per projection (weights
959    /// stay 2-bit resident, no NVFP4 requant) as the highest-priority path.
960    /// Caller must ensure the `q2_0_gemv` kernel is present in the target
961    /// (checked at forward time; falls through to a clear error otherwise).
962    /// Prefill for packed-Q2 is a deferred phase — see `forward_prefill`.
963    pub fn set_q2_weights(
964        &mut self,
965        gate: PackedQ2Weight,
966        up: PackedQ2Weight,
967        down: PackedQ2Weight,
968        gpu: &dyn GpuBackend,
969    ) {
970        self.q2_weights = Some(DenseFfnWeightsQ2 {
971            gate_proj: gate,
972            up_proj: up,
973            down_proj: down,
974        });
975        // Resolved here, not in the constructor: these ship only in
976        // GGUF-serving targets and the boot audit fails closed on an
977        // unconditional probe everywhere else.
978        self.q2_0_mmq_nc_k = super::try_kernel(gpu, "q2_0_mmq", "avarok_q2_0_mmq128_nc");
979        self.q2_0_mmq_wc_k = super::try_kernel(gpu, "q2_0_mmq", "avarok_q2_0_mmq128_wc");
980    }
981
982    /// Install BF16 dense MLP weights. After this call, the forward paths
983    /// dispatch to the BF16 GEMV/GEMM kernels instead of w4a16. The
984    /// caller must ensure the BF16 kernels are loaded (see
985    /// `dense_gemv_bf16_k` / `dense_gemm_bf16_k` checks). Small-batch
986    /// paths reuse `forward_prefill` so they cannot enter NVFP4 kernels
987    /// with the null placeholder weights used by BF16-native layers.
988    pub fn set_bf16_weights(&mut self, gate: DenseWeight, up: DenseWeight, down: DenseWeight) {
989        self.bf16_weights = Some(DenseFfnWeightsBf16 {
990            gate_proj: gate,
991            up_proj: up,
992            down_proj: down,
993        });
994    }
995
996    /// Ensure the int8 W4A8 copy of one NVFP4 projection weight exists, building
997    /// it once via `requant_w_nvfp4_int8` and caching it in `cell`. Reads the
998    /// NON-transposed NVFP4 layout (`weight` = packed E2M1 `[N, K/2]`,
999    /// `weight_scale` = per-16 E4M3 `[N, K/16]`, `weight_scale_2` = per-tensor
1000    /// F32) — so it is independent of the `*_proj_t` transposed copies. The
1001    /// requant launches on `stream`; the subsequent faith2 read is stream-ordered
1002    /// after it, so no host sync is needed.
1003    fn ensure_int8_weight(
1004        &self,
1005        cell: &std::sync::OnceLock<Int8Weight>,
1006        gpu: &dyn GpuBackend,
1007        src: &QuantizedWeight,
1008        n: u32,
1009        k: u32,
1010        stream: u64,
1011    ) -> Result<Int8Weight> {
1012        if let Some(w) = cell.get() {
1013            return Ok(*w);
1014        }
1015        let (nn, kk) = (n as usize, k as usize);
1016        let w_i8 = gpu.alloc(nn * kk)?; // [N, K] int8
1017        let w_scale = gpu.alloc(nn * (kk / 32) * 4)?; // [N, K/32] F32
1018        ops::requant_w_nvfp4_int8(
1019            gpu,
1020            self.requant_w_int8_k,
1021            src.weight,
1022            src.weight_scale,
1023            src.weight_scale_2,
1024            w_i8,
1025            w_scale,
1026            n,
1027            k,
1028            stream,
1029        )?;
1030        let built = Int8Weight { w_i8, w_scale };
1031        // Lost a race (another thread built first): free our duplicate buffers.
1032        if let Err(dup) = cell.set(built) {
1033            let _ = gpu.free(dup.w_i8);
1034            let _ = gpu.free(dup.w_scale);
1035        }
1036        Ok(*cell.get().expect("int8 weight cell set above"))
1037    }
1038
1039    /// Lazily materialize a Q4_K FFN weight from the NVFP4 source: dequant NVFP4→bf16
1040    /// (transient buffer, freed) then quantize bf16→GGML block_q4_K (cached for the
1041    /// process lifetime). `src` is the non-transposed NVFP4 weight `[n, k]`.
1042    fn ensure_q4k_weight(
1043        &self,
1044        cell: &std::sync::OnceLock<Q4kWeight>,
1045        gpu: &dyn GpuBackend,
1046        src: &QuantizedWeight,
1047        n: u32,
1048        k: u32,
1049        stream: u64,
1050    ) -> Result<Q4kWeight> {
1051        if let Some(w) = cell.get() {
1052            return Ok(*w);
1053        }
1054        // transient bf16 [n, k] (freed after quantize); persistent Q4_K bytes.
1055        let bf16_tmp = gpu.alloc((n as usize) * (k as usize) * 2)?;
1056        ops::dequant_nvfp4_to_bf16(
1057            gpu,
1058            self.dequant_nvfp4_bf16_k,
1059            src.weight,
1060            src.weight_scale,
1061            bf16_tmp,
1062            src.weight_scale_2,
1063            n,
1064            k,
1065            stream,
1066        )?;
1067        let w_q4k = gpu.alloc(ops::q4k_weight_bytes(n, k))?;
1068        ops::quantize_weight_q4k(gpu, self.q4k_quant_w_k, bf16_tmp, w_q4k, n, k, stream)?;
1069        // bf16_tmp consumed by the quantize on `stream`; sync before freeing it.
1070        gpu.synchronize(stream)?;
1071        let _ = gpu.free(bf16_tmp);
1072        let built = Q4kWeight { w_q4k };
1073        if let Err(dup) = cell.set(built) {
1074            let _ = gpu.free(dup.w_q4k);
1075        }
1076        Ok(*cell.get().expect("q4k weight cell set above"))
1077    }
1078
1079    /// Single-token decode: 2-3 kernel launches depending on activation.
1080    /// SiLU: dual GEMV + SiLU-fused down GEMV (2 launches).
1081    /// GELU: dual GEMV + gelu_mul + down GEMV (3 launches, no fused GELU down kernel).
1082    pub fn forward(
1083        &self,
1084        input: DevicePtr,
1085        ctx: &ForwardContext,
1086        stream: u64,
1087    ) -> Result<DevicePtr> {
1088        let h = ctx.config.hidden_size as u32;
1089        let inter = ctx.config.intermediate_size as u32;
1090
1091        let gate_out = ctx.buffers.expert_gate_out();
1092        let up_out = ctx.buffers.expert_up_out();
1093
1094        // Native keep-packed Q2_0 dispatch (highest priority). Per-projection
1095        // `q2_0_gemv`: BF16 activation × packed 2-bit weight, dequant in the
1096        // dot-product — weights never expand to BF16/NVFP4. No fused dual/silu
1097        // kernel yet, so this is gate + up + silu_mul + down (4 launches),
1098        // mirroring the FP8 non-fused fallback. SiLU only (Ternary-Bonsai is a
1099        // Qwen-family SwiGLU); GeLU packed-Q2 is a follow-up.
1100        if let Some(ref q2w) = self.q2_weights {
1101            if self.q2_0_gemv_k.0 == 0 {
1102                anyhow::bail!(
1103                    "q2_0_gemv kernel missing in this target build — packed-Q2 decode \
1104                     (AVAROK_GGUF_NATIVE_Q2) is unavailable"
1105                );
1106            }
1107            if self.activation != FfnActivation::SiLU {
1108                anyhow::bail!(
1109                    "packed-Q2 FFN decode supports SiLU only (got {:?})",
1110                    self.activation
1111                );
1112            }
1113            let output = ctx.buffers.moe_output();
1114            ops::q2_0_gemv_vec(
1115                ctx.gpu,
1116                self.q2_0_gemv_k,
1117                input,
1118                &q2w.gate_proj,
1119                gate_out,
1120                stream,
1121            )?;
1122            ops::q2_0_gemv_vec(
1123                ctx.gpu,
1124                self.q2_0_gemv_k,
1125                input,
1126                &q2w.up_proj,
1127                up_out,
1128                stream,
1129            )?;
1130            ops::silu_mul(
1131                ctx.gpu,
1132                self.act_mul,
1133                gate_out,
1134                up_out,
1135                gate_out,
1136                inter,
1137                stream,
1138            )?;
1139            ops::q2_0_gemv_vec(
1140                ctx.gpu,
1141                self.q2_0_gemv_k,
1142                gate_out,
1143                &q2w.down_proj,
1144                output,
1145                stream,
1146            )?;
1147            return Ok(output);
1148        }
1149
1150        // FP8 dispatch: the fused FP8 dual-GEMV (gate+up in one launch) then the
1151        // down projection, mirroring the NVFP4 path. Falls back to the 4-launch
1152        // per-projection `w8a16_gemv` path when the fused kernels or a non-SiLU
1153        // activation make the fast path unavailable.
1154        //
1155        // SPLIT SiLU+down (DEFAULT; kill-switch AVAROK_NO_DECODE_SPLIT_SILU) —
1156        // #928. The NVFP4 arm below has staged `silu(gate)*up` once since the
1157        // ncu receipt quoted there; the FP8 arm never got the same treatment
1158        // and paid for it. `w8a16_gemv_silu_input` recomputes the SwiGLU PER
1159        // OUTPUT, not per block: each of the N/4 CTAs gives every one of its 4
1160        // outputs a 64-lane team that walks all of K, so the launch evaluates
1161        // N*K = 5,120 x 17,408 = 89.1 M silu(gate)*up — 5,120x the 17,408 the
1162        // token actually needs — and each one is an `__expf` plus a true FP32
1163        // divide (`--fmad=false`, no `-use_fast_math`, so `g/(1+e^-g)` is the
1164        // IEEE division sequence, not a reciprocal). nsys, 1xH100,
1165        // Qwen/Qwen3.8-27B-FP8, 2026-09-11 round 7, C=1 step 21.891 ms: 64
1166        // launches x 103.9 us = 6.65 ms/step = 30.4% of the step at 858 GB/s,
1167        // against the `w8a16_gemv_dual` that reads the SAME 89.1 MB of weights
1168        // per layer at 1,979 GB/s. Staging the activation once (one elementwise
1169        // launch over K, microseconds, and CUDA graphs amortise the launch)
1170        // leaves the down GEMV a pure weight-streaming kernel.
1171        //
1172        // NUMERICS, stated rather than implied: this is NOT bit-identical to
1173        // the fused kernel. `moe_silu_mul` rounds `g*(1/(1+e^-g))*u` to BF16
1174        // before the GEMV consumes it, where the fused kernel keeps
1175        // `(g/(1+e^-g))*u` in FP32 all the way into the dot product — a BF16
1176        // round plus a reciprocal-vs-divide difference on the activation. It
1177        // IS the numerics prefill runs, and the same trade the NVFP4 arm has
1178        // shipped by default; `AVAROK_NO_DECODE_SPLIT_SILU` restores the fused
1179        // kernel bit-for-bit.
1180        if let Some(ref fp8w) = self.fp8_weights {
1181            let output = ctx.buffers.moe_output();
1182            let arm = fp8_down::fp8_down_arm(
1183                self.activation == FfnActivation::SiLU,
1184                self.w8a16_gemv_dual_k.0 != 0,
1185                self.w8a16_gemv_silu_input_k.0 != 0,
1186                self.act_mul.0 != 0,
1187                self.w8a16_gemv_k.0 != 0,
1188                ctx.levers.decode_split_silu,
1189            );
1190            if arm != fp8_down::Fp8DownArm::PerProjection {
1191                ops::w8a16_gemv_dual(
1192                    ctx.gpu,
1193                    self.w8a16_gemv_dual_k,
1194                    input,
1195                    fp8w.gate_proj.weight,
1196                    fp8w.gate_proj.row_scale,
1197                    gate_out,
1198                    fp8w.up_proj.weight,
1199                    fp8w.up_proj.row_scale,
1200                    up_out,
1201                    inter,
1202                    h,
1203                    stream,
1204                )?;
1205                if arm == fp8_down::Fp8DownArm::SplitSilu {
1206                    ops::silu_mul(
1207                        ctx.gpu,
1208                        self.act_mul,
1209                        gate_out,
1210                        up_out,
1211                        gate_out,
1212                        inter,
1213                        stream,
1214                    )?;
1215                    ops::w8a16_gemv(
1216                        ctx.gpu,
1217                        self.w8a16_gemv_k,
1218                        gate_out,
1219                        fp8w.down_proj.weight,
1220                        fp8w.down_proj.row_scale,
1221                        output,
1222                        h,
1223                        inter,
1224                        stream,
1225                    )?;
1226                } else {
1227                    ops::w8a16_gemv_silu_input(
1228                        ctx.gpu,
1229                        self.w8a16_gemv_silu_input_k,
1230                        gate_out,
1231                        up_out,
1232                        fp8w.down_proj.weight,
1233                        fp8w.down_proj.row_scale,
1234                        output,
1235                        h,
1236                        inter,
1237                        stream,
1238                    )?;
1239                }
1240                return Ok(output);
1241            }
1242            ops::w8a16_gemv(
1243                ctx.gpu,
1244                self.w8a16_gemv_k,
1245                input,
1246                fp8w.gate_proj.weight,
1247                fp8w.gate_proj.row_scale,
1248                gate_out,
1249                inter,
1250                h,
1251                stream,
1252            )?;
1253            ops::w8a16_gemv(
1254                ctx.gpu,
1255                self.w8a16_gemv_k,
1256                input,
1257                fp8w.up_proj.weight,
1258                fp8w.up_proj.row_scale,
1259                up_out,
1260                inter,
1261                h,
1262                stream,
1263            )?;
1264            ops::silu_mul(
1265                ctx.gpu,
1266                self.act_mul,
1267                gate_out,
1268                up_out,
1269                gate_out,
1270                inter,
1271                stream,
1272            )?;
1273            ops::w8a16_gemv(
1274                ctx.gpu,
1275                self.w8a16_gemv_k,
1276                gate_out,
1277                fp8w.down_proj.weight,
1278                fp8w.down_proj.row_scale,
1279                output,
1280                h,
1281                inter,
1282                stream,
1283            )?;
1284            return Ok(output);
1285        }
1286
1287        // BF16 dispatch: per-projection GEMV via `dense_gemv_bf16`. We
1288        // don't have a fused dual-BF16-GEMV kernel today; two sequential
1289        // launches are still BF16-precision-correct and only ~10% slower
1290        // than the fused w4a16 path on Gemma-4-31B (the cost is dominated
1291        // by the bigger BF16 weight reads, not launch overhead).
1292        if let Some(ref bf16w) = self.bf16_weights {
1293            ops::dense_gemv(
1294                ctx.gpu,
1295                self.dense_gemv_bf16_k,
1296                input,
1297                &bf16w.gate_proj,
1298                gate_out,
1299                inter,
1300                h,
1301                stream,
1302            )?;
1303            ops::dense_gemv(
1304                ctx.gpu,
1305                self.dense_gemv_bf16_k,
1306                input,
1307                &bf16w.up_proj,
1308                up_out,
1309                inter,
1310                h,
1311                stream,
1312            )?;
1313            ops::silu_mul(
1314                ctx.gpu,
1315                self.act_mul,
1316                gate_out,
1317                up_out,
1318                gate_out,
1319                inter,
1320                stream,
1321            )?;
1322            let output = ctx.buffers.moe_output();
1323            ops::dense_gemv(
1324                ctx.gpu,
1325                self.dense_gemv_bf16_k,
1326                gate_out,
1327                &bf16w.down_proj,
1328                output,
1329                h,
1330                inter,
1331                stream,
1332            )?;
1333            return Ok(output);
1334        }
1335
1336        // AVAROK_DECODE_FFN_VIA_GEMM=1: route decode's M=1 FFN projections
1337        // through the SAME transposed-weight GEMM kernels the DFlash verify
1338        // path uses (`w4a16_prefill_gemm` → w4a16_gemm_t / _t_k64), instead
1339        // of the dedicated GEMV kernels. Purpose: bit-identical FFN numerics
1340        // between serial decode and batched verify — the batch-K vs batch-1
1341        // divergence #218's bisect isolated ("FFN non-associativity") and the
1342        // root cause of the T=0 spec trajectory flips (2026-07-07 session).
1343        // Split SiLU staging already matches prefill SiLU numerics (swiglu
1344        // clamp), so with this arm the whole FFN block is kernel-identical to
1345        // a verify row. Requires the *_proj_t transposed copies (the NVFP4-MMQ
1346        // prefill arm FREES them — disable it if the warn below fires).
1347        // The `OnceLock<bool>` static that lived here is now a field on
1348        // `layers::ops::ModelLevers` — resolved when the model is built and carried
1349        // on `ForwardContext`, because a static outlives the model whose flags it
1350        // encodes.
1351        if ctx.levers.decode_ffn_via_gemm
1352            && self.activation == FfnActivation::SiLU
1353            && self.act_mul.0 != 0
1354        {
1355            let wt_alive =
1356                |w: &Option<QuantizedWeight>| w.as_ref().is_some_and(|w| !w.weight.is_null());
1357            if wt_alive(&self.weights.gate_proj_t) && wt_alive(&self.weights.up_proj_t) {
1358                // Log-once latch (see `avarok_core::scope`). It holds no model-derived
1359                // value — the message is rebuilt from the arguments every call — so a
1360                // stale entry cannot produce a wrong answer, only a suppressed duplicate
1361                // line after a model swap. Scoping it would thread a logging concern
1362                // through the call path to prevent one repeated INFO line.
1363                if ctx.stats.once("log:decode_ffn_via_gemm") {
1364                    tracing::info!(
1365                        "decode FFN via verify GEMM path (AVAROK_DECODE_FFN_VIA_GEMM=1): \
1366                         gate/up/down through w4a16_prefill_gemm at M=1"
1367                    );
1368                }
1369                self.w4a16_prefill_gemm(
1370                    ctx,
1371                    &self.weights.gate_proj,
1372                    self.weights.gate_proj_t.as_ref(),
1373                    input,
1374                    gate_out,
1375                    1,
1376                    inter,
1377                    h,
1378                    stream,
1379                )?;
1380                self.w4a16_prefill_gemm(
1381                    ctx,
1382                    &self.weights.up_proj,
1383                    self.weights.up_proj_t.as_ref(),
1384                    input,
1385                    up_out,
1386                    1,
1387                    inter,
1388                    h,
1389                    stream,
1390                )?;
1391                ops::silu_mul(
1392                    ctx.gpu,
1393                    self.act_mul,
1394                    gate_out,
1395                    up_out,
1396                    gate_out,
1397                    inter,
1398                    stream,
1399                )?;
1400                let output = ctx.buffers.moe_output();
1401                self.w4a16_prefill_gemm(
1402                    ctx,
1403                    &self.weights.down_proj,
1404                    self.weights.down_proj_t.as_ref(),
1405                    gate_out,
1406                    output,
1407                    1,
1408                    h,
1409                    inter,
1410                    stream,
1411                )?;
1412                return Ok(output);
1413            }
1414            // Log-once latch (see `avarok_core::scope`). It holds no model-derived
1415            // value — the message is rebuilt from the arguments every call — so a
1416            // stale entry cannot produce a wrong answer, only a suppressed duplicate
1417            // line after a model swap. Scoping it would thread a logging concern
1418            // through the call path to prevent one repeated INFO line.
1419            if ctx.stats.once("log:decode_ffn_no_twins") {
1420                tracing::warn!(
1421                    "AVAROK_DECODE_FFN_VIA_GEMM=1 requested but transposed FFN copies \
1422                     are freed/absent (NVFP4-MMQ prefill arm?) — falling back to GEMV; \
1423                     the unification experiment is NOT active"
1424                );
1425            }
1426        }
1427
1428        // Fused gate_proj + up_proj: [1, H] → [1, inter] × 2.
1429        // Single-warp variant (lossless) when the lever is on and the kernel
1430        // resolved; otherwise the 64-thread kernel. Dual and silu-input SW
1431        // are independent — missing silu_input_sw must not skip dual_sw on
1432        // the default split-SiLU path.
1433        let use_dual_sw = ops::use_gemv_sw(ctx.levers.gemv_sw, self.w4a16_gemv_dual_sw);
1434        let use_silu_sw = ops::use_gemv_sw(ctx.levers.gemv_sw, self.w4a16_gemv_silu_input_sw);
1435        if use_dual_sw {
1436            ops::w4a16_gemv_dual_sw(
1437                ctx.gpu,
1438                self.w4a16_gemv_dual_sw,
1439                input,
1440                &self.weights.gate_proj,
1441                gate_out,
1442                &self.weights.up_proj,
1443                up_out,
1444                inter,
1445                h,
1446                stream,
1447            )?;
1448        } else {
1449            ops::w4a16_gemv_dual(
1450                ctx.gpu,
1451                self.w4a16_gemv_dual,
1452                input,
1453                &self.weights.gate_proj,
1454                gate_out,
1455                &self.weights.up_proj,
1456                up_out,
1457                inter,
1458                h,
1459                stream,
1460            )?;
1461        }
1462
1463        let output = ctx.buffers.moe_output();
1464        // Split SiLU+down (DEFAULT; kill-switch AVAROK_NO_DECODE_SPLIT_SILU): the fused
1465        // silu_input kernel recomputes the SiLU transcendentals per OUTPUT ROW (N/4
1466        // blocks × redundant __expf) and measures COMPUTE-bound — ncu: SM 57% vs
1467        // memory 23%, 186 GB/s vs the dual GEMV's 266. Staging silu(gate)*up once
1468        // (one elementwise launch, CUDA graphs amortize it) lets the down GEMV run
1469        // memory-bound like the dual. Also aligns decode with the prefill SiLU
1470        // numerics (swiglu clamp), which the fused kernel lacked.
1471        //
1472        // An installed LoRA adapter PINS this path. The fused `silu_input`
1473        // alternative never materialises silu(gate)*up — it consumes gate and
1474        // up straight into the down GEMV — and the down delta has to contract
1475        // over exactly that activation. Reproducing it into `lora_hact` just
1476        // to feed the delta would compute the SiLU twice for a path that is
1477        // already the default and already the numerically preferred one, so
1478        // the adapter pins it instead. `set_lora_weights` refuses an adapter
1479        // when this path is unavailable on the layer, which makes
1480        // `lora.is_some()` imply the three conditions above.
1481        let split_silu = self.activation == FfnActivation::SiLU
1482            && self.act_mul.0 != 0
1483            && self.w4a16_gemv.0 != 0
1484            && (ctx.levers.decode_split_silu || self.lora.is_some());
1485        if split_silu {
1486            self.apply_lora_gate_up(ctx, input, gate_out, up_out, 1, stream)?;
1487            ops::silu_mul(
1488                ctx.gpu,
1489                self.act_mul,
1490                gate_out,
1491                up_out,
1492                gate_out,
1493                inter,
1494                stream,
1495            )?;
1496            ops::w4a16_decode_gemv(
1497                ctx.gpu,
1498                self.w4a16_gemv,
1499                self.w4a16_gemv_sw,
1500                ctx.levers.gemv_sw,
1501                gate_out,
1502                &self.weights.down_proj,
1503                output,
1504                h,
1505                inter,
1506                stream,
1507            )?;
1508            self.apply_lora_down(ctx, gate_out, output, 1, stream)?;
1509            return Ok(output);
1510        }
1511        debug_assert!(
1512            self.lora.is_none(),
1513            "LoRA installed but decode took the fused silu_input path, which \
1514             never materialises the activation the down delta contracts over; \
1515             set_lora_weights is supposed to make this unreachable"
1516        );
1517        match self.activation {
1518            FfnActivation::SiLU => {
1519                // Fused SiLU(gate)*up + down_proj: [1, inter] → [1, H]
1520                if use_silu_sw {
1521                    ops::w4a16_gemv_silu_input_sw(
1522                        ctx.gpu,
1523                        self.w4a16_gemv_silu_input_sw,
1524                        gate_out,
1525                        up_out,
1526                        &self.weights.down_proj,
1527                        output,
1528                        h,
1529                        inter,
1530                        stream,
1531                    )?;
1532                } else {
1533                    ops::w4a16_gemv_silu_input(
1534                        ctx.gpu,
1535                        self.w4a16_gemv_silu_input,
1536                        gate_out,
1537                        up_out,
1538                        &self.weights.down_proj,
1539                        output,
1540                        h,
1541                        inter,
1542                        stream,
1543                    )?;
1544                }
1545            }
1546            FfnActivation::GeLU => {
1547                // GELU(gate)*up → gate_out, then down_proj GEMV
1548                ops::silu_mul(
1549                    ctx.gpu,
1550                    self.act_mul,
1551                    gate_out,
1552                    up_out,
1553                    gate_out,
1554                    inter,
1555                    stream,
1556                )?;
1557                ops::w4a16_decode_gemv(
1558                    ctx.gpu,
1559                    self.w4a16_gemv,
1560                    self.w4a16_gemv_sw,
1561                    ctx.levers.gemv_sw,
1562                    gate_out,
1563                    &self.weights.down_proj,
1564                    output,
1565                    h,
1566                    inter,
1567                    stream,
1568                )?;
1569            }
1570        }
1571
1572        Ok(output)
1573    }
1574
1575    /// Packed-Q2 batched decode FFN for `m` concurrent rows (`m >= 2`). Mirrors
1576    /// the single-token `forward` packed-Q2 arm — per-projection keep-packed
1577    /// `q2_0_gemv_vec_batchm` (BF16 `[m,·]` activation × 2-bit weight, dequant in
1578    /// the dot-product, no BF16/NVFP4 expansion), SiLU-mul between gate/up, then
1579    /// down — but with `m` activation rows staged per weight read. This is the
1580    /// correctness path for concurrent decode (C>=2): the NVFP4 `forward_k2/k3`
1581    /// GEMVs read the NULL NVFP4 fallback weights, so packed-Q2 must route here.
1582    /// The wrapper chunks internally for `m > 8`. SiLU only (Ternary-Bonsai is a
1583    /// SwiGLU); output lands in `moe_output` as `[m, h]` row-major.
1584    fn forward_km_q2(
1585        &self,
1586        q2w: &DenseFfnWeightsQ2,
1587        input: DevicePtr,
1588        ctx: &ForwardContext,
1589        m: u32,
1590        stream: u64,
1591    ) -> Result<()> {
1592        if self.q2_0_gemv_batchm_k.0 == 0 {
1593            anyhow::bail!(
1594                "q2_0_gemv_vec_batchm kernel missing in this target build — packed-Q2 \
1595                 batched decode (AVAROK_GGUF_NATIVE_Q2, C>=2) is unavailable"
1596            );
1597        }
1598        if self.activation != FfnActivation::SiLU {
1599            anyhow::bail!(
1600                "packed-Q2 FFN batched decode supports SiLU only (got {:?})",
1601                self.activation
1602            );
1603        }
1604        let inter = ctx.config.intermediate_size as u32;
1605        let gate_out = ctx.buffers.expert_gate_out();
1606        let up_out = ctx.buffers.expert_up_out();
1607        let batchm = |w: &PackedQ2Weight, inp: DevicePtr, out: DevicePtr| -> Result<()> {
1608            ops::q2_0_gemv_vec_batchm(ctx.gpu, self.q2_0_gemv_batchm_k, inp, w, out, m, stream)
1609        };
1610        batchm(&q2w.gate_proj, input, gate_out)?;
1611        batchm(&q2w.up_proj, input, up_out)?;
1612        ops::silu_mul(
1613            ctx.gpu,
1614            self.act_mul,
1615            gate_out,
1616            up_out,
1617            gate_out,
1618            m * inter,
1619            stream,
1620        )?;
1621        let output = ctx.buffers.moe_output();
1622        batchm(&q2w.down_proj, gate_out, output)?;
1623        Ok(())
1624    }
1625
1626    /// K=2 speculative: batched GEMV for 2 tokens.
1627    /// 3 launches: dual batch2 (gate+up) + silu_mul + batch2 (down).
1628    pub fn forward_k2(&self, input: DevicePtr, ctx: &ForwardContext, stream: u64) -> Result<()> {
1629        // Packed-Q2: NVFP4 fallback weights are NULL, so the NVFP4 batch2 GEMVs
1630        // below would fault. Route to the keep-packed batchm FFN (m=2).
1631        if let Some(ref q2w) = self.q2_weights {
1632            return self.forward_km_q2(q2w, input, ctx, 2, stream);
1633        }
1634        if native_small_batch_uses_prefill(self.bf16_weights.is_some(), self.fp8_weights.is_some())
1635        {
1636            return self.forward_prefill(input, 2, ctx, stream);
1637        }
1638
1639        let h = ctx.config.hidden_size as u32;
1640        let inter = ctx.config.intermediate_size as u32;
1641
1642        let gate_out = ctx.buffers.expert_gate_out();
1643        let up_out = ctx.buffers.expert_up_out();
1644
1645        // Fused gate+up for 2 tokens
1646        ops::w4a16_gemv_dual_batch2(
1647            ctx.gpu,
1648            self.w4a16_gemv_dual_batch2,
1649            input,
1650            &self.weights.gate_proj,
1651            gate_out,
1652            &self.weights.up_proj,
1653            up_out,
1654            inter,
1655            h,
1656            stream,
1657        )?;
1658        self.apply_lora_gate_up(ctx, input, gate_out, up_out, 2, stream)?;
1659        ops::silu_mul(
1660            ctx.gpu,
1661            self.act_mul,
1662            gate_out,
1663            up_out,
1664            gate_out,
1665            2 * inter,
1666            stream,
1667        )?;
1668        let output = ctx.buffers.moe_output();
1669        ops::w4a16_gemv_batch2(
1670            ctx.gpu,
1671            self.w4a16_gemv_batch2,
1672            gate_out,
1673            &self.weights.down_proj,
1674            output,
1675            h,
1676            inter,
1677            stream,
1678        )?;
1679        self.apply_lora_down(ctx, gate_out, output, 2, stream)?;
1680
1681        Ok(())
1682    }
1683
1684    /// K=3 speculative: batched GEMV for 3 tokens.
1685    /// 3 launches: dual batch3 (gate+up) + silu_mul + batch3 (down).
1686    pub fn forward_k3(&self, input: DevicePtr, ctx: &ForwardContext, stream: u64) -> Result<()> {
1687        // Packed-Q2: route to the keep-packed batchm FFN (m=3); NVFP4 weights null.
1688        if let Some(ref q2w) = self.q2_weights {
1689            return self.forward_km_q2(q2w, input, ctx, 3, stream);
1690        }
1691        if native_small_batch_uses_prefill(self.bf16_weights.is_some(), self.fp8_weights.is_some())
1692        {
1693            return self.forward_prefill(input, 3, ctx, stream);
1694        }
1695
1696        let h = ctx.config.hidden_size as u32;
1697        let inter = ctx.config.intermediate_size as u32;
1698
1699        let gate_out = ctx.buffers.expert_gate_out();
1700        let up_out = ctx.buffers.expert_up_out();
1701
1702        // Fused gate+up for 3 tokens
1703        ops::w4a16_gemv_dual_batch3(
1704            ctx.gpu,
1705            self.w4a16_gemv_dual_batch3,
1706            input,
1707            &self.weights.gate_proj,
1708            gate_out,
1709            &self.weights.up_proj,
1710            up_out,
1711            inter,
1712            h,
1713            stream,
1714        )?;
1715        self.apply_lora_gate_up(ctx, input, gate_out, up_out, 3, stream)?;
1716        ops::silu_mul(
1717            ctx.gpu,
1718            self.act_mul,
1719            gate_out,
1720            up_out,
1721            gate_out,
1722            3 * inter,
1723            stream,
1724        )?;
1725        let output = ctx.buffers.moe_output();
1726        ops::w4a16_gemv_batch3(
1727            ctx.gpu,
1728            self.w4a16_gemv_batch3,
1729            gate_out,
1730            &self.weights.down_proj,
1731            output,
1732            h,
1733            inter,
1734            stream,
1735        )?;
1736        self.apply_lora_down(ctx, gate_out, output, 3, stream)?;
1737
1738        Ok(())
1739    }
1740
1741    /// Batchm-GEMV kernel for `m` verify rows: the narrowest resolved tier in
1742    /// `w4a16_gemv_batch{4,5,6,7,8}` that covers `m`. 0-handle when out of
1743    /// range or absent. See `layers::w4a16_gemv_tiers` for the decision and
1744    /// the `AVAROK_NO_GEMV_EXACT_M_TIERS=1` kill switch.
1745    fn batchm_kernel(&self, m: u32) -> KernelHandle {
1746        self.w4a16_batchm.kernel(m)
1747    }
1748
1749    /// Whether the M-row batched-GEMV verify path is available for `m` rows
1750    /// (batchm kernel present AND NVFP4 weights loaded — the batchm GEMV
1751    /// reads the non-transposed NVFP4 layout).
1752    pub fn can_forward_km(&self, m: u32) -> bool {
1753        self.batchm_kernel(m).0 != 0
1754            && (!self.weights.gate_proj.weight.is_null()
1755                // Native FP8 (#915): the loader no longer builds the NVFP4
1756                // fallback, but `forward_km` redirects to `forward_prefill` for
1757                // an FP8 layer anyway (`native_small_batch_uses_prefill`), so
1758                // the answer must stay `true` or the n=4..8 verify arm in
1759                // `multi_seq/ffn.rs:145` would fall through to a DIFFERENT
1760                // branch and quietly change the routing this fix must not touch.
1761                || self.fp8_weights.is_some())
1762    }
1763
1764    /// K=m (m<=8) speculative verify: batched GEMV for m tokens.
1765    /// 4 launches: batchm gate + batchm up + silu_mul + batchm down — each
1766    /// projection weight is read ONCE for all m rows at near-peak stream
1767    /// bandwidth. nsys (2026-07-18, M=4): the `forward_prefill` MMQ arm this
1768    /// replaces for the K=4 verify cost 54.8 ms/step across the 64-layer
1769    /// dense FFN stack (~156 GB/s effective at M=4); the batch GEMV family
1770    /// measures ~290 GB/s on the same shapes (w8a16_gemv_batch4 sibling),
1771    /// putting this path at the ~31 ms weight-traffic floor. m=5..8 uses
1772    /// `w4a16_gemv_batch8` (batchm_bench: same weight-streaming bandwidth,
1773    /// removing the M>4 tile-GEMM cliff for chain-verify K=5..8).
1774    pub fn forward_km(
1775        &self,
1776        input: DevicePtr,
1777        m: u32,
1778        ctx: &ForwardContext,
1779        stream: u64,
1780    ) -> Result<()> {
1781        // As in k2/k3, concurrent decode must preserve an installed native
1782        // overlay even when valid, lower-precision NVFP4 fallbacks coexist.
1783        if native_small_batch_uses_prefill(self.bf16_weights.is_some(), self.fp8_weights.is_some())
1784        {
1785            return self.forward_prefill(input, m as usize, ctx, stream);
1786        }
1787        let h = ctx.config.hidden_size as u32;
1788        let inter = ctx.config.intermediate_size as u32;
1789        let kh = self.batchm_kernel(m);
1790
1791        let gate_out = ctx.buffers.expert_gate_out();
1792        let up_out = ctx.buffers.expert_up_out();
1793
1794        ops::w4a16_gemv_batchm(
1795            ctx.gpu,
1796            kh,
1797            input,
1798            &self.weights.gate_proj,
1799            gate_out,
1800            m,
1801            inter,
1802            h,
1803            stream,
1804        )?;
1805        ops::w4a16_gemv_batchm(
1806            ctx.gpu,
1807            kh,
1808            input,
1809            &self.weights.up_proj,
1810            up_out,
1811            m,
1812            inter,
1813            h,
1814            stream,
1815        )?;
1816        self.apply_lora_gate_up(ctx, input, gate_out, up_out, m, stream)?;
1817        ops::silu_mul(
1818            ctx.gpu,
1819            self.act_mul,
1820            gate_out,
1821            up_out,
1822            gate_out,
1823            m * inter,
1824            stream,
1825        )?;
1826        let output = ctx.buffers.moe_output();
1827        ops::w4a16_gemv_batchm(
1828            ctx.gpu,
1829            kh,
1830            gate_out,
1831            &self.weights.down_proj,
1832            output,
1833            m,
1834            h,
1835            inter,
1836            stream,
1837        )?;
1838        self.apply_lora_down(ctx, gate_out, output, m, stream)?;
1839
1840        Ok(())
1841    }
1842
1843    /// N-token prefill: GEMM for all projections.
1844    /// W4A16 prefill/verify GEMM dispatch, routed by (M, K) per
1845    /// w4a16_m17_bench measurements on GB10:
1846    ///   - M<=64 (DFlash verify M=17): the M64-tile `w4a16_gemm_t` beats the
1847    ///     M128-tile kernels (283 vs 324us on gate/up — 87% of an M128 tile
1848    ///     is padding at M=17), and `w4a16_gemm_t_k64` wins deep-K down_proj
1849    ///     (554 vs 810us at K=17408, where N/128 CTAs can't fill the GPU and
1850    ///     the halved K-loop matters).
1851    ///   - M>64 (real prefill): v2 (8-warp) > t_m128 (4-warp), unchanged.
1852    ///   - No transposed copy: base `w4a16_gemm` (9-12x the bandwidth floor —
1853    ///     last resort).
1854    ///
1855    /// Kill-switch: AVAROK_FFN_SMALLM=0 restores the m128-only dispatch for A/B.
1856    #[allow(clippy::too_many_arguments)]
1857    fn w4a16_prefill_gemm(
1858        &self,
1859        ctx: &ForwardContext,
1860        w: &QuantizedWeight,
1861        wt: Option<&QuantizedWeight>,
1862        input: DevicePtr,
1863        output: DevicePtr,
1864        m: u32,
1865        n: u32,
1866        k: u32,
1867        stream: u64,
1868    ) -> Result<()> {
1869        // The `OnceLock<bool>` static that lived here is now a field on
1870        // `layers::ops::ModelLevers` — resolved when the model is built and carried
1871        // on `ForwardContext`, because a static outlives the model whose flags it
1872        // encodes.
1873        if let Some(wt) = wt {
1874            if m <= 64 && k.is_multiple_of(32) && ctx.levers.ffn_small_m {
1875                if k >= crate::layers::w4a16_k64_min_k()
1876                    && k.is_multiple_of(64)
1877                    && self.w4a16_gemm_t_k64_k.0 != 0
1878                {
1879                    return ops::w4a16_gemm_n128(
1880                        ctx.gpu,
1881                        self.w4a16_gemm_t_k64_k,
1882                        input,
1883                        wt,
1884                        output,
1885                        m,
1886                        n,
1887                        k,
1888                        stream,
1889                    );
1890                }
1891                if self.w4a16_gemm_t_k.0 != 0 {
1892                    return ops::w4a16_gemm_n128(
1893                        ctx.gpu,
1894                        self.w4a16_gemm_t_k,
1895                        input,
1896                        wt,
1897                        output,
1898                        m,
1899                        n,
1900                        k,
1901                        stream,
1902                    );
1903                }
1904            }
1905            if self.w4a16_gemm_t_m128_v2_k.0 != 0 {
1906                return ops::w4a16_gemm_n128_m128_v2(
1907                    ctx.gpu,
1908                    self.w4a16_gemm_t_m128_v2_k,
1909                    input,
1910                    wt,
1911                    output,
1912                    m,
1913                    n,
1914                    k,
1915                    stream,
1916                );
1917            }
1918            if self.w4a16_gemm_t_m128_k.0 != 0 {
1919                return ops::w4a16_gemm_n128_m128(
1920                    ctx.gpu,
1921                    self.w4a16_gemm_t_m128_k,
1922                    input,
1923                    wt,
1924                    output,
1925                    m,
1926                    n,
1927                    k,
1928                    stream,
1929                );
1930            }
1931        }
1932        ops::w4a16_gemm(ctx.gpu, self.w4a16_gemm, input, w, output, m, n, k, stream)
1933    }
1934
1935    /// Timed wrapper around the dense-FFN prefill.
1936    ///
1937    /// ★ THIS PATH HAD NO TIMERS AT ALL, and that hid the largest unexplained
1938    /// number on the board. Profiling nvidia/Gemma-4-31B-IT-NVFP4 at a
1939    /// 4096-token prompt: wall 28,180 ms, while EVERY profiled phase across
1940    /// `ATTN prefill [...]` and `MoE prefill [...]` summed to 3,269.8 ms. 88% of
1941    /// the prefill was invisible — not attributed to something slow, simply not
1942    /// instrumented. `forward_prefill` dispatches ~20 quantization arms and none
1943    /// of them reported elapsed time; only one-shot "which arm was chosen" INFO
1944    /// lines existed.
1945    ///
1946    /// One coarse timer first, deliberately: it answers whether the missing time
1947    /// is here at all before anyone threads timers through twenty arms. Same
1948    /// `<AREA> prefill [phase] N=<n>: <us>µs` shape the attention and MoE paths
1949    /// already emit, so the existing log-summing one-liners pick it up unchanged.
1950    pub fn forward_prefill(
1951        &self,
1952        input: DevicePtr,
1953        num_tokens: usize,
1954        ctx: &ForwardContext,
1955        stream: u64,
1956    ) -> Result<()> {
1957        if !ctx.profile {
1958            return self.forward_prefill_inner(input, num_tokens, ctx, stream);
1959        }
1960        let t0 = std::time::Instant::now();
1961        let r = self.forward_prefill_inner(input, num_tokens, ctx, stream);
1962        // Sync so the figure is the kernel's, not the launch queue's — the
1963        // attention and MoE timers do the same under `ctx.profile`.
1964        ctx.gpu.synchronize(stream)?;
1965        tracing::info!(
1966            "  FFN prefill [dense_total] N={}: {}µs",
1967            num_tokens,
1968            t0.elapsed().as_micros()
1969        );
1970        r
1971    }
1972
1973    fn forward_prefill_inner(
1974        &self,
1975        input: DevicePtr,
1976        num_tokens: usize,
1977        ctx: &ForwardContext,
1978        stream: u64,
1979    ) -> Result<()> {
1980        let h = ctx.config.hidden_size as u32;
1981        let inter = ctx.config.intermediate_size as u32;
1982        let m = num_tokens as u32;
1983
1984        let gate_out = ctx.buffers.expert_gate_out();
1985        let up_out = ctx.buffers.expert_up_out();
1986
1987        // Native keep-packed Q2_0 prefill (Tier-1): the resident weight stays
1988        // 2-bit, but prefill has no packed-MMQ kernel yet (that's Tier-2). So we
1989        // dequant each projection into a TRANSIENT BF16 scratch `[N, K]` via the
1990        // load-time `dequant_q2_0_gn_to_bf16` kernel, run the normal BF16
1991        // prefill GEMM (tensor-core when present), then free the scratch. Only a
1992        // per-matmul scratch is BF16 — the WeightStore blocks stay 2-bit. Decode
1993        // still uses the native `q2_0_gemv` (no dequant). SiLU only.
1994        if let Some(ref q2w) = self.q2_weights {
1995            if self.activation != FfnActivation::SiLU {
1996                anyhow::bail!(
1997                    "packed-Q2 FFN prefill supports SiLU only (got {:?})",
1998                    self.activation
1999                );
2000            }
2001
2002            // Tier-2 native MMQ prefill (AVAROK_GGUF_NATIVE_Q2_MMQ=1): quantize the
2003            // activation to q8_1 ONCE per projection-input (gate/up share `input`;
2004            // down re-quantizes `gate_out`), then run the packed 2-bit MMQ GEMM —
2005            // no BF16 weight dequant, no shared `q2_dequant_scratch`. Requires the
2006            // MMQ kernel + the shared q8_1 quantizer + group-128 weights.
2007            let q2_mmq = self.q2_0_mmq_nc_k.0 != 0
2008                && self.q4k_quant_act_k.0 != 0
2009                && ops::native_q2_mmq_enabled()
2010                && q2w.gate_proj.group == 128
2011                && q2w.up_proj.group == 128
2012                && q2w.down_proj.group == 128;
2013            if q2_mmq {
2014                static Q2MMQ_LOG: std::sync::Once = std::sync::Once::new();
2015                Q2MMQ_LOG.call_once(|| {
2016                    eprintln!(
2017                        "[avarok] AVAROK_GGUF_NATIVE_Q2_MMQ=1: dense-FFN prefill via native packed Q2_0 MMQ (W2A8, keep-packed)"
2018                    );
2019                });
2020                let a_q8 = ctx.buffers.q2_act_q8();
2021                let mmq = |w: &PackedQ2Weight, out: DevicePtr| -> Result<()> {
2022                    ops::q2_0_mmq_gemm(
2023                        ctx.gpu,
2024                        self.q2_0_mmq_nc_k,
2025                        self.q2_0_mmq_wc_k,
2026                        a_q8,
2027                        w.weight,
2028                        out,
2029                        m,
2030                        w.n,
2031                        w.k,
2032                        stream,
2033                    )
2034                };
2035                // gate/up: quantize `input` [m,h] once, feed both.
2036                ops::quantize_act_q8_1(ctx.gpu, self.q4k_quant_act_k, input, a_q8, m, h, stream)?;
2037                mmq(&q2w.gate_proj, gate_out)?;
2038                mmq(&q2w.up_proj, up_out)?;
2039                ops::silu_mul(
2040                    ctx.gpu,
2041                    self.act_mul,
2042                    gate_out,
2043                    up_out,
2044                    gate_out,
2045                    m * inter,
2046                    stream,
2047                )?;
2048                // down: quantize `gate_out` [m,inter] (same-stream after silu_mul).
2049                let output = ctx.buffers.moe_output();
2050                ops::quantize_act_q8_1(
2051                    ctx.gpu,
2052                    self.q4k_quant_act_k,
2053                    gate_out,
2054                    a_q8,
2055                    m,
2056                    inter,
2057                    stream,
2058                )?;
2059                mmq(&q2w.down_proj, output)?;
2060                return Ok(());
2061            }
2062
2063            // Transient-dequant stopgap (Tier-1): requires the load-time dequant kernel.
2064            if self.dequant_q2_0_gn_k.0 == 0 {
2065                anyhow::bail!(
2066                    "dequant_q2_0_gn_to_bf16 kernel missing in this target build — \
2067                     packed-Q2 (AVAROK_GGUF_NATIVE_Q2) prefill is unavailable"
2068                );
2069            }
2070            let tc = self.dense_gemm_tc_k.0 != 0;
2071            // Dequant one packed-Q2 projection into the PERSISTENT arena BF16
2072            // scratch `[n, k]`, run the BF16 GEMM (A=`in` [m,k] → out [m,n]). No
2073            // per-matmul alloc/sync/free: the arena buffer is sized to the
2074            // largest packed projection and reused. Gate → up → down run
2075            // sequentially on `stream`, so each GEMM consumes the scratch before
2076            // the next projection's dequant overwrites it (same-stream order).
2077            let scratch = ctx.buffers.q2_dequant_scratch();
2078            let q2_gemm = |w: &PackedQ2Weight, input: DevicePtr, out: DevicePtr| -> Result<()> {
2079                let (n, k) = (w.n, w.k);
2080                debug_assert!(
2081                    (n as usize) * (k as usize) * 2 <= ctx.buffers.q2_dequant_scratch_bytes(),
2082                    "packed-Q2 FFN dequant scratch too small for [{n},{k}] BF16"
2083                );
2084                ops::dequant_q2_0_gn_to_bf16(
2085                    ctx.gpu,
2086                    self.dequant_q2_0_gn_k,
2087                    w.weight,
2088                    scratch,
2089                    n,
2090                    k,
2091                    w.group as u32,
2092                    stream,
2093                )?;
2094                let dw = DenseWeight { weight: scratch };
2095                if tc {
2096                    ops::dense_gemm_tc(
2097                        ctx.gpu,
2098                        self.dense_gemm_tc_k,
2099                        input,
2100                        &dw,
2101                        out,
2102                        m,
2103                        n,
2104                        k,
2105                        stream,
2106                    )?;
2107                } else {
2108                    ops::dense_gemm(
2109                        ctx.gpu,
2110                        self.dense_gemm_bf16_k,
2111                        input,
2112                        &dw,
2113                        out,
2114                        m,
2115                        n,
2116                        k,
2117                        stream,
2118                    )?;
2119                }
2120                Ok(())
2121            };
2122            q2_gemm(&q2w.gate_proj, input, gate_out)?;
2123            q2_gemm(&q2w.up_proj, input, up_out)?;
2124            ops::silu_mul(
2125                ctx.gpu,
2126                self.act_mul,
2127                gate_out,
2128                up_out,
2129                gate_out,
2130                m * inter,
2131                stream,
2132            )?;
2133            let output = ctx.buffers.moe_output();
2134            q2_gemm(&q2w.down_proj, gate_out, output)?;
2135            return Ok(());
2136        }
2137
2138        // Native FP8: small batches stream each weight once via the existing
2139        // batched GEMVs, avoiding padded MMA tiles. Larger prefills prefer a
2140        // transposed copy when available, then the same-format pipelined GEMM.
2141        // Every fallback retains the original E4M3 bytes and FP32 block scales.
2142        //
2143        // 🔴 ARM ORDER IS THE DISPATCH RULE — decode rungs first, widest last:
2144        //
2145        //   1. m <= 4      w8a16_gemv_batch4        one weight pass, 4-row tier
2146        //   2. m 5..=16    w8a16_gemm_m16           MMA, AVAROK_FFN_M16_TC only
2147        //   3. m 17..=32   w8a16_gemm_m16 x2        MMA, AVAROK_FFN_M16_TC only
2148        //      (AVAROK_FFN_M16_TC reaches THIS arm only; the attention tiers
2149        //       take AVAROK_ATTN_M16_TC, and AVAROK_M16_TC is both. Round 6
2150        //       measured them moving in opposite directions: attention -21.7%,
2151        //       this arm +13.7%. WHY: `dense_ffn_m16_tc.rs`.)
2152        //   4. m 5..=16    w8a16_gemv_batch16       OPT-IN, off by default
2153        //   5. m 17..=32   w8a16_gemv_batch16 x2    OPT-IN, off by default
2154        //   6. W8A8 block-scaled prefill            (#917/#928)
2155        //   7. transposed / pipelined / base W8A16 tile GEMMs
2156        //
2157        // Rungs 2-3 are the TENSOR-CORE tier and are OFF unless
2158        // `AVAROK_FFN_M16_TC` is set. `AVAROK_FFN_M16_TC` reaches THIS arm only;
2159        // the attention tiers take `AVAROK_ATTN_M16_TC`, and `AVAROK_M16_TC` is
2160        // both. They are the only arm here that does NOT reproduce the scalar
2161        // `w8a16_gemv` bit-for-bit: an m16n8k16 MMA reassociates the K
2162        // reduction (<= 2 BF16 ULP), which is the same reassociation the
2163        // pre-#927 tile GEMMs had at these widths. WHY, the H100 numbers and
2164        // the seam: `dense_ffn_m16_tc.rs`.
2165        //
2166        // Rungs 4-5 are #927, and they are DISARMED unless `AVAROK_FFN_BATCH16=1`
2167        // — so a stock serve runs rungs 1, 6, 7 exactly as it did before #927.
2168        // The cliff they answer is real (the rung-7 tile GEMMs pad M to a
2169        // 128-row MMA tile, 5-12 TFLOP/s on these shapes), but the tier as a
2170        // whole lost the only end-to-end A/B it has: H100, 2026-09-11,
2171        // Qwen3.8-27B-FP8, C=16 aggregate 121.4 ON -> 128.0 tok/s OFF, because
2172        // it dispatches by row count and so catches a chunked prefill's tail
2173        // chunk. It has never been measured on GB10. Receipt, opt-in and the
2174        // consequence for rung 6's lower edge: `dense_ffn_batch16_decode.rs`.
2175        if let Some(ref fp8w) = self.fp8_weights {
2176            // Resolved ONCE for the whole FFN, not per projection: gate, up and
2177            // down share `m`, so a per-arm call would re-run the same match
2178            // three times and could not be read as one rule.
2179            let batch16 = self.ffn_batch16_plan(m);
2180            // Resolved per PROJECTION depth, not once: gate/up reduce over `h`
2181            // and down over `inter`, and the tier declines a K that is not a
2182            // whole number of 128-wide scale blocks.
2183            let m16_tc = |k: u32| self.ffn_m16_tc_plan(m, k);
2184            macro_rules! w8_gemm {
2185                ($w:expr, $wt:expr, $in:expr, $out:expr, $n:expr, $k:expr, $a8:expr, $cap:expr) => {
2186                    match $wt {
2187                        // 1. m <= 4.
2188                        _ if (1..=4).contains(&m) && self.w8a16_gemv_batch4_k.0 != 0 => {
2189                            ops::w8a16_gemv_batch4(
2190                                ctx.gpu,
2191                                self.w8a16_gemv_batch4_k,
2192                                $in,
2193                                $w.weight,
2194                                $w.row_scale,
2195                                $out,
2196                                m,
2197                                $n,
2198                                $k,
2199                                stream,
2200                            )?
2201                        }
2202                        // 2-3. TENSOR-CORE tier, 5..=16 (one launch) and
2203                        // 17..=32 (two halves). `Some(plan)` already encodes the
2204                        // handle, the `AVAROK_FFN_M16_TC` lever (default OFF) and
2205                        // the K % 128 guard, so this arm is inert until an
2206                        // operator opts in. It REASSOCIATES — see the rule above.
2207                        _ if m16_tc($k).is_some() => self.w8a16_m16_tc_proj(
2208                            ctx,
2209                            m16_tc($k).expect("guarded by is_some"),
2210                            &$w,
2211                            $in,
2212                            $out,
2213                            m,
2214                            $n,
2215                            $k,
2216                            stream,
2217                        )?,
2218                        // 4-5. m 5..=16 (one launch) and 17..=32 (two halves).
2219                        // `Some(plan)` already encodes the handle and the
2220                        // `AVAROK_FFN_BATCH16=1` opt-in, so this is `None` on a
2221                        // stock serve and the match falls through to rung 4.
2222                        _ if batch16.is_some() => self.w8a16_batch16_proj(
2223                            ctx,
2224                            batch16.expect("guarded by is_some"),
2225                            &$w,
2226                            $in,
2227                            $out,
2228                            m,
2229                            $n,
2230                            $k,
2231                            stream,
2232                        )?,
2233                        // 6. W8A8 block-scaled (#917/#928) — ahead of the W8A16
2234                        // arms below, which run the BF16 MMA at ~12 TFLOP/s on
2235                        // these shapes. `$a8` is `Some` exactly when
2236                        // `prefill_w8a8_selected` held for this projection.
2237                        _ if $a8.is_some() => {
2238                            let (a_fp8, a_scale) = $a8.expect("guarded by is_some");
2239                            self.w8a8_gemm(ctx, a_fp8, a_scale, &$w, $out, $cap, m, $n, $k, stream)?
2240                        }
2241                        // 7. Tile GEMMs — prefill widths (m > 32) and any
2242                        // shape the rungs above declined.
2243                        Some(wt) if self.w8a16_gemm_t_m128_k.0 != 0 => {
2244                            let wt: Fp8WeightTransposed = wt;
2245                            ops::w8a16_gemm_n128_m128(
2246                                ctx.gpu,
2247                                self.w8a16_gemm_t_m128_k,
2248                                $in,
2249                                wt.weight_t,
2250                                wt.scale_t,
2251                                $out,
2252                                m,
2253                                $n,
2254                                $k,
2255                                stream,
2256                            )?
2257                        }
2258                        _ if self.w8a16_gemm_pipelined_k.0 != 0 => ops::w8a16_gemm_pipelined(
2259                            ctx.gpu,
2260                            self.w8a16_gemm_pipelined_k,
2261                            $in,
2262                            $w.weight,
2263                            $w.row_scale,
2264                            $out,
2265                            m,
2266                            $n,
2267                            $k,
2268                            stream,
2269                        )?,
2270                        _ => ops::w8a16_gemm(
2271                            ctx.gpu,
2272                            self.w8a16_gemm_k,
2273                            $in,
2274                            $w.weight,
2275                            $w.row_scale,
2276                            $out,
2277                            m,
2278                            $n,
2279                            $k,
2280                            stream,
2281                        )?,
2282                    }
2283                };
2284            }
2285            let gate_t: Option<Fp8WeightTransposed> = None;
2286            let up_t: Option<Fp8WeightTransposed> = None;
2287            let down_t: Option<Fp8WeightTransposed> = None;
2288            // W8A8 activation quant (#917/#928), ONCE per shared input: gate and
2289            // up both read `input`, so quantizing per projection would pay the
2290            // per-token E4M3 cast twice per layer for identical bytes. down
2291            // quantizes separately because its input is the post-SiLU product.
2292            // `None` => that projection keeps today's W8A16 dispatch.
2293            // Selection rule + WHY: `dense_ffn_w8a8_prefill.rs` (SSOT).
2294            //
2295            // `batch16.is_none()` is part of the condition and not only of the
2296            // match: rungs 2-3 sit AHEAD of the W8A8 arm, so at m=5..=32 the
2297            // quantizer launch below would be dead work whose result no arm
2298            // reads. This keeps the W8A8 lower edge honest at m > 32.
2299            // `m16_tc` is checked at BOTH projection depths: gate/up reduce
2300            // over `h` and down over `inter`, and either claiming the width
2301            // makes the quantizer launch below dead work for that projection.
2302            let w8a8_reachable =
2303                batch16.is_none() && m16_tc(h).is_none() && m16_tc(inter).is_none();
2304            let gate_up_w8a8 = w8a8_reachable
2305                && self.prefill_w8a8_selected(ctx, m, inter, h, &fp8w.gate_proj)
2306                && self.prefill_w8a8_selected(ctx, m, inter, h, &fp8w.up_proj);
2307            let down_w8a8 =
2308                w8a8_reachable && self.prefill_w8a8_selected(ctx, m, h, inter, &fp8w.down_proj);
2309            if !gate_up_w8a8 && !down_w8a8 && w8a8_reachable {
2310                self.log_w8a16_prefill_route(ctx);
2311            }
2312            let gu_a8 = if gate_up_w8a8 {
2313                Some(self.w8a8_quant_act(ctx, input, m, h, stream)?)
2314            } else {
2315                None
2316            };
2317            let gu_cap = ctx.buffers.expert_gate_out_bytes();
2318            // FUSED gate+up (#927): ONE cuBLASLt W8A8 GEMM at N=2*inter, then
2319            // the strided SiLU straight out of its `[m, 2*inter]` rows. It sits
2320            // HERE, ahead of the two `w8_gemm!` calls, because it is the SAME
2321            // W8A8 arm those calls would take (`gate_up_w8a8` is a clause of
2322            // its rule) with the two N's concatenated — not a new rung of the
2323            // ladder. Declines to the pair below at every width, target and
2324            // checkpoint it does not claim. Rule and the round-13 receipt:
2325            // `dense_ffn_gateup_fused.rs` (SSOT).
2326            if let Some((a_fp8, a_scale)) = gu_a8
2327                && let Some(fused) = self.gateup_fused_plan(ctx, m, inter, gate_up_w8a8)
2328            {
2329                self.w8a8_gate_up_fused(ctx, a_fp8, a_scale, fused, gate_out, m, inter, h, stream)?;
2330            } else {
2331                w8_gemm!(
2332                    fp8w.gate_proj,
2333                    gate_t,
2334                    input,
2335                    gate_out,
2336                    inter,
2337                    h,
2338                    gu_a8,
2339                    gu_cap
2340                );
2341                w8_gemm!(fp8w.up_proj, up_t, input, up_out, inter, h, gu_a8, gu_cap);
2342                ops::silu_mul(
2343                    ctx.gpu,
2344                    self.act_mul,
2345                    gate_out,
2346                    up_out,
2347                    gate_out,
2348                    m * inter,
2349                    stream,
2350                )?;
2351            }
2352            let output = ctx.buffers.moe_output();
2353            // `gate_out` (the SiLU product) is fully written by the launch above
2354            // and re-read here; the quant lands in the SAME scratch the gate/up
2355            // quant used, which is safe because every launch is on `stream`.
2356            let down_a8 = if down_w8a8 {
2357                Some(self.w8a8_quant_act(ctx, gate_out, m, inter, stream)?)
2358            } else {
2359                None
2360            };
2361            let down_cap = ctx.buffers.moe_output_bytes();
2362            w8_gemm!(
2363                fp8w.down_proj,
2364                down_t,
2365                gate_out,
2366                output,
2367                h,
2368                inter,
2369                down_a8,
2370                down_cap
2371            );
2372            return Ok(());
2373        }
2374
2375        // BF16 prefill dispatch. Prefer the tensor-core m16n8k16 MMA kernel
2376        // (`dense_gemm_tc`, 3-5x+ over scalar) — the scalar `dense_gemm_bf16`
2377        // was the flat ~155 tok/s prefill bottleneck on Qwen3.6-27B dense
2378        // NVFP4 (FFN = ~83% of prefill). Falls back to scalar if the TC
2379        // kernel isn't loaded for this target. Decode (gemv, M=1) is a
2380        // separate path, so TPOT is unaffected; BF16 MMA preserves coherence.
2381        if let Some(ref bf16w) = self.bf16_weights {
2382            let tc = self.dense_gemm_tc_k.0 != 0;
2383            // helper: cuBLASLt when enabled (the big win at prefill M), else the
2384            // tensor-core MMA kernel, else scalar. dense_gemm_tc is ~1.4 TFLOP/s
2385            // on the large dense-FFN shapes (e.g. Laguna layer-0 gate/up/down at
2386            // N=12288/3072, K=3072) — nsys measured its 3 launches at ~100 ms
2387            // EACH = 33% of the whole C=1 prefill. cuBLASLt runs the identical
2388            // BF16×BF16→FP32 GEMM at 90+ TFLOP/s (~65× faster), the same path
2389            // q/k/v/o and the head-gate already use. Gated on AVAROK_CUBLAS_GEMM.
2390            macro_rules! ffn_gemm {
2391                ($a:expr, $b:expr, $c:expr, $n:expr, $k:expr) => {
2392                    if ctx.dispatch.cublas.ffn {
2393                        ops::cublas_bf16_proj_dense($a, $b.weight, $c, m, $n, $k, stream)?;
2394                    } else if tc {
2395                        ops::dense_gemm_tc(
2396                            ctx.gpu,
2397                            self.dense_gemm_tc_k,
2398                            $a,
2399                            $b,
2400                            $c,
2401                            m,
2402                            $n,
2403                            $k,
2404                            stream,
2405                        )?;
2406                    } else {
2407                        ops::dense_gemm(
2408                            ctx.gpu,
2409                            self.dense_gemm_bf16_k,
2410                            $a,
2411                            $b,
2412                            $c,
2413                            m,
2414                            $n,
2415                            $k,
2416                            stream,
2417                        )?;
2418                    }
2419                };
2420            }
2421            ffn_gemm!(input, &bf16w.gate_proj, gate_out, inter, h);
2422            ffn_gemm!(input, &bf16w.up_proj, up_out, inter, h);
2423            ops::silu_mul(
2424                ctx.gpu,
2425                self.act_mul,
2426                gate_out,
2427                up_out,
2428                gate_out,
2429                m * inter,
2430                stream,
2431            )?;
2432            let output = ctx.buffers.moe_output();
2433            ffn_gemm!(gate_out, &bf16w.down_proj, output, h, inter);
2434            return Ok(());
2435        }
2436
2437        // Prefill: prefer the 128x128 cp.async-pipelined `w4a16_gemm_t_m128`
2438        // (the kernel attention/SSM use) over the M64xN64 base `w4a16_gemm`
2439        // (~10 TFLOPS, the flat ~155 tok/s bottleneck). That kernel needs the
2440        // TRANSPOSED weight layout, so we use the `*_proj_t` copies built at
2441        // load (decode keeps the non-transposed weights via gemv → TPOT/
2442        // coherence unaffected). Falls back to base when no transposed copy /
2443        // kernel is present.
2444        // LOSSLESS prefill opt-in: when AVAROK_BF16_TC_PREFILL is set AND the
2445        // BF16 128x128 kernel is present, route prefill GEMMs through the
2446        // bit-equivalent BF16 tensor-core path instead of the default FP8-E4M3
2447        // `t_m128`. The FP8 crush is fast but perturbs generation (measured
2448        // length-truncations / accuracy risk on Qwen3.6-27B); the BF16 variant
2449        // keeps the same 128x128 cp.async speed at base-kernel precision.
2450        // Unset (default) → every arm below is byte-for-byte the prior behavior
2451        // (PCND: explicit opt-in, no silent default change). Read once per call.
2452        // Env read only here; the usable gate (`bf16_tc_prefill`) is derived
2453        // below AFTER v1/v2 selection, from the handle actually launched.
2454        // Gating on v1's handle while dispatching v2 admitted launches of a
2455        // kernel this target may not carry.
2456        let bf16_tc_env = ctx.levers.bf16_tc_prefill;
2457        // FP8 M64 fast-prefill opt-in: route prefill GEMMs through the m16n8k32
2458        // e4m3 M64 kernel (~1.47x vs v2 BF16, smem-relieved). Lossy (cosine 0.9997)
2459        // → highest priority when set, so it overrides the BF16/FP8 t_m128 arms.
2460        // PCND: explicit opt-in, default off = byte-for-byte prior behavior.
2461        let fp8_m64_prefill = self.w4a16_gemm_t_k.0 != 0 && ctx.levers.fp8_m64_prefill;
2462        // int8 W4A8 fast-prefill opt-in (AVAROK_INT8_PREFILL): route prefill GEMMs
2463        // through the validated requant→`int8_gemm_faith2` pipeline (cosine
2464        // 0.999978 vs the host full-precision dequant GEMM). HIGHEST priority when
2465        // set, so it overrides every other prefill arm. Needs both operands int8:
2466        // the NVFP4 weights are requanted to int8 once (cached, see
2467        // `ensure_int8_weight`) and the BF16 activations are requanted every call
2468        // into the shared scratch (`ensure_int8_scratch`). LOSSY (perf gate, not
2469        // bit-identical) — the _2.5h IoU gate is the final arbiter.
2470        // PCND: explicit opt-in, default off = byte-for-byte prior behavior; the
2471        // arm is a no-op (and no buffers are built) unless the kernels are loaded.
2472        let int8_prefill = self.int8_faith2_k.0 != 0 && ctx.levers.int8_prefill;
2473        if int8_prefill {
2474            // Log-once latch (see `avarok_core::scope`). It holds no model-derived
2475            // value — the message is rebuilt from the arguments every call — so a
2476            // stale entry cannot produce a wrong answer, only a suppressed duplicate
2477            // line after a model swap. Scoping it would thread a logging concern
2478            // through the call path to prevent one repeated INFO line.
2479            if ctx.stats.once("log:ffn_int8_prefill") {
2480                tracing::info!(
2481                    "[avarok] AVAROK_INT8_PREFILL=1: dense-FFN prefill via int8_gemm_faith2 (W4A8 requant→int8 MMA, lossy ~0.99998 cosine)"
2482                );
2483            }
2484        }
2485        // NVFP4 W4A4 MMQ prefill (AVAROK_FFN_NVFP4_MMQ) — vendored llama Blackwell
2486        // block-scale FP4 MMA, gate/up ONLY (hybrid: down stays on the default t_m128
2487        // path — SiLU(gate)*up is heavy-tailed and accuracy-critical). SiLU models only
2488        // (the scale2 fold lives in the scaled SiLU-mul). Mutually exclusive with
2489        // AVAROK_FFN_MMQ (both use the shared ffn_act_q8 scratch); this arm wins.
2490        //
2491        // An installed LoRA adapter turns this arm OFF. The MMQ path leaves
2492        // gate_out/up_out holding UNSCALED products and folds each
2493        // projection's `weight_scale_2` later, inside the SiLU-mul kernel. A
2494        // LoRA delta is a true-valued quantity, so adding it to those buffers
2495        // would put it through a scale2 multiply that does not belong to it —
2496        // silently wrong output rather than a failure. Folding scale2 into the
2497        // delta instead would mean reproducing the quant layout's arithmetic
2498        // in the adapter path, which is a much larger commitment than the
2499        // ~8% prefill this arm is worth (measured 831 vs 767 tok/s at 8K).
2500        // Correctness first; making LoRA and MMQ coexist is its own change.
2501        let fp4mmq_prefill = self.nvfp4_mmq_nc_k.0 != 0
2502            && self.nvfp4_quant_act_k.0 != 0
2503            && self.nvfp4_silu_scaled_k.0 != 0
2504            && matches!(self.activation, FfnActivation::SiLU)
2505            && self.lora.is_none()
2506            && ctx.levers.ffn_nvfp4_mmq;
2507        if fp4mmq_prefill {
2508            // Log-once latch (see `avarok_core::scope`). It holds no model-derived
2509            // value — the message is rebuilt from the arguments every call — so a
2510            // stale entry cannot produce a wrong answer, only a suppressed duplicate
2511            // line after a model swap. Scoping it would thread a logging concern
2512            // through the call path to prevent one repeated INFO line.
2513            if ctx.stats.once("log:ffn_fp4_mmq_prefill") {
2514                tracing::info!(
2515                    "[avarok] AVAROK_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)"
2516                );
2517            }
2518        }
2519        // Down-projection MMQ arm (DEFAULT ON; kill-switch AVAROK_NO_FFN_NVFP4_MMQ_DOWN=1): route down through
2520        // the same MMQ arm (t_m128 runs the narrow-N down at only ~34 TFLOP/s in-model).
2521        // Accuracy note: down W4A4 cosine 0.9961 (random) — better than the previously
2522        // coherence-validated all-W4A4 config (0.991) — but still the heavy-tailed
2523        // projection, so it stays a SEPARATE opt-in gate.
2524        let fp4mmq_down =
2525            fp4mmq_prefill && self.nvfp4_scale_k.0 != 0 && ctx.levers.ffn_nvfp4_mmq_down;
2526        // HYBRID: route the accuracy-critical down_proj OFF Q4_K onto the near-lossless faith2
2527        // NVFP4 path (W4A8 requant, cos 0.99998). down=SiLU(gate)*up is heavy-tailed; Q4_K
2528        // superblock scaling clips it (BFCL `multiple` -4.0%; llama promotes only down→Q6_K for
2529        // this reason). gate/up stay on Q4_K. Default ON when MMQ active; AVAROK_FFN_MMQ_DOWN_Q4K=1
2530        // = lossy all-Q4_K (A/B only). Defined here (self-fields+env, no q4k_prefill var dep) so the
2531        // int8 scratch below can size for the hybrid down.
2532        let down_faith2 = self.q4k_mmq_nc_k.0 != 0
2533            && self.q4k_quant_act_k.0 != 0
2534            && self.q4k_quant_w_k.0 != 0
2535            && self.dequant_nvfp4_bf16_k.0 != 0
2536            && self.int8_faith2_k.0 != 0
2537            && self.requant_a_int8_k.0 != 0
2538            && !fp4mmq_prefill
2539            && ctx.levers.ffn_mmq
2540            && !ctx.levers.ffn_mmq_down_q4k;
2541        // Pre-allocate (or reuse) the activation-requant scratch once per call,
2542        // sized to the largest projection K (= max(h, inter)) so the per-GEMM
2543        // arms never trigger a mid-call grow/sync. NULL when the int8 path is off.
2544        // Shared, arena-owned activation-requant scratch (sized once for
2545        // max_batch_tokens × max(h, inter) in BufferSizes::from_config). Replaces
2546        // the former per-DenseFfnLayer grow-on-demand allocator that leaked
2547        // ~286MB × 64 layers on the MMQ prefill path.
2548        let (int8_a_i8, int8_a_scale) = if int8_prefill || down_faith2 {
2549            (ctx.buffers.ffn_act_a(), ctx.buffers.ffn_act_scale())
2550        } else {
2551            (DevicePtr::NULL, DevicePtr::NULL)
2552        };
2553        // W4A4 native-FP4 prefill (AVAROK_FP4_PREFILL) — HIGHEST priority. NVFP4 weights
2554        // used directly (no requant); BF16 activations quantized to NVFP4 each GEMM into
2555        // the shared scratch. Native FP4 tensor cores (sm_121a). Lossy (cos ~0.99 vs fp32).
2556        let fp4_prefill =
2557            self.w4a4_gemm_k.0 != 0 && self.quantize_nvfp4_k.0 != 0 && ctx.levers.fp4_prefill;
2558        if fp4_prefill {
2559            // Log-once latch (see `avarok_core::scope`). It holds no model-derived
2560            // value — the message is rebuilt from the arguments every call — so a
2561            // stale entry cannot produce a wrong answer, only a suppressed duplicate
2562            // line after a model swap. Scoping it would thread a logging concern
2563            // through the call path to prevent one repeated INFO line.
2564            if ctx.stats.once("log:ffn_fp4_prefill") {
2565                tracing::info!(
2566                    "[avarok] AVAROK_FP4_PREFILL=1: dense-FFN prefill via w4a4_gemm (native FP4 MMA sm_121a, W4A4)"
2567                );
2568            }
2569        }
2570        // NVFP4 packed [m,K/2] + scale [m,K/16] both fit within the shared int8
2571        // buffers (a_i8 [m,K] ⊇ packed; a_scale [m,(K/32)*4] ⊇ scale). FP4-prefill
2572        // is a standalone A/B flag, never co-active with the int8/Q4_K down path.
2573        let (nvfp4_a_packed, nvfp4_a_scale) = if fp4_prefill {
2574            (ctx.buffers.ffn_act_a(), ctx.buffers.ffn_act_scale())
2575        } else {
2576            (DevicePtr::NULL, DevicePtr::NULL)
2577        };
2578        // Q4_K MMQ prefill (AVAROK_FFN_MMQ) — vendored llama Q4_K W4A8 GEMM. Highest priority
2579        // when enabled. Lossy (Q4_K weight format ≠ NVFP4); gate via BFCL before relying on it.
2580        let q4k_prefill = self.q4k_mmq_nc_k.0 != 0
2581            && self.q4k_quant_act_k.0 != 0
2582            && self.q4k_quant_w_k.0 != 0
2583            && self.dequant_nvfp4_bf16_k.0 != 0
2584            && !fp4mmq_prefill
2585            && ctx.levers.ffn_mmq;
2586        if q4k_prefill {
2587            // Log-once latch (see `avarok_core::scope`). It holds no model-derived
2588            // value — the message is rebuilt from the arguments every call — so a
2589            // stale entry cannot produce a wrong answer, only a suppressed duplicate
2590            // line after a model swap. Scoping it would thread a logging concern
2591            // through the call path to prevent one repeated INFO line.
2592            if ctx.stats.once("log:ffn_q4k_prefill") {
2593                tracing::info!(
2594                    "[avarok] AVAROK_FFN_MMQ=1: dense-FFN prefill via vendored llama Q4_K MMQ (W4A8, +25%/+10% gate·down vs faith2)"
2595                );
2596            }
2597        }
2598        let q4k_a = if q4k_prefill {
2599            ctx.buffers.ffn_act_q8()
2600        } else {
2601            DevicePtr::NULL
2602        };
2603        // FP4-MMQ y scratch: block_fp4_mmq activations, in the SAME shared arena buffer
2604        // (fp4_act_scratch_bytes ≤ q8_1_scratch_bytes; mutually exclusive with q4k_prefill).
2605        let fp4_y = if fp4mmq_prefill {
2606            ctx.buffers.ffn_act_q8()
2607        } else {
2608            DevicePtr::NULL
2609        };
2610        // A/B escape hatch (benchmark only): force the proven v1 BF16 kernel even
2611        // when v2 is loaded, so v1-vs-v2 prefill TTFT can be compared in one
2612        // binary. Default unset → prefer v2 (the faster, bit-identical variant).
2613        let use_v2 = self.w4a16_gemm_t_m128_bf16_v2_k.0 != 0 && ctx.levers.prefill_v2;
2614        let bf16_kernel = if use_v2 {
2615            self.w4a16_gemm_t_m128_bf16_v2_k
2616        } else {
2617            self.w4a16_gemm_t_m128_bf16_k
2618        };
2619        // Final gate: the flag is honored only when the SELECTED kernel is
2620        // loaded (v2 when preferred, else v1) — not v1's handle unconditionally.
2621        let bf16_tc_prefill = bf16_kernel.0 != 0 && bf16_tc_env;
2622
2623        macro_rules! w4_gemm {
2624            ($w:expr, $wt:expr, $cell:expr, $qcell:expr, $fp4cell:expr, $allow_fp4:expr, $in:expr, $out:expr, $n:expr, $k:expr, $allow_q4k:expr) => {
2625                match $wt {
2626                    // NVFP4 W4A4 MMQ prefill (AVAROK_FFN_NVFP4_MMQ) — HIGHEST priority.
2627                    // `$allow_fp4` = fp4mmq_prefill for gate/up, fp4mmq_down for down.
2628                    // Activation pre-quantized into `fp4_y` by the caller; the output is
2629                    // missing ×scale2, folded downstream (scaled SiLU-mul / scale_bf16).
2630                    _ if $allow_fp4 => {
2631                        let _ = $in;
2632                        let qw =
2633                            self.ensure_nvfp4_mmq_weight($fp4cell, ctx.gpu, $w, $n, $k, stream)?;
2634                        // Size the M tile to the batch when the batch is small and
2635                        // the small-tile entries are present. m must be <= mmq_x or
2636                        // grid.y>1 re-streams the weights per tile.
2637                        let (tk_nc, tk_wc, tile) = if m <= 16
2638                            && self.nvfp4_mmq16_nc_k.0 != 0
2639                            && mmq_small_tile_enabled()
2640                        {
2641                            (self.nvfp4_mmq16_nc_k, self.nvfp4_mmq16_wc_k, 16u32)
2642                        } else if m <= 32
2643                            && self.nvfp4_mmq32_nc_k.0 != 0
2644                            && mmq_small_tile_enabled()
2645                        {
2646                            (self.nvfp4_mmq32_nc_k, self.nvfp4_mmq32_wc_k, 32u32)
2647                        } else if m <= 64
2648                            && self.nvfp4_mmq64_nc_k.0 != 0
2649                            && mmq_small_tile_enabled()
2650                            && mmq_tile64_enabled()
2651                        {
2652                            (self.nvfp4_mmq64_nc_k, self.nvfp4_mmq64_wc_k, 64u32)
2653                        } else {
2654                            (self.nvfp4_mmq_nc_k, self.nvfp4_mmq_wc_k, 128u32)
2655                        };
2656                        ops::nvfp4_mmq_gemm_tiled(
2657                            ctx.gpu, tk_nc, tk_wc, tile, fp4_y, qw.w, $out, m, $n, $k, stream,
2658                        )?;
2659                    }
2660                    // Q4_K MMQ prefill (AVAROK_FFN_MMQ) — next priority, gated per-GEMM by
2661                    // `$allow_q4k` (false for down in the hybrid → falls to the faith2 arm).
2662                    // Activation `$in` is pre-quantized to q8_1 in `q4k_a` by the caller.
2663                    _ if q4k_prefill && $allow_q4k => {
2664                        let qw = self.ensure_q4k_weight($qcell, ctx.gpu, $w, $n, $k, stream)?;
2665                        ops::q4k_mmq_gemm(
2666                            ctx.gpu,
2667                            self.q4k_mmq_nc_k,
2668                            self.q4k_mmq_wc_k,
2669                            q4k_a,
2670                            qw.w_q4k,
2671                            $out,
2672                            m,
2673                            $n,
2674                            $k,
2675                            stream,
2676                        )?;
2677                    }
2678                    // W4A4 native-FP4 prefill (AVAROK_FP4_PREFILL) — HIGHEST priority.
2679                    // The activation is PRE-quantized into the NVFP4 scratch by the caller
2680                    // (`input` once for gate+up which share it; `gate_out` for down) — opt #1,
2681                    // avoids the redundant re-quant. This arm just runs w4a4_gemm against the
2682                    // native NVFP4 weight `$w` (no requant). sm_121a FP4 MMA.
2683                    _ if fp4_prefill => {
2684                        let _ = $in;
2685                        ops::w4a4_gemm(
2686                            ctx.gpu,
2687                            self.w4a4_gemm_k,
2688                            nvfp4_a_packed,
2689                            nvfp4_a_scale,
2690                            $w,
2691                            $out,
2692                            m,
2693                            $n,
2694                            $k,
2695                            stream,
2696                        )?;
2697                    }
2698                    // int8 W4A8 fast prefill (AVAROK_INT8_PREFILL) — next priority.
2699                    // Independent of `$wt`/the transposed copies: requant reads the
2700                    // non-transposed NVFP4 `$w` directly. Builds (once) + caches the
2701                    // int8 weight in `$cell`, then requant_a + faith2 via the shared
2702                    // scratch. Lossy (cosine ~0.99998). Also the HYBRID down path
2703                    // (down_faith2 && !$allow_q4k): down falls here instead of Q4_K.
2704                    _ if int8_prefill || (down_faith2 && !$allow_q4k) => {
2705                        let iw = self.ensure_int8_weight($cell, ctx.gpu, $w, $n, $k, stream)?;
2706                        // faith5 (AVAROK_INT8_FAITH5=1): int32 per-sb accumulation
2707                        // breaks the MMA→scale dependency chain. Same kernel signature
2708                        // + grid/block as faith2 — just a different KernelHandle.
2709                        let int8_kernel = if self.int8_faith5_k.0 != 0 && ctx.levers.int8_faith5 {
2710                            self.int8_faith5_k
2711                        } else {
2712                            self.int8_faith2_k
2713                        };
2714                        ops::int8_gemm_faith2_prefill(
2715                            ctx.gpu,
2716                            int8_kernel,
2717                            self.requant_a_int8_k,
2718                            $in,
2719                            iw.w_i8,
2720                            iw.w_scale,
2721                            int8_a_i8,
2722                            int8_a_scale,
2723                            $out,
2724                            m,
2725                            $n,
2726                            $k,
2727                            stream,
2728                        )?;
2729                    }
2730                    // Lossless opt-in: BF16 128x128 tensor-core prefill (bit-equivalent
2731                    // to base `w4a16_gemm`). Preferred over the FP8 t_m128/v2 paths only
2732                    // when AVAROK_BF16_TC_PREFILL is set and the kernel is loaded. Within
2733                    // the lossless path, prefer the higher-occupancy v2 kernel (3 CTAs/SM,
2734                    // bit-identical to v1) when it is loaded; else the proven v1 kernel.
2735                    // Both go through the same launch helper (identical grid/block/args).
2736                    // FP8 M64 fast prefill (AVAROK_FP8_M64_PREFILL) — highest priority,
2737                    // M64 grid via the w4a16_gemm_n128 launcher.
2738                    Some(wt) if fp8_m64_prefill => ops::w4a16_gemm_n128(
2739                        ctx.gpu,
2740                        self.w4a16_gemm_t_k,
2741                        $in,
2742                        &wt,
2743                        $out,
2744                        m,
2745                        $n,
2746                        $k,
2747                        stream,
2748                    )?,
2749                    // v2's COMPILED signature carries a 9th param, `ldb`
2750                    // (transposed-B row stride; == N for the FFN twins, which
2751                    // are built unpadded). It MUST go through the `_ldb`
2752                    // launcher: the 8-arg helper leaves cuLaunchKernel reading
2753                    // one-past-the-end of the param array for `ldb` —
2754                    // CUDA_ERROR_INVALID_VALUE or a host SIGSEGV depending on
2755                    // the neighboring heap word. v1 takes exactly 8 params and
2756                    // stays on the 8-arg helper.
2757                    Some(wt) if bf16_tc_prefill && use_v2 => ops::w4a16_gemm_n128_m128_bf16_ldb(
2758                        ctx.gpu,
2759                        bf16_kernel,
2760                        $in,
2761                        &wt,
2762                        $out,
2763                        m,
2764                        $n,
2765                        $k,
2766                        $n,
2767                        stream,
2768                    )?,
2769                    Some(wt) if bf16_tc_prefill => ops::w4a16_gemm_n128_m128_bf16(
2770                        ctx.gpu,
2771                        bf16_kernel,
2772                        $in,
2773                        &wt,
2774                        $out,
2775                        m,
2776                        $n,
2777                        $k,
2778                        stream,
2779                    )?,
2780                    // Small-M routing (DFlash verify, M<=64): delegate to
2781                    // `w4a16_prefill_gemm`, which picks `w4a16_gemm_t` /
2782                    // `w4a16_gemm_t_k64` per the w4a16_m17_bench numbers and
2783                    // falls back to the same v2/m128 kernels below.
2784                    // AVAROK_FFN_SMALLM=0 disables. Sits after the opt-in
2785                    // quant arms so explicit MMQ/int8/FP8 experiments keep
2786                    // priority.
2787                    Some(wt) if m <= 64 => {
2788                        self.w4a16_prefill_gemm(ctx, $w, Some(&wt), $in, $out, m, $n, $k, stream)?
2789                    }
2790                    // Prefer v2 (8-warp) > t_m128 (4-warp) > scalar-tile base.
2791                    Some(wt) if self.w4a16_gemm_t_m128_v2_k.0 != 0 => ops::w4a16_gemm_n128_m128_v2(
2792                        ctx.gpu,
2793                        self.w4a16_gemm_t_m128_v2_k,
2794                        $in,
2795                        &wt,
2796                        $out,
2797                        m,
2798                        $n,
2799                        $k,
2800                        stream,
2801                    )?,
2802                    Some(wt) if self.w4a16_gemm_t_m128_k.0 != 0 => ops::w4a16_gemm_n128_m128(
2803                        ctx.gpu,
2804                        self.w4a16_gemm_t_m128_k,
2805                        $in,
2806                        &wt,
2807                        $out,
2808                        m,
2809                        $n,
2810                        $k,
2811                        stream,
2812                    )?,
2813                    _ => {
2814                        ops::w4a16_gemm(ctx.gpu, self.w4a16_gemm, $in, $w, $out, m, $n, $k, stream)?
2815                    }
2816                }
2817            };
2818        }
2819
2820        // W4A4 opt #1: quantize the gate/up SHARED input `[M, H]` to NVFP4 ONCE
2821        // (gate and up both read it) instead of per-GEMM. Reused by both arms below.
2822        if fp4_prefill {
2823            ops::quantize_bf16_to_nvfp4(
2824                ctx.gpu,
2825                self.quantize_nvfp4_k,
2826                input,
2827                nvfp4_a_packed,
2828                nvfp4_a_scale,
2829                m,
2830                h,
2831                stream,
2832            )?;
2833        }
2834        // Q4_K opt: quantize the gate/up SHARED input `[M, H]` to q8_1 ONCE (both read it).
2835        if q4k_prefill {
2836            ops::quantize_act_q8_1(ctx.gpu, self.q4k_quant_act_k, input, q4k_a, m, h, stream)?;
2837        }
2838        // FP4-MMQ: quantize the gate/up SHARED input `[M, H]` to block_fp4_mmq ONCE.
2839        if fp4mmq_prefill {
2840            ops::nvfp4_mmq_quantize_act(
2841                ctx.gpu,
2842                self.nvfp4_quant_act_k,
2843                input,
2844                fp4_y,
2845                m,
2846                h,
2847                stream,
2848            )?;
2849        }
2850        // Per-projection timers. `dense_total` localised 23.7 s of a 28.2 s
2851        // Gemma-4-31B prefill to this function; these say WHICH of the three
2852        // projections it is. Roofline for that shape: 3 x 5376 x 21504 x 4012 tok
2853        // x 60 layers = 167 TFLOP, so 23.7 s is ~7 TFLOP/s against a bf16
2854        // tensor-core peak two orders higher — a hypothesis these numbers test
2855        // rather than assume.
2856        // ★ PER-STEP, NOT CUMULATIVE. The first version of this timer measured
2857        // elapsed-since-one-start at each of the three call sites, so `up_proj`
2858        // included `gate_proj` and `down_proj` included both — and the summed
2859        // "total profiled" then exceeded the wall clock, which is the tell.
2860        macro_rules! ffn_step {
2861            ($label:expr, $t0:expr) => {
2862                if ctx.profile {
2863                    ctx.gpu.synchronize(stream)?;
2864                    tracing::info!(
2865                        "  FFN prefill [{}] N={}: {}µs",
2866                        $label,
2867                        num_tokens,
2868                        $t0.elapsed().as_micros()
2869                    );
2870                    #[allow(unused_assignments)]
2871                    {
2872                        $t0 = std::time::Instant::now();
2873                    }
2874                }
2875            };
2876        }
2877        #[allow(unused_mut, unused_assignments)]
2878        let mut t_ffn = std::time::Instant::now();
2879        // gate_proj GEMM: [M, H] → [M, inter]
2880        w4_gemm!(
2881            &self.weights.gate_proj,
2882            self.weights.gate_proj_t,
2883            &self.int8_gate,
2884            &self.q4k_gate,
2885            &self.fp4mmq_gate,
2886            fp4mmq_prefill,
2887            input,
2888            gate_out,
2889            inter,
2890            h,
2891            true
2892        );
2893        ffn_step!("gate_proj", t_ffn);
2894        // up_proj GEMM: [M, H] → [M, inter]
2895        w4_gemm!(
2896            &self.weights.up_proj,
2897            self.weights.up_proj_t,
2898            &self.int8_up,
2899            &self.q4k_up,
2900            &self.fp4mmq_up,
2901            fp4mmq_prefill,
2902            input,
2903            up_out,
2904            inter,
2905            h,
2906            true
2907        );
2908        ffn_step!("up_proj", t_ffn);
2909
2910        // LoRA gate/up deltas land here: the projections are complete and, with
2911        // the MMQ arm disabled above, gate_out/up_out hold true-valued BF16 —
2912        // so the delta adds in the same units it was trained in.
2913        self.apply_lora_gate_up(ctx, input, gate_out, up_out, m, stream)?;
2914        // activation(gate) * up for all M tokens (SiLU or GELU)
2915        let fused_down_quant = fp4mmq_down && self.nvfp4_silu_quant_k.0 != 0;
2916        if fused_down_quant {
2917            // Fused SiLU-mul + quantize straight into the down MMQ's y-format: the
2918            // [M, inter] bf16 intermediate is never written or re-read (that round-trip
2919            // is why the unfused down arm measured neutral). scale2 folds happen inside,
2920            // pre-clamp — identical math to the two-step path below.
2921            ops::nvfp4_silu_mul_quant(
2922                ctx.gpu,
2923                self.nvfp4_silu_quant_k,
2924                gate_out,
2925                up_out,
2926                fp4_y,
2927                self.weights.gate_proj.weight_scale_2,
2928                self.weights.up_proj.weight_scale_2,
2929                m,
2930                inter,
2931                stream,
2932            )?;
2933        } else if fp4mmq_prefill {
2934            // FP4-MMQ outputs are missing the per-tensor FP32 scale2 (the hardware MMA
2935            // applies only the per-16 e4m3 scales) — fold it here, before the nonlinearity.
2936            ops::nvfp4_silu_mul_scaled(
2937                ctx.gpu,
2938                self.nvfp4_silu_scaled_k,
2939                gate_out,
2940                up_out,
2941                gate_out,
2942                self.weights.gate_proj.weight_scale_2,
2943                self.weights.up_proj.weight_scale_2,
2944                m * inter,
2945                stream,
2946            )?;
2947        } else {
2948            ops::silu_mul(
2949                ctx.gpu,
2950                self.act_mul,
2951                gate_out,
2952                up_out,
2953                gate_out,
2954                m * inter,
2955                stream,
2956            )?;
2957        }
2958
2959        // W4A4 opt #1: quantize the down input (SiLU(gate)*up, `[M, inter]`) to NVFP4.
2960        if fp4_prefill {
2961            ops::quantize_bf16_to_nvfp4(
2962                ctx.gpu,
2963                self.quantize_nvfp4_k,
2964                gate_out,
2965                nvfp4_a_packed,
2966                nvfp4_a_scale,
2967                m,
2968                inter,
2969                stream,
2970            )?;
2971        }
2972        // Q4_K opt: quantize the down input (SiLU(gate)*up, `[M, inter]`) to q8_1.
2973        // Skip when the hybrid routes down to faith2 (it does its own int8 requant).
2974        if q4k_prefill && !down_faith2 {
2975            ops::quantize_act_q8_1(
2976                ctx.gpu,
2977                self.q4k_quant_act_k,
2978                gate_out,
2979                q4k_a,
2980                m,
2981                inter,
2982                stream,
2983            )?;
2984        }
2985        // FP4-MMQ down (two-step fallback, only when the fused kernel is absent):
2986        // quantize the down input (SiLU(gate)*up, `[M, inter]`) to block_fp4_mmq.
2987        if fp4mmq_down && !fused_down_quant {
2988            ops::nvfp4_mmq_quantize_act(
2989                ctx.gpu,
2990                self.nvfp4_quant_act_k,
2991                gate_out,
2992                fp4_y,
2993                m,
2994                inter,
2995                stream,
2996            )?;
2997        }
2998        // down_proj GEMM: [M, inter] → [M, H]
2999        // ($fp4cell is a placeholder — the FP4-MMQ arm is gated off by `false` below;
3000        // down stays on the default path in the FP4-MMQ hybrid.)
3001        let output = ctx.buffers.moe_output();
3002        w4_gemm!(
3003            &self.weights.down_proj,
3004            self.weights.down_proj_t,
3005            &self.int8_down,
3006            &self.q4k_down,
3007            &self.fp4mmq_down,
3008            fp4mmq_down,
3009            gate_out,
3010            output,
3011            h,
3012            inter,
3013            false
3014        );
3015        ffn_step!("down_proj", t_ffn);
3016        // FP4-MMQ down: fold the down-projection's per-tensor scale2 (no SiLU-mul here;
3017        // the consumer is the residual add).
3018        if fp4mmq_down {
3019            ops::nvfp4_scale_bf16(
3020                ctx.gpu,
3021                self.nvfp4_scale_k,
3022                output,
3023                self.weights.down_proj.weight_scale_2,
3024                m * h,
3025                stream,
3026            )?;
3027        }
3028        // AFTER the scale2 fold, not before: that fold scales the base
3029        // projection's output, and the delta is not part of that product.
3030        // `gate_out` holds silu(gate)*up — the activation the base down GEMM
3031        // just contracted over — because the fused silu+quant arm is off
3032        // whenever an adapter is installed.
3033        self.apply_lora_down(ctx, gate_out, output, m, stream)?;
3034
3035        Ok(())
3036    }
3037
3038    /// Batched forward (per-token loop). Used by forward_batched in model loop.
3039    pub fn forward_batched(
3040        &self,
3041        input: DevicePtr,
3042        num_tokens: usize,
3043        ctx: &ForwardContext,
3044        stream: u64,
3045    ) -> Result<()> {
3046        self.forward_prefill(input, num_tokens, ctx, stream)
3047    }
3048}
3049
3050/// The native-FP8 M=1 decode DOWN projection (#928) — the arm rule. A CHILD
3051/// module, not a sibling: this file is already at the CI size cap, and the
3052/// nsys attribution that motivates the arm needs room this file does not have.
3053#[path = "dense_ffn_fp8_down.rs"]
3054pub mod fp8_down;
3055/// W8A8 block-scaled prefill branch (#917/#928). A CHILD module, not a
3056/// sibling: it adds `impl DenseFfnLayer` methods that read this layer's
3057/// private kernel handles, and this file is already at the CI size cap.
3058#[path = "dense_ffn_w8a8_prefill.rs"]
3059pub mod w8a8_prefill;
3060
3061/// The 5..=32-row native-FP8 DECODE tier (#927) — same child-module reason as
3062/// `w8a8_prefill` above: it reads this layer's private kernel handles, and the
3063/// arm's rule, its opt-in and the measurements that made it opt-in do not fit
3064/// in this file's budget.
3065#[path = "dense_ffn_batch16_decode.rs"]
3066pub mod batch16_decode;
3067
3068/// The TENSOR-CORE 5..=32-row decode tier (`AVAROK_FFN_M16_TC`, #927) — same
3069/// child-module reason as the two above: it reads this layer's private kernel
3070/// handles, and its rule, lever and the numerics seam it opens need room.
3071#[path = "dense_ffn_m16_tc.rs"]
3072pub mod m16_tc;
3073
3074/// The FUSED gate+up DECODE GEMM (`ffn_gateup_fused`, #927) — a child module
3075/// because it reads this layer's private kernel handles, and the round-13
3076/// receipt, the layout decision and the residency-neutrality argument need
3077/// room this file does not have.
3078#[path = "dense_ffn_gateup_fused.rs"]
3079pub mod gateup_fused;
3080
3081/// Native BF16/FP8 overlays take precedence over any NVFP4 fallback weights.
3082/// Small batches must use the same format-aware dispatcher as prefill.
3083fn native_small_batch_uses_prefill(has_bf16: bool, has_fp8: bool) -> bool {
3084    has_bf16 || has_fp8
3085}
3086
3087#[cfg(test)]
3088#[path = "dense_ffn_mmq_tests.rs"]
3089mod mmq_tests;
3090
3091#[cfg(test)]
3092#[path = "dense_ffn_native_batch_tests.rs"]
3093mod native_batch_tests;
3094
3095#[cfg(test)]
3096#[path = "dense_ffn_kernel_tests.rs"]
3097mod kernel_tests;
3098
3099/// #915: the native-FP8 route must work with NO NVFP4 fallback weights, so the
3100/// loader can stop allocating 18.4 GiB of them.
3101#[cfg(test)]
3102#[path = "dense_ffn_fp8_residency_tests.rs"]
3103mod fp8_residency_tests;
3104
3105#[cfg(test)]
3106#[path = "dense_ffn_fp8_down_tests.rs"]
3107mod fp8_down_tests;
3108
3109#[cfg(test)]
3110mod tests {
3111    use super::native_small_batch_uses_prefill;
3112
3113    #[test]
3114    fn native_weight_presence_requires_prefill_dispatch() {
3115        assert!(native_small_batch_uses_prefill(true, false));
3116        assert!(native_small_batch_uses_prefill(false, true));
3117        assert!(native_small_batch_uses_prefill(true, true));
3118        assert!(!native_small_batch_uses_prefill(false, false));
3119    }
3120}