metrale_model_layers/layers/
dense_ffn.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Dense gated FFN layer, `down(act(gate(x)) * up(x))` with SiLU or GELU, for
4//! single-token decode, small verify batches and prefill.
5//!
6//! Owner: model-layers (dense FFN).
7//! Invariants:
8//! - On success, every forward entry point leaves its `[rows, hidden]` BF16 output in
9//!   `ctx.buffers.moe_output()`; decode `forward` also returns that pointer.
10//! - `forward`, `forward_k2`, `forward_k3` and `forward_prefill` serve an installed
11//!   packed-Q2 overlay first, then native FP8, then BF16, and use the NVFP4 weights only
12//!   when none is installed.
13
14use anyhow::Result;
15use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
16
17// 2026-09-26: Imported so the moved bodies in `dense_ffn_init.rs` and `dense_ffn_overlays.rs` keep
18// calling these as `super::`.
19use super::{k64_kernel, tgemm_kernel, try_kernel, try_target_kernel, w4a16_v2_kernel};
20use crate::layer::ForwardContext;
21use crate::layers::ops;
22use crate::layers::w4a16_gemv_tiers::W4a16BatchmTiers;
23use crate::weight_map::Fp8Weight;
24
25/// 2026-09-26: The public weight sets and `FfnActivation`.
26#[path = "dense_ffn_weights.rs"]
27mod weights;
28pub use weights::{
29    DenseFfnWeights, DenseFfnWeightsBf16, DenseFfnWeightsFp8, DenseFfnWeightsQ2, FfnActivation,
30};
31use weights::{Fp4MmqWeight, Int8Weight, Q4kWeight};
32
33pub struct DenseFfnLayer {
34    pub weights: DenseFfnWeights,
35    activation: FfnActivation,
36    w4a16_gemv: KernelHandle,
37    /// 2026-09-25: Single-warp `w4a16_gemv_sw`. `KernelHandle(0)` on miss, which selects the base
38    /// GEMV. `examples/w4a16_gemv_sw_microtest.rs` requires it to match `w4a16_gemv` bit for bit.
39    w4a16_gemv_sw: KernelHandle,
40    w4a16_gemv_dual: KernelHandle,
41    w4a16_gemv_silu_input: KernelHandle,
42    // 2026-09-25: Single-warp-per-output decode variants, used when `ModelLevers::gemv_sw` is on
43    // (unless `METRALE_NO_GEMV_SW=1`) and the handle resolved; otherwise the 64-thread
44    // kernels run.
45    w4a16_gemv_dual_sw: KernelHandle,
46    w4a16_gemv_silu_input_sw: KernelHandle,
47    w4a16_gemv_dual_batch2: KernelHandle,
48    w4a16_gemv_dual_batch3: KernelHandle,
49    w4a16_gemv_batch2: KernelHandle,
50    w4a16_gemv_batch3: KernelHandle,
51    /// 2026-09-25: The `w4a16_gemv_batch{M}` tiers `forward_km` launches.
52    /// `W4a16BatchmTiers::kernel` picks the tier for `m` and returns a zero handle when none serves
53    /// it.
54    w4a16_batchm: W4a16BatchmTiers,
55    w4a16_gemm: KernelHandle,
56    w4a16_gemm_t_m128_k: KernelHandle,
57    // 2026-09-25: 8-warp variant of `w4a16_gemm_t_m128`, preferred over it when resolved.
58    // `w4a16_v2_kernel` resolves it only under `METRALE_W4A16_VARIANT=v2` or `v3`.
59    w4a16_gemm_t_m128_v2_k: KernelHandle,
60    // 2026-09-25: BF16 tensor-core variant of `w4a16_gemm_t_m128`, used for prefill only when
61    // `METRALE_BF16_TC_PREFILL` is set. `examples/w4a16_bf16_v2_microtest.rs` compares it with
62    // the base `w4a16_gemm` by cosine, not bit for bit.
63    w4a16_gemm_t_m128_bf16_k: KernelHandle,
64    // 2026-09-25: Second BF16 tensor-core variant, chosen over `w4a16_gemm_t_m128_bf16` when it
65    // resolved unless `METRALE_DISABLE_PREFILL_V2` is set. `examples/w4a16_bf16_v2_microtest.rs`
66    // requires its output to equal that kernel's bit for bit.
67    w4a16_gemm_t_m128_bf16_v2_k: KernelHandle,
68    // 2026-09-25: `w4a16_gemm_t` (M tile 64; `tgemm_kernel` prefers the `_p3` variant).
69    // `w4a16_prefill_gemm` uses it for m <= 64, and `METRALE_FP8_M64_PREFILL` sends every
70    // prefill projection that has a transposed copy through it.
71    w4a16_gemm_t_k: KernelHandle,
72    // 2026-09-25: int8 prefill (`METRALE_INT8_PREFILL`, and the down projection under
73    // `METRALE_FFN_MMQ`): `requant_w_nvfp4_int8` converts each NVFP4 weight once into the
74    // `OnceLock`s below, and `requant_a_bf16_int8` converts the BF16 activation on every call
75    // into the arena's `ffn_act_a` / `ffn_act_scale`. A zero `int8_faith2_k` disables both.
76    int8_faith2_k: KernelHandle,
77    // 2026-09-25: `int8_gemm_i32acc`, which replaces `int8_gemm_faith2` in the int8 arms when
78    // `METRALE_INT8_FAITH5` is set.
79    int8_faith5_k: KernelHandle,
80    requant_w_int8_k: KernelHandle,
81    requant_a_int8_k: KernelHandle,
82    int8_gate: std::sync::OnceLock<Int8Weight>,
83    int8_up: std::sync::OnceLock<Int8Weight>,
84    int8_down: std::sync::OnceLock<Int8Weight>,
85    // 2026-09-25: W4A4 prefill (`METRALE_FP4_PREFILL`): `w4a4_gemm` reads the NVFP4 weights as they
86    // are, and `quantize_bf16_to_nvfp4` writes the activation into the arena's `ffn_act_a` /
87    // `ffn_act_scale`. Taken only when both handles resolved.
88    w4a4_gemm_k: KernelHandle,
89    quantize_nvfp4_k: KernelHandle,
90    // 2026-09-25: Q4_K MMQ prefill (`METRALE_FFN_MMQ`): each NVFP4 weight is dequantized to BF16
91    // and quantized to `block_q4_K` once (`ensure_q4k_weight`), and the activation is
92    // quantized to q8_1 on every call into the arena's `ffn_act_q8`. All four handles must
93    // resolve.
94    q4k_mmq_nc_k: KernelHandle,
95    q4k_mmq_wc_k: KernelHandle,
96    q4k_quant_act_k: KernelHandle,
97    q4k_quant_w_k: KernelHandle,
98    dequant_nvfp4_bf16_k: KernelHandle,
99    q4k_gate: std::sync::OnceLock<Q4kWeight>,
100    q4k_up: std::sync::OnceLock<Q4kWeight>,
101    q4k_down: std::sync::OnceLock<Q4kWeight>,
102    // 2026-09-25: NVFP4 MMQ prefill, on by default (`ModelLevers::ffn_nvfp4_mmq`, off with
103    // `METRALE_NO_FFN_NVFP4_MMQ`) for SiLU layers without a LoRA adapter.
104    // `finalize_nvfp4_mmq_load` repacks the weights to `block_nvfp4` at load, and the
105    // activation is quantized on every call into the arena's `ffn_act_q8`. Each projection's
106    // `weight_scale_2` is applied after the GEMM (`nvfp4_silu_mul_scaled`,
107    // `nvfp4_silu_mul_quant` or `nvfp4_scale_bf16`).
108    nvfp4_mmq_nc_k: KernelHandle,
109    nvfp4_mmq_wc_k: KernelHandle,
110    /// 2026-09-25: The 16-, 32- and 64-row MMQ tiles, chosen for m <= 16, 32 or 64
111    /// (`mmq_small_tile_enabled`, `mmq_tile64_enabled`). A zero handle keeps the 128-row tile.
112    nvfp4_mmq16_nc_k: KernelHandle,
113    nvfp4_mmq16_wc_k: KernelHandle,
114    nvfp4_mmq32_nc_k: KernelHandle,
115    nvfp4_mmq32_wc_k: KernelHandle,
116    nvfp4_mmq64_nc_k: KernelHandle,
117    nvfp4_mmq64_wc_k: KernelHandle,
118    nvfp4_quant_act_k: KernelHandle,
119    nvfp4_repack_k: KernelHandle,
120    nvfp4_silu_scaled_k: KernelHandle,
121    nvfp4_silu_quant_k: KernelHandle,
122    nvfp4_scale_k: KernelHandle,
123    fp4mmq_gate: std::sync::OnceLock<Fp4MmqWeight>,
124    fp4mmq_up: std::sync::OnceLock<Fp4MmqWeight>,
125    fp4mmq_down: std::sync::OnceLock<Fp4MmqWeight>,
126    // 2026-09-25: Deep-K variant of `w4a16_gemm_t` (`k64_kernel` prefers `_p3`).
127    // `w4a16_prefill_gemm` uses it for m <= 64 when k >= `w4a16_k64_min_k()` and k % 64 == 0.
128    w4a16_gemm_t_k64_k: KernelHandle,
129    /// 2026-09-25: SiLU(gate)*up or GELU(gate)*up, depending on the activation.
130    act_mul: KernelHandle,
131    /// 2026-09-25: Set by `set_bf16_weights`. When present and no packed-Q2 or FP8 overlay is
132    /// installed, every forward path uses the BF16 kernels.
133    bf16_weights: Option<DenseFfnWeightsBf16>,
134    dense_gemv_bf16_k: KernelHandle,
135    dense_gemm_bf16_k: KernelHandle,
136    // 2026-09-25: Tensor-core BF16 GEMM for BF16 and packed-Q2 prefill; a zero handle falls back to
137    // `dense_gemm_bf16`. For BF16 prefill, cuBLASLt comes first when `METRALE_CUBLAS_GEMM`
138    // arms the `ffn` family.
139    dense_gemm_tc_k: KernelHandle,
140    /// 2026-09-25: Set by `set_fp8_weights`. When present (and no packed-Q2 overlay), every forward
141    /// path runs FP8 kernels: `forward_k2`, `forward_k3` and `forward_km` hand the layer to
142    /// `forward_prefill`.
143    fp8_weights: Option<DenseFfnWeightsFp8>,
144    /// 2026-09-28: W8A8 gate, up and down (`set_w8a8_decode_weights`), for a checkpoint that
145    /// declares them FP8 W8A8; run ahead of every other arm at 1..=64 decode rows.
146    pub(crate) w8a8: Option<crate::layers::W8a8Ffn>,
147    w8a16_gemv_k: KernelHandle,
148    w8a16_gemm_k: KernelHandle,
149    w8a16_gemv_batch4_k: KernelHandle,
150    /// 2026-09-25: `w8a16_gemv_batch16` for the opt-in 5..=32-row tier; zero when the target lacks
151    /// it. Rule: `batch16_decode::batch16_plan`.
152    w8a16_gemv_batch16_k: KernelHandle,
153    /// 2026-09-25: Whether the batch16 tier is armed (`METRALE_FFN_BATCH16=1`), copied from
154    /// `ffn_batch16_enabled()` at construction. A field, so the dispatch tests can set either
155    /// value without touching the process-wide `OnceLock`.
156    batch16_enabled: bool,
157    /// 2026-09-25: `w8a16_gemm_m16`, the tensor-core tier `w8_gemm!` tries ahead of batch16 at
158    /// 5..=32 rows when `m16_tc` is on; zero when the target lacks the module. Rule:
159    /// `m16_tc::m16_tc_plan`.
160    w8a16_gemm_m16_k: KernelHandle,
161    /// 2026-09-25: The 64-wide N tile twin, used when `METRALE_FFN_M16_TC_NTILE=64` and it
162    /// resolved; otherwise the 32-wide kernel runs (`m16_tc::m16_tc_kernel`).
163    w8a16_gemm_m16_n64_k: KernelHandle,
164    /// 2026-09-25: `m16_tc::m16_tc_levers().ffn`, copied at construction: the target's `[defaults]
165    /// ffn_m16_tc`, overridden by `METRALE_FFN_M16_TC`, or by `METRALE_M16_TC` when that is
166    /// unset. A field for the same reason as `batch16_enabled`.
167    m16_tc: bool,
168    /// 2026-09-25: The CTA N width this layer asks for, 32 or 64.
169    m16_tc_n_tile: u32,
170    w8a16_gemm_pipelined_k: KernelHandle,
171    // 2026-09-25: Fused FP8 decode GEMVs: gate+up in one launch, and `silu(gate)*up` computed
172    // inside the down GEMV. `fp8_down::fp8_down_arm` decides which run.
173    w8a16_gemv_dual_k: KernelHandle,
174    w8a16_gemv_silu_input_k: KernelHandle,
175    // 2026-09-25: Transposed-weight FP8 prefill GEMM. `forward_prefill_inner` passes no transposed
176    // FP8 copy (`gate_t` / `up_t` / `down_t` are `None`), so its `w8_gemm!` arm never matches.
177    w8a16_gemm_t_m128_k: KernelHandle,
178    // 2026-09-25: The W8A8 prefill pair: per-token, 128-group FP8 activation quantization and the
179    // block-scaled FP8 x FP8 GEMM. Rule: `w8a8_prefill::w8a8_prefill_selected`.
180    per_token_group_quant_fp8_k: ops::Fp8ActQuant,
181    fp8_gemm_t_blockscaled_k: KernelHandle,
182    // 2026-09-25: Activation-scale layout adapter for the cuBLASLt arm of `w8a8_gemm`. When
183    // `ops::cublas_scale_layout_kmajor()` holds, a zero handle makes that arm decline and the
184    // in-tree GEMM run.
185    fp8_act_scale_kmajor_k: KernelHandle,
186    /// 2026-09-25: The `[2*inter, hidden]` FP8 gate+up weight installed by `set_fp8_gate_up_fused`;
187    /// `None` unless the loader built it. The installer's contract is that `gate_proj` and
188    /// `up_proj` are views into it.
189    fp8_gate_up_fused: Option<Fp8Weight>,
190    /// 2026-09-25: `gateup_fused::ffn_gateup_fused()` at construction: the target's `[defaults]
191    /// ffn_gateup_fused`, overridden by `METRALE_FFN_GATEUP_FUSED`. It also decides whether
192    /// `silu_mul_strided_k` is looked up.
193    gateup_fused: bool,
194    /// 2026-09-25: The strided SiLU·mul that reads the fused `[m, 2*inter]` output. Its source
195    /// exists only under `kernels/hopper/common/`, so the handle is zero on other targets, and it
196    /// is looked up only when `gateup_fused` is on.
197    silu_mul_strided_k: KernelHandle,
198    /// 2026-09-25: LoRA overlay for gate/up/down, set by `set_lora_weights` (NVFP4 layers only).
199    /// `apply_lora_gate_up` runs after the gate/up projections and before the activation;
200    /// `apply_lora_down` runs after the down projection. `forward` (split-SiLU path),
201    /// `forward_k2`, `forward_k3`, `forward_km` and the NVFP4 part of `forward_prefill` call
202    /// both. The `ModelLevers::decode_ffn_via_gemm` branch of `forward` calls neither.
203    lora: Option<ops::lora_delta::LoraFfnWeights>,
204
205    /// 2026-09-25: Set by `set_q2_weights`. Checked first by `forward`, `forward_k2`, `forward_k3`
206    /// and `forward_prefill`.
207    q2_weights: Option<DenseFfnWeightsQ2>,
208    q2_0_gemv_k: KernelHandle,
209    // 2026-09-25: Batched packed-Q2 decode GEMV, used by `forward_km_q2`.
210    #[allow(dead_code)]
211    q2_0_gemv_batchm_k: KernelHandle,
212    // 2026-09-25: Packed-Q2 to BF16 dequant for packed-Q2 prefill when the Q2_0 MMQ arm is not
213    // taken: each projection is dequantized into the arena's `q2_dequant_scratch` and run
214    // through the BF16 GEMM.
215    dequant_q2_0_gn_k: KernelHandle,
216    // 2026-09-25: Q2_0 MMQ prefill (`METRALE_GGUF_NATIVE_Q2_MMQ=1`, group-128 weights) against a
217    // q8_1 activation from `q4k_quant_act_k`. Looked up by `set_q2_weights`; a zero handle leaves
218    // prefill on the dequant path.
219    q2_0_mmq_nc_k: KernelHandle,
220    q2_0_mmq_wc_k: KernelHandle,
221}
222
223/// 2026-09-25: The 16/32/64-row MMQ tiles: on unless `METRALE_NO_MMQ_SMALL_TILE` is exactly `1`.
224/// Read once per process.
225fn mmq_small_tile_enabled() -> bool {
226    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
227    *ON.get_or_init(|| std::env::var("METRALE_NO_MMQ_SMALL_TILE").as_deref() != Ok("1"))
228}
229
230/// 2026-09-25: The 64-row MMQ tile: on unless `METRALE_NO_MMQ_TILE64` is exactly `1`;
231/// `METRALE_NO_MMQ_SMALL_TILE=1` also turns it off. Read once per process.
232fn mmq_tile64_enabled() -> bool {
233    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
234    *ON.get_or_init(|| std::env::var("METRALE_NO_MMQ_TILE64").as_deref() != Ok("1"))
235}
236impl DenseFfnLayer {
237    pub fn new(weights: DenseFfnWeights, gpu: &dyn GpuBackend) -> Result<Self> {
238        Self::new_with_activation(weights, FfnActivation::SiLU, gpu)
239    }
240
241    /// 2026-09-25: Load-time setup for the Q4_K MMQ prefill arm. Returns at once for packed-Q2 or
242    /// FP8 layers, when one of the four Q4_K kernels is missing, or when `METRALE_FFN_MMQ` is
243    /// unset. Otherwise it builds the gate/up Q4_K copies and the down copy (int8 when
244    /// `int8_gemm_faith2` and `requant_a_bf16_int8` resolved and `METRALE_FFN_MMQ_DOWN_Q4K` is
245    /// unset, Q4_K otherwise), synchronizes `stream`, and frees the transposed `_t` copies.
246    pub fn finalize_q4k_load(
247        &mut self,
248        gpu: &dyn GpuBackend,
249        h: u32,
250        inter: u32,
251        stream: u64,
252    ) -> Result<()> {
253        // 2026-09-25: Packed-Q2 prefill never reaches the Q4_K arm (`forward_prefill_inner`), so
254        // the copies would be unused.
255        if self.q2_weights.is_some() {
256            return Ok(());
257        }
258        // 2026-09-25: FP8 prefill returns before the Q4_K arm, so the copies would be unused.
259        if self.fp8_weights.is_some() {
260            return Ok(());
261        }
262        let q4k_active = self.q4k_mmq_nc_k.0 != 0
263            && self.q4k_quant_act_k.0 != 0
264            && self.q4k_quant_w_k.0 != 0
265            && self.dequant_nvfp4_bf16_k.0 != 0
266            && std::env::var_os("METRALE_FFN_MMQ").is_some();
267        if !q4k_active {
268            return Ok(());
269        }
270        // 2026-09-25: Build the copies before freeing the `_t` weights.
271        self.build_q4k_gate_up(gpu, h, inter, stream)?;
272        let down_faith2 = self.int8_faith2_k.0 != 0
273            && self.requant_a_int8_k.0 != 0
274            && std::env::var_os("METRALE_FFN_MMQ_DOWN_Q4K").is_none();
275        self.finish_q4k_load(gpu, h, inter, stream, down_faith2)
276    }
277
278    /// 2026-09-25: Load-time setup for the NVFP4 MMQ prefill arm. Returns at once for packed-Q2 or
279    /// FP8 layers, GELU layers, when a kernel it needs is missing, or when
280    /// `METRALE_NO_FFN_NVFP4_MMQ` is set. Otherwise it repacks gate and up, and down unless
281    /// `METRALE_NO_FFN_NVFP4_MMQ_DOWN` is set, synchronizes `stream`, and frees the transposed `_t`
282    /// copies of the repacked projections.
283    pub fn finalize_nvfp4_mmq_load(
284        &mut self,
285        gpu: &dyn GpuBackend,
286        h: u32,
287        inter: u32,
288        stream: u64,
289    ) -> Result<()> {
290        // 2026-09-25: Packed-Q2 prefill never reaches the MMQ arm, so a repack would be unused.
291        if self.q2_weights.is_some() {
292            return Ok(());
293        }
294        // 2026-09-25: FP8 prefill returns before the MMQ arm, so a repack would be unused.
295        if self.fp8_weights.is_some() {
296            return Ok(());
297        }
298        let active = self.nvfp4_mmq_nc_k.0 != 0
299            && self.nvfp4_quant_act_k.0 != 0
300            && self.nvfp4_repack_k.0 != 0
301            && self.nvfp4_silu_scaled_k.0 != 0
302            && matches!(self.activation, FfnActivation::SiLU)
303            && self.mmq_gate_up_declared()
304            && std::env::var_os("METRALE_NO_FFN_NVFP4_MMQ").is_none();
305        if !active {
306            return Ok(());
307        }
308        self.build_nvfp4_mmq_gate_up(gpu, h, inter, stream)?;
309        let down_mmq = std::env::var_os("METRALE_NO_FFN_NVFP4_MMQ_DOWN").is_none();
310        self.finish_nvfp4_mmq_load(gpu, h, inter, stream, down_mmq)
311    }
312
313    /// 2026-09-26: The BF16 branch of `forward_prefill_inner` (`dense_ffn_prefill.rs`). It reads
314    /// `dispatch.cublas.ffn`, which `ops/dispatch_config_routing_tests.rs` allows only in this
315    /// file, `dense_ffn_w8a8_prefill.rs` and `moe/`.
316    fn prefill_bf16(
317        &self,
318        bf16w: &DenseFfnWeightsBf16,
319        input: DevicePtr,
320        ctx: &ForwardContext,
321        m: u32,
322        h: u32,
323        inter: u32,
324        gate_out: DevicePtr,
325        up_out: DevicePtr,
326        stream: u64,
327    ) -> Result<()> {
328        let tc = self.dense_gemm_tc_k.0 != 0;
329        // 2026-09-25: BF16 prefill GEMM: cuBLASLt when `METRALE_CUBLAS_GEMM` arms the `ffn`
330        // family, else `dense_gemm_tc` when it resolved, else `dense_gemm_bf16`.
331        macro_rules! ffn_gemm {
332            ($a:expr, $b:expr, $c:expr, $n:expr, $k:expr) => {
333                if ctx.dispatch.cublas.ffn {
334                    ops::cublas_bf16_proj_dense($a, $b.weight, $c, m, $n, $k, stream)?;
335                } else if tc {
336                    ops::dense_gemm_tc(
337                        ctx.gpu,
338                        self.dense_gemm_tc_k,
339                        $a,
340                        $b,
341                        $c,
342                        m,
343                        $n,
344                        $k,
345                        stream,
346                    )?;
347                } else {
348                    ops::dense_gemm(
349                        ctx.gpu,
350                        self.dense_gemm_bf16_k,
351                        $a,
352                        $b,
353                        $c,
354                        m,
355                        $n,
356                        $k,
357                        stream,
358                    )?;
359                }
360            };
361        }
362        ffn_gemm!(input, &bf16w.gate_proj, gate_out, inter, h);
363        ffn_gemm!(input, &bf16w.up_proj, up_out, inter, h);
364        ops::silu_mul(
365            ctx.gpu,
366            self.act_mul,
367            gate_out,
368            up_out,
369            gate_out,
370            m * inter,
371            stream,
372        )?;
373        let output = ctx.buffers.moe_output();
374        ffn_gemm!(gate_out, &bf16w.down_proj, output, h, inter);
375        Ok(())
376    }
377
378    /// 2026-09-25: Same as `forward_prefill`.
379    pub fn forward_batched(
380        &self,
381        input: DevicePtr,
382        num_tokens: usize,
383        ctx: &ForwardContext,
384        stream: u64,
385    ) -> Result<()> {
386        self.forward_prefill(input, num_tokens, ctx, stream)
387    }
388}
389
390/// 2026-09-25: Arm choice for the native-FP8 decode FFN.
391#[path = "dense_ffn_fp8_down.rs"]
392pub mod fp8_down;
393/// 2026-09-25: The W8A8 block-scaled FP8 prefill arm.
394#[path = "dense_ffn_w8a8_prefill.rs"]
395pub mod w8a8_prefill;
396
397/// 2026-09-25: The opt-in 5..=32-row native-FP8 decode tier.
398#[path = "dense_ffn_batch16_decode.rs"]
399pub mod batch16_decode;
400
401/// 2026-09-25: The tensor-core 5..=32-row FP8 tier (`m16_tc`).
402#[path = "dense_ffn_m16_tc.rs"]
403pub mod m16_tc;
404
405/// 2026-09-25: The fused FP8 gate+up GEMM (`ffn_gateup_fused`).
406#[path = "dense_ffn_gateup_fused.rs"]
407pub mod gateup_fused;
408
409/// 2026-09-26: `new_with_activation`, which resolves every kernel handle.
410#[path = "dense_ffn_init.rs"]
411mod init;
412#[path = "dense_ffn_w8a8.rs"]
413mod w8a8;
414
415/// 2026-09-26: The `ensure_*_weight` builders and the steps of the `finalize_*_load` functions.
416#[path = "dense_ffn_load.rs"]
417mod load;
418
419/// 2026-09-26: The weight-overlay installers and the LoRA delta launches.
420#[path = "dense_ffn_overlays.rs"]
421mod overlays;
422
423/// 2026-09-30: The fixed `--activation-quantization` path (`fixed_ok`, `forward_fixed`).
424#[path = "dense_ffn_fixed.rs"]
425mod fixed;
426
427/// 2026-09-26: Single-token decode (`forward`).
428#[path = "dense_ffn_decode.rs"]
429mod decode;
430
431/// 2026-09-26: Small-batch decode (`forward_k2`, `forward_k3`, `forward_km`).
432#[path = "dense_ffn_decode_batch.rs"]
433mod decode_batch;
434
435/// 2026-09-26: Prefill entry point, its packed-Q2 branch, and `w4a16_prefill_gemm`.
436#[path = "dense_ffn_prefill.rs"]
437mod prefill;
438
439/// 2026-09-26: The native-FP8 prefill branch (the `w8_gemm!` ladder).
440#[path = "dense_ffn_prefill_fp8.rs"]
441mod prefill_fp8;
442
443/// 2026-09-26: The NVFP4 prefill branch (the `w4_gemm!` ladder).
444#[path = "dense_ffn_prefill_nvfp4.rs"]
445mod prefill_nvfp4;
446
447/// 2026-09-26: The per-call arm choices of the NVFP4 prefill branch.
448#[path = "dense_ffn_nvfp4_plan.rs"]
449mod nvfp4_plan;
450
451/// 2026-09-25: Whether `forward_k2`, `forward_k3` or `forward_km` must hand the layer to
452/// `forward_prefill`: true when a BF16 or FP8 overlay is installed.
453fn native_small_batch_uses_prefill(has_bf16: bool, has_fp8: bool) -> bool {
454    has_bf16 || has_fp8
455}
456
457#[cfg(test)]
458#[path = "dense_ffn_mmq_tests.rs"]
459mod mmq_tests;
460
461#[cfg(test)]
462#[path = "dense_ffn_native_batch_tests.rs"]
463mod native_batch_tests;
464
465#[cfg(test)]
466#[path = "dense_ffn_kernel_tests.rs"]
467mod kernel_tests;
468
469#[cfg(test)]
470#[path = "dense_ffn_fp8_residency_tests.rs"]
471mod fp8_residency_tests;
472
473#[cfg(test)]
474#[path = "dense_ffn_fp8_down_tests.rs"]
475mod fp8_down_tests;
476
477#[cfg(test)]
478mod tests {
479    use super::native_small_batch_uses_prefill;
480
481    #[test]
482    fn native_weight_presence_requires_prefill_dispatch() {
483        assert!(native_small_batch_uses_prefill(true, false));
484        assert!(native_small_batch_uses_prefill(false, true));
485        assert!(native_small_batch_uses_prefill(true, true));
486        assert!(!native_small_batch_uses_prefill(false, false));
487    }
488}