metrale_model_layers/layers/dense_ffn_gateup_fused.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The fused dense-FFN gate+up decode arm: one W8A8 block-scaled GEMM at `N = 2 * inter`, not two.
4//!
5//! Owner: model-layers (dense FFN).
6//! Invariants:
7//! - The arm is taken only when `gateup_fused_selected` holds: lever on for a
8//! SiLU layer, the W8A8 arm selected for both gate and up, the fused weight
9//! installed, the strided SiLU handle loaded, `m` in 5..=`GATEUP_FUSED_MAX_M`,
10//! and an output buffer of at least `fused_out_bytes(m, inter)`.
11//!
12//! The loader (`qwen35_dense.rs`) builds one `[2 * inter, K]` E4M3 buffer and
13//! one FP32 scale grid, and re-points `gate_proj` and `up_proj` at views inside
14//! them, so every un-fused rung of `w8_gemm!` reads the same bytes. A fused
15//! output row is `[gate | up]`: gate in columns `[0, inter)`, up in
16//! `[inter, 2 * inter)`. `ops::silu_mul_strided` reads the two halves at a row
17//! stride of `2 * inter` and writes the contiguous `[m, inter]` the down
18//! projection reads.
19//!
20//! Output column `j` of the fused GEMM is the dot product of gate column `j`
21//! (`j < inter`) or up column `j - inter`, over the same K with the same
22//! scales. `examples/native_fp8_ffn_gateup_fused_microtest.rs` gates byte
23//! equality of both halves against the un-fused cuBLASLt GEMMs at M = 5, 8 and
24//! 16. The GEMM is `w8a8_gemm`: cuBLASLt when the dispatch's cuBLASLt scope
25//! includes `ffn` and its other clauses pass, the in-tree
26//! `fp8_gemm_t_blockscaled` otherwise.
27//!
28//! The lever is `kernels/<hw>/HARDWARE.toml` `[defaults] ffn_gateup_fused`:
29//! true on hopper, false on gb10, b200 and b300. `silu_mul_strided.cu` exists
30//! only in `kernels/hopper/common`, so on other targets the SiLU handle is 0
31//! and the arm declines even when `METRALE_FFN_GATEUP_FUSED` turns the lever
32//! on.
33
34use anyhow::Result;
35use metrale_gpu_runtime::buffers::GATEUP_FUSED_MAX_M;
36use metrale_gpu_runtime::gpu::DevicePtr;
37
38use super::DenseFfnLayer;
39use crate::layer::ForwardContext;
40use crate::layers::ops;
41use crate::weight_map::Fp8Weight;
42
43/// 2026-09-25: Whether the fused gate+up arm is on for this process:
44/// `[defaults] ffn_gateup_fused`, overridden by `METRALE_FFN_GATEUP_FUSED`
45/// (`0`, `false`, `off` or `no` turns it off; any other value turns it on).
46pub fn ffn_gateup_fused() -> bool {
47 ops::target_defaults::resolved().ffn_gateup_fused.value
48}
49
50/// 2026-09-25: Bytes the fused `[ceil16(m), 2 * inter]` BF16 output occupies:
51/// `cublas_fp8_proj_prequant` hands cuBLASLt `ceil16(m)` rows and writes all
52/// of them.
53pub(crate) fn fused_out_bytes(m: u32, inter: u32) -> usize {
54 ops::cublas_fp8_m_pad(m) as usize * 2 * inter as usize * 2
55}
56
57/// 2026-09-25: The fused-arm selection rule, as a pure function, so the CPU
58/// tests can pin every clause without a `ForwardContext`. `lever` is injected
59/// because [`ffn_gateup_fused`] reads a process-global `OnceLock`.
60///
61/// Clauses:
62///
63/// * `lever`: the target's declaration with the environment override applied.
64/// * `gate_up_w8a8`: the W8A8 arm was selected for both gate and up. The fused
65/// GEMM is that arm at twice the N, so a pair the ladder would have put on a
66/// W8A16 rung must not be fused.
67/// * `5..=GATEUP_FUSED_MAX_M`: the decode band.
68/// * `fused_installed`: the loader built the `[2 * inter, K]` weight.
69/// * `silu_strided_loaded`: the strided SiLU consumer's handle is loaded.
70/// * `out_capacity_bytes`: the buffer holds `fused_out_bytes(m, inter)`, the
71/// padded extent. A short buffer declines instead of being written past its
72/// end.
73#[allow(clippy::too_many_arguments)]
74pub(crate) fn gateup_fused_selected(
75 m: u32,
76 inter: u32,
77 lever: bool,
78 gate_up_w8a8: bool,
79 fused_installed: bool,
80 silu_strided_loaded: bool,
81 out_capacity_bytes: usize,
82) -> bool {
83 lever
84 && gate_up_w8a8
85 && fused_installed
86 && silu_strided_loaded
87 && (5..=GATEUP_FUSED_MAX_M as u32).contains(&m)
88 && fused_out_bytes(m, inter) <= out_capacity_bytes
89}
90
91impl DenseFfnLayer {
92 /// 2026-09-26: Whether this layer fuses gate+up for `m` rows.
93 ///
94 /// `gate_up_w8a8` comes from the caller (`prefill_fp8`), which
95 /// already resolved the W8A8 rule for both projections; asking again here
96 /// would be a second copy of that rule.
97 pub(crate) fn gateup_fused_plan(
98 &self,
99 ctx: &ForwardContext,
100 m: u32,
101 inter: u32,
102 gate_up_w8a8: bool,
103 ) -> Option<&Fp8Weight> {
104 let fused = self.fp8_gate_up_fused.as_ref();
105 // 2026-09-25: SiLU only: the consumer this arm launches is the SiLU-mul,
106 // so any other activation keeps the `w8_gemm!` pair and `self.act_mul`.
107 let silu = self.activation == super::FfnActivation::SiLU;
108 gateup_fused_selected(
109 m,
110 inter,
111 self.gateup_fused && silu,
112 gate_up_w8a8,
113 fused.is_some(),
114 self.silu_mul_strided_k.0 != 0,
115 ctx.buffers.ffn_gate_up_fused_bytes(),
116 )
117 .then_some(fused)
118 .flatten()
119 }
120
121 /// 2026-09-25: gate+up in one GEMM into the arena's `ffn_gate_up_fused`, then
122 /// the strided SiLU-mul into the contiguous `[m, inter]` at `gate_out`.
123 /// `the_fused_arm_allocates_nothing_per_call` checks that the in-tree kernel
124 /// path allocates nothing.
125 #[allow(clippy::too_many_arguments)]
126 pub(crate) fn w8a8_gate_up_fused(
127 &self,
128 ctx: &ForwardContext,
129 a_fp8: DevicePtr,
130 a_scale: DevicePtr,
131 fused_w: &Fp8Weight,
132 gate_out: DevicePtr,
133 m: u32,
134 inter: u32,
135 h: u32,
136 stream: u64,
137 ) -> Result<()> {
138 self.log_gateup_fused_route(ctx, m);
139 let fused_out = ctx.buffers.ffn_gate_up_fused();
140 let cap = ctx.buffers.ffn_gate_up_fused_bytes();
141 debug_assert!(fused_out_bytes(m, inter) <= cap);
142 self.w8a8_gemm(
143 ctx,
144 a_fp8,
145 a_scale,
146 fused_w,
147 fused_out,
148 cap,
149 m,
150 2 * inter,
151 h,
152 stream,
153 )?;
154 // 2026-09-25: `up` starts `inter` BF16 elements into each fused row.
155 const BF16: usize = 2;
156 ops::silu_mul_strided(
157 ctx.gpu,
158 self.silu_mul_strided_k,
159 fused_out,
160 fused_out.offset(inter as usize * BF16),
161 gate_out,
162 m,
163 inter,
164 2 * inter,
165 inter,
166 stream,
167 )
168 }
169
170 /// 2026-09-25: Logs the route once per model (`log:ffn_gateup_fused`).
171 fn log_gateup_fused_route(&self, ctx: &ForwardContext, m: u32) {
172 if ctx.stats.once("log:ffn_gateup_fused") {
173 tracing::info!(
174 "[metrale] dense FFN decode: gate+up FUSED into ONE W8A8 \
175 block-scaled GEMM at N=2*intermediate (m={m}, band 5..={max}) \
176 — same weight bytes, one launch instead of two. Round 13 \
177 priced the un-fused pair at 5 730.5 us/step, 59.4% of HBM, \
178 against `down`'s 71.4% for the same bytes in one launch. \
179 Bit-identical per element; METRALE_FFN_GATEUP_FUSED=0 restores \
180 the two-GEMM arm (#927).",
181 max = GATEUP_FUSED_MAX_M,
182 );
183 }
184 }
185}
186
187#[cfg(test)]
188#[path = "dense_ffn_gateup_fused_tests.rs"]
189mod tests;