metrale_model_layers/layers/
dense_ffn_overlays.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The weight overlays a loader installs on a `DenseFfnLayer` (FP8, fused FP8
4//! gate+up, BF16, packed Q2, LoRA) and the LoRA delta launches the forward paths call.
5//!
6//! Owner: model-layers (dense FFN).
7//! Invariants:
8//! - `set_lora_weights` refuses an adapter while an FP8, BF16 or packed-Q2 overlay is
9//!   installed, or when the layer cannot take the split-SiLU decode path.
10
11use anyhow::Result;
12use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
13
14use super::{
15    DenseFfnLayer, DenseFfnWeightsBf16, DenseFfnWeightsFp8, DenseFfnWeightsQ2, FfnActivation,
16};
17use crate::layer::ForwardContext;
18use crate::layers::ops;
19use crate::weight_map::{DenseWeight, Fp8Weight, PackedQ2Weight};
20
21impl DenseFfnLayer {
22    /// 2026-09-25: Install native block-scaled FP8 dense MLP weights. The FP8 kernel handles are
23    /// optional lookups (zero when absent), so the caller installs these only on a target that
24    /// carries the FP8 kernels.
25    pub fn set_fp8_weights(&mut self, gate: Fp8Weight, up: Fp8Weight, down: Fp8Weight) {
26        self.fp8_weights = Some(DenseFfnWeightsFp8 {
27            gate_proj: gate,
28            up_proj: up,
29            down_proj: down,
30        });
31    }
32
33    /// 2026-09-25: Install the `[2*inter, hidden]` FP8 gate+up weight that `gateup_fused_plan` can
34    /// select. Contract: the `gate` and `up` passed to [`Self::set_fp8_weights`] are views into
35    /// `fused` (`gate.weight == fused.weight`, `up.weight == fused.weight + inter*hidden`, and
36    /// likewise for the scale grid). Only the first equality is checked, by `debug_assert!`.
37    /// Otherwise the fused and unfused arms of one layer compute from different bytes.
38    pub fn set_fp8_gate_up_fused(&mut self, fused: Fp8Weight) {
39        debug_assert!(
40            self.fp8_weights
41                .as_ref()
42                .is_some_and(|w| w.gate_proj.weight == fused.weight),
43            "the fused gate+up weight must be the buffer gate_proj is a view into"
44        );
45        self.fp8_gate_up_fused = Some(fused);
46    }
47
48    /// 2026-09-25: Install the LoRA overlay for gate/up/down. Errors when an FP8, BF16 or packed-Q2
49    /// overlay is installed (their branches return before the NVFP4 code that applies the
50    /// deltas), or when the layer cannot take the split-SiLU decode path.
51    pub fn set_lora_weights(&mut self, w: ops::lora_delta::LoraFfnWeights) -> Result<()> {
52        anyhow::ensure!(
53            self.fp8_weights.is_none() && self.bf16_weights.is_none(),
54            "LoRA v0 supports only the NVFP4 dense-FFN path (FP8/BF16 weight \
55             overlays installed on this layer)"
56        );
57        anyhow::ensure!(
58            self.q2_weights.is_none(),
59            "LoRA v0 supports only the NVFP4 dense-FFN path (packed-Q2 weights \
60             installed on this layer)"
61        );
62        // 2026-09-25: The down delta needs `silu(gate)*up` materialised, which only the split-SiLU
63        // decode path does; `forward` takes that path whenever an adapter is installed.
64        anyhow::ensure!(
65            self.activation == FfnActivation::SiLU && self.act_mul.0 != 0 && self.w4a16_gemv.0 != 0,
66            "LoRA v0 needs the split-SiLU decode path (SiLU activation + \
67             act_mul + w4a16_gemv kernels); this layer resolved activation \
68             {:?}, act_mul={}, w4a16_gemv={}",
69            self.activation,
70            self.act_mul.0,
71            self.w4a16_gemv.0,
72        );
73        self.lora = Some(w);
74        Ok(())
75    }
76
77    /// 2026-09-25: `gate_out += ΔW_gate · x` and `up_out += ΔW_up · x` for `m` rows. Call after the
78    /// gate/up projections and before the activation. No launches when the layer has no
79    /// adapter or `METRALE_LORA_NO_FFN=1`.
80    pub(super) fn apply_lora_gate_up(
81        &self,
82        ctx: &ForwardContext,
83        input: DevicePtr,
84        gate_out: DevicePtr,
85        up_out: DevicePtr,
86        m: u32,
87        stream: u64,
88    ) -> Result<()> {
89        if ops::lora_delta::lora_no_ffn() {
90            return Ok(());
91        }
92        let Some(ref lw) = self.lora else {
93            return Ok(());
94        };
95        for (pair, base) in [(&lw.gate, gate_out), (&lw.up, up_out)] {
96            if let Some(pair) = pair.as_ref() {
97                ops::lora_delta::apply_lora_delta(
98                    ctx.gpu,
99                    &lw.kernels,
100                    pair,
101                    input,
102                    base,
103                    m,
104                    ctx.buffers.lora_xa(),
105                    ctx.buffers.lora_delta(),
106                    stream,
107                )?;
108            }
109        }
110        Ok(())
111    }
112
113    /// 2026-09-25: `output += ΔW_down · act`, where `act` is the `silu(gate)*up` the down
114    /// projection read (every caller passes `expert_gate_out`, which the activation overwrites in
115    /// place). No launches in the same cases as `apply_lora_gate_up`.
116    pub(super) fn apply_lora_down(
117        &self,
118        ctx: &ForwardContext,
119        act: DevicePtr,
120        output: DevicePtr,
121        m: u32,
122        stream: u64,
123    ) -> Result<()> {
124        if ops::lora_delta::lora_no_ffn() {
125            return Ok(());
126        }
127        let Some(ref lw) = self.lora else {
128            return Ok(());
129        };
130        let Some(ref pair) = lw.down else {
131            return Ok(());
132        };
133        ops::lora_delta::apply_lora_delta(
134            ctx.gpu,
135            &lw.kernels,
136            pair,
137            act,
138            output,
139            m,
140            ctx.buffers.lora_xa(),
141            ctx.buffers.lora_delta(),
142            stream,
143        )
144    }
145
146    /// 2026-09-25: Install packed Q2_0 dense MLP weights and look up the Q2_0 MMQ kernels. A
147    /// missing `q2_0_gemv_vec`, `q2_0_gemv_vec_batchm` or dequant kernel makes the path that needs
148    /// it return an error.
149    pub fn set_q2_weights(
150        &mut self,
151        gate: PackedQ2Weight,
152        up: PackedQ2Weight,
153        down: PackedQ2Weight,
154        gpu: &dyn GpuBackend,
155    ) {
156        self.q2_weights = Some(DenseFfnWeightsQ2 {
157            gate_proj: gate,
158            up_proj: up,
159            down_proj: down,
160        });
161        self.q2_0_mmq_nc_k = super::try_kernel(gpu, "q2_0_mmq", "metrale_q2_0_mmq128_nc");
162        self.q2_0_mmq_wc_k = super::try_kernel(gpu, "q2_0_mmq", "metrale_q2_0_mmq128_wc");
163    }
164
165    /// 2026-09-25: Install BF16 dense MLP weights. The BF16 kernel handles are optional lookups, so
166    /// the caller installs these only on a target that carries them. `forward_k2`, `forward_k3`
167    /// and `forward_km` then hand the layer to `forward_prefill`, so no NVFP4 kernel reads its
168    /// NVFP4 weights.
169    pub fn set_bf16_weights(&mut self, gate: DenseWeight, up: DenseWeight, down: DenseWeight) {
170        self.bf16_weights = Some(DenseFfnWeightsBf16 {
171            gate_proj: gate,
172            up_proj: up,
173            down_proj: down,
174        });
175    }
176}