metrale_model_layers/layers/
dense_ffn_fp8_down.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Chooses the arm that runs the native-FP8 decode FFN for one token
4//! (`Fp8DownArm`), as a pure function `DenseFfnLayer::forward` calls.
5//!
6//! Owner: model-layers (dense FFN).
7//! Invariants:
8//! - `fp8_down_arm` returns `PerProjection` for a non-SiLU layer or a missing
9//!   `w8a16_gemv_dual`, and returns `SplitSilu` or `FusedSilu` only when every kernel
10//!   that arm launches resolved.
11//!
12//! Why `SplitSilu` is the preferred arm: `w8a16_gemv_silu_input` gives each of the 4
13//! outputs of a 256-thread CTA a 64-lane team that walks the whole K and evaluates
14//! `(g / (1 + __expf(-g))) * u` for every element, so one launch computes the SwiGLU once
15//! per output rather than once per element. `SplitSilu` computes it once with
16//! `moe_silu_mul` and then runs the plain `w8a16_gemv`. Measured 2026-09-11 on 1xH100,
17//! Qwen/Qwen3.8-27B-FP8, C=1: the fused down projection took 6.65 ms (30.4%) of a
18//! 21.891 ms decode step at 858 GB/s, against 1,979 GB/s for `w8a16_gemv_dual`.
19
20/// 2026-09-25: Which kernels the native-FP8 decode FFN runs for one token.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub(crate) enum Fp8DownArm {
23    /// 2026-09-25: `w8a16_gemv_dual`, then `moe_silu_mul` writes `silu(gate)*up` over
24    /// `gate_out`, then the plain `w8a16_gemv` for down. The default:
25    /// `ModelLevers::decode_split_silu` comes from `[defaults] decode_split_silu`, true in
26    /// every `kernels/*/HARDWARE.toml` that declares it, and `METRALE_NO_DECODE_SPLIT_SILU`
27    /// turns it off.
28    ///
29    /// Not bit-identical to `FusedSilu`: `moe_silu_mul` rounds `g * (1 / (1 + e^-g)) * u`
30    /// to BF16 before the GEMV reads it, while the fused kernel keeps
31    /// `(g / (1 + e^-g)) * u` in FP32 inside the dot product. The non-fused FP8 prefill
32    /// path runs the same `moe_silu_mul`.
33    SplitSilu,
34    /// 2026-09-25: `w8a16_gemv_dual`, then the fused `w8a16_gemv_silu_input`. Taken
35    /// when the split-SiLU lever is off or `act_mul` / `w8a16_gemv` did not resolve.
36    FusedSilu,
37    /// 2026-09-25: Neither fused path is usable: four launches, `w8a16_gemv` for gate and
38    /// up, `act_mul`, then `w8a16_gemv` for down.
39    PerProjection,
40}
41
42/// 2026-09-25: The arm rule `DenseFfnLayer::forward` uses, pure so the CPU tests can
43/// cover every combination without a GPU.
44///
45/// `is_silu` is the layer's activation; `split_silu_lever` is
46/// `ModelLevers::decode_split_silu`; every other `bool` is "this handle is nonzero".
47pub(crate) fn fp8_down_arm(
48    is_silu: bool,
49    dual: bool,
50    fused_silu: bool,
51    act_mul: bool,
52    plain_gemv: bool,
53    split_silu_lever: bool,
54) -> Fp8DownArm {
55    // 2026-09-25: Both fused arms read the gate/up pair that `w8a16_gemv_dual` writes
56    // to `gate_out`/`up_out`, so without it neither can run.
57    if !is_silu || !dual {
58        return Fp8DownArm::PerProjection;
59    }
60    if split_silu_lever && act_mul && plain_gemv {
61        Fp8DownArm::SplitSilu
62    } else if fused_silu {
63        Fp8DownArm::FusedSilu
64    } else {
65        Fp8DownArm::PerProjection
66    }
67}