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}