spark_model/layers/dense_ffn.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Dense SwiGLU FFN component for non-MoE models.
4//!
5//! Forward: gate = gate_proj(x), up = up_proj(x), out = down_proj(SiLU(gate) * up)
6//! 2 fused kernel launches per decode token (dual GEMV + SiLU-fused down GEMV).
7
8use anyhow::Result;
9use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
10
11use crate::layer::ForwardContext;
12use crate::layers::ops;
13use crate::layers::w4a16_gemv_tiers::W4a16BatchmTiers;
14use crate::weight_map::{
15 DenseWeight, Fp8Weight, Fp8WeightTransposed, PackedQ2Weight, QuantizedWeight,
16};
17
18pub struct DenseFfnWeights {
19 pub gate_proj: QuantizedWeight,
20 pub up_proj: QuantizedWeight,
21 pub down_proj: QuantizedWeight,
22 /// Transposed ([K/2, N]) copies for the fast `w4a16_gemm_t_m128` prefill
23 /// kernel. `None` → prefill falls back to the slow M64xN64 base kernel.
24 /// The non-transposed copies above are kept for the decode gemv path.
25 pub gate_proj_t: Option<QuantizedWeight>,
26 pub up_proj_t: Option<QuantizedWeight>,
27 pub down_proj_t: Option<QuantizedWeight>,
28}
29
30/// BF16 dense MLP weights — alternative to NVFP4 for precision-sensitive
31/// models (Gemma-4-31B). Each is `[N, K]` row-major BF16. When installed
32/// on a `DenseFfnLayer` via `set_bf16_weights`, the forward paths
33/// dispatch to `dense_gemv_bf16` / `dense_gemm_bf16` instead of the
34/// w4a16 NVFP4 kernels. Costs ~3.4 GB extra GPU memory on Gemma-4-31B
35/// (3 × hidden×intermediate × 2 bytes) vs NVFP4's 0.5 bytes/weight.
36pub struct DenseFfnWeightsBf16 {
37 pub gate_proj: DenseWeight,
38 pub up_proj: DenseWeight,
39 pub down_proj: DenseWeight,
40}
41
42/// Native block-scaled FP8 dense MLP weights — loaded directly from an FP8
43/// checkpoint (no NVFP4 requant). When installed via `set_fp8_weights`, decode
44/// dispatches `w8a16_gemv` and prefill `w8a16_gemm` per projection (BF16 act ×
45/// FP8 E4M3 weight with 2D block scales), mirroring the SSM/attention FP8 path.
46pub struct DenseFfnWeightsFp8 {
47 pub gate_proj: Fp8Weight,
48 pub up_proj: Fp8Weight,
49 pub down_proj: Fp8Weight,
50}
51
52/// Native keep-packed ternary Q2_0 dense MLP weights — loaded directly from a
53/// PrismML Q2_0 GGUF (`ATLAS_GGUF_NATIVE_Q2=1`) with NO dequant / NVFP4 requant.
54/// Each projection is a raw `block_q2_0` buffer (2-bit codes + inline fp16 scale
55/// per group). When installed via `set_q2_weights`, decode dispatches
56/// `q2_0_gemv` (BF16 act × 2-bit weight, dequant-in-dot-product), mirroring the
57/// FP8 path but with the weights ~4× smaller resident.
58pub struct DenseFfnWeightsQ2 {
59 pub gate_proj: PackedQ2Weight,
60 pub up_proj: PackedQ2Weight,
61 pub down_proj: PackedQ2Weight,
62}
63
64/// Activation function for gated FFN (SiLU for Qwen/Llama, GELU for Gemma-4).
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum FfnActivation {
67 SiLU,
68 GeLU,
69}
70
71/// A per-projection int8 W4A8 weight, built lazily from the NVFP4 weight on the
72/// first `ATLAS_INT8_PREFILL` prefill (see `DenseFfnLayer::ensure_int8_weight`).
73/// `w_i8` is `[N, K]` signed int8; `w_scale` is `[N, K/32]` F32. Cached for the
74/// process lifetime in a `OnceLock`, so the requant kernel runs once per weight.
75#[derive(Debug, Clone, Copy)]
76struct Int8Weight {
77 w_i8: DevicePtr,
78 w_scale: DevicePtr,
79}
80
81/// Q4_K-quantized FFN weight (GGML block_q4_K layout), materialized once at first
82/// `ATLAS_FFN_MMQ` prefill and cached for process lifetime in a `OnceLock`.
83#[derive(Debug, Clone, Copy)]
84struct Q4kWeight {
85 w_q4k: DevicePtr,
86}
87
88/// block_nvfp4-repacked FFN weight for the `ATLAS_FFN_NVFP4_MMQ` W4A4 prefill arm.
89/// Raw bit shuffle of the checkpoint's NVFP4 (same e2m1 codes + e4m3 scale bytes,
90/// same total bytes) — materialized once and cached for process lifetime.
91#[derive(Debug, Clone, Copy)]
92struct Fp4MmqWeight {
93 w: DevicePtr,
94}
95
96pub struct DenseFfnLayer {
97 pub weights: DenseFfnWeights,
98 activation: FfnActivation,
99 w4a16_gemv: KernelHandle,
100 /// Single-warp `w4a16_gemv_sw`. `KernelHandle(0)` on miss → base GEMV.
101 w4a16_gemv_sw: KernelHandle,
102 w4a16_gemv_dual: KernelHandle,
103 w4a16_gemv_silu_input: KernelHandle,
104 // LOSSLESS single-warp-per-output decode variants (8 outputs/block, no smem
105 // cross-warp reduce). Bit-identical to the 64-thread kernels (proven by the
106 // w4a16_gemv_sw microtest). Default ON via `ModelLevers::gemv_sw`;
107 // `ATLAS_NO_GEMV_SW=1` restores the 64-thread kernels. KernelHandle(0) on
108 // miss → fall back to base kernels.
109 w4a16_gemv_dual_sw: KernelHandle,
110 w4a16_gemv_silu_input_sw: KernelHandle,
111 w4a16_gemv_dual_batch2: KernelHandle,
112 w4a16_gemv_dual_batch3: KernelHandle,
113 w4a16_gemv_batch2: KernelHandle,
114 w4a16_gemv_batch3: KernelHandle,
115 /// Narrow `w4a16_gemv_batch{M}` family (M=4..8) for the K=4 verify FFN and
116 /// the K=5..8 chain verify. SSOT for the M -> tier decision; individual
117 /// tiers are 0-handles when the target did not load them.
118 w4a16_batchm: W4a16BatchmTiers,
119 w4a16_gemm: KernelHandle,
120 // 128x128 2-stage cp.async pipelined w4a16 GEMM — the fast prefill kernel
121 // attention/SSM already use. The base `w4a16_gemm` (M64xN64) only hits
122 // ~10 TFLOPS at M=8k and was the flat ~155 tok/s dense-FFN prefill
123 // bottleneck on Qwen3.6-27B. KernelHandle(0) on miss → scalar-tile fallback.
124 w4a16_gemm_t_m128_k: KernelHandle,
125 // v2: 8-warp (256-thread) variant of t_m128 — parallel chunk MMAs, 3 CTAs/SM.
126 // Preferred over t_m128 for dense-FFN prefill when present. KernelHandle(0) → use t_m128.
127 w4a16_gemm_t_m128_v2_k: KernelHandle,
128 // LOSSLESS BF16 variant of t_m128: same 128x128 cp.async tiling, but FP4→BF16
129 // dequant + BF16 m16n8k16 MMA (FP32 accum) instead of the FP8-E4M3 crush the
130 // default NVIDIA t_m128 uses. The FP8 path perturbs generation (measured
131 // length-truncations / accuracy risk on Qwen3.6-27B); this kernel keeps prefill
132 // outputs bit-for-bit vs the base `w4a16_gemm`. OPT-IN only, gated by
133 // ATLAS_BF16_TC_PREFILL (default off → dispatch unchanged). KernelHandle(0) on miss.
134 w4a16_gemm_t_m128_bf16_k: KernelHandle,
135 // v2 of the LOSSLESS BF16 128x128 prefill kernel: same MMA instruction order
136 // (so BIT-IDENTICAL to bf16_k, proven by w4a16_bf16_v2_microtest) but a
137 // smaller A-tile smem pad lifts occupancy from 2→3 CTAs/SM (~+50% resident
138 // warps), giving a measured ~3-8% faster prefill GEMM on this latency-bound
139 // kernel. Preferred over bf16_k when present. KernelHandle(0) on miss → bf16_k.
140 w4a16_gemm_t_m128_bf16_v2_k: KernelHandle,
141 // FP8 M64 prefill (w4a16_gemm_t): m16n8k32 e4m3 MMA + M_TILE=64. Packed 1-byte
142 // operands cut shared-memory load instructions ~4x (the v2 BF16 path is
143 // smem-bandwidth-bound, L1/TEX 90% per ncu), and M64's lower register pressure
144 // lifts occupancy → measured ~44 TFLOP/s vs ~30 for v2 (~1.47x prefill) on dgx1.
145 // LOSSY (FP8 E4M3, cosine ~0.9997) — OPT-IN via ATLAS_FP8_M64_PREFILL, gated on
146 // quality. KernelHandle(0) on miss → dispatch unchanged.
147 w4a16_gemm_t_k: KernelHandle,
148 // int8 W4A8 prefill (ATLAS_INT8_PREFILL): the validated requant→faith2
149 // pipeline (cosine 0.999978). `int8_gemm_faith2` is an int8×int8 MMA with
150 // per-32 block scales, so BOTH operands must be int8 — unlike the FP8 path
151 // (mixed BF16×FP8). At first int8 prefill we requant the NVFP4 gate/up/down
152 // weights to int8 once (`requant_w_nvfp4_int8`, cached in the OnceLocks
153 // below) and requant the BF16 activations every call (`requant_a_bf16_int8`,
154 // into `int8_a_scratch`). KernelHandle(0) on miss → arm never taken.
155 int8_faith2_k: KernelHandle,
156 // faith5: int32 per-sb accumulation (breaks the MMA→scale dependency chain).
157 // Opt-in via ATLAS_INT8_FAITH5=1 (replaces faith2 for int8 prefill GEMMs).
158 int8_faith5_k: KernelHandle,
159 requant_w_int8_k: KernelHandle,
160 requant_a_int8_k: KernelHandle,
161 // Lazily-built, process-lifetime int8 weight copies (one per projection),
162 // requanted from `self.weights.{gate,up,down}_proj`. Only ever touched when
163 // ATLAS_INT8_PREFILL is set → default-off path is byte-identical.
164 int8_gate: std::sync::OnceLock<Int8Weight>,
165 int8_up: std::sync::OnceLock<Int8Weight>,
166 int8_down: std::sync::OnceLock<Int8Weight>,
167 // Activation-requant scratch for the int8/NVFP4/Q4_K prefill GEMMs is now
168 // shared, arena-owned (BufferArena::ffn_act_{q8,a,scale}), sized once for
169 // max_batch_tokens × max(h, inter) — no per-layer allocation.
170 // W4A4 native-FP4 prefill (ATLAS_FP4_PREFILL): NVFP4 weights consumed directly
171 // (no requant), BF16 activations quantized to NVFP4 each call into ffn_act_a/scale.
172 // KernelHandle(0) on miss → arm never taken (default-off byte-identical).
173 w4a4_gemm_k: KernelHandle,
174 quantize_nvfp4_k: KernelHandle,
175 // Q4_K MMQ prefill (ATLAS_FFN_MMQ): vendored llama Q4_K W4A8 GEMM. Weights
176 // materialized NVFP4→bf16→Q4_K once (lazy, cached in the OnceLocks); activations
177 // quantized to q8_1_mmq each call into ffn_act_q8. KernelHandle(0) → arm skipped.
178 q4k_mmq_nc_k: KernelHandle,
179 q4k_mmq_wc_k: KernelHandle,
180 q4k_quant_act_k: KernelHandle,
181 q4k_quant_w_k: KernelHandle,
182 dequant_nvfp4_bf16_k: KernelHandle,
183 q4k_gate: std::sync::OnceLock<Q4kWeight>,
184 q4k_up: std::sync::OnceLock<Q4kWeight>,
185 q4k_down: std::sync::OnceLock<Q4kWeight>,
186 // NVFP4 W4A4 MMQ prefill (ATLAS_FFN_NVFP4_MMQ): vendored llama Blackwell block-scale
187 // FP4 MMA (80 TFLOP/s vs t_m128 ~51 on GB10). Gate/up weights repacked ONCE at load
188 // (raw bit shuffle, checkpoint layout → block_nvfp4, zero requantization); activations
189 // quantized per call into the shared ffn_act_q8 scratch; the per-tensor scale2 is
190 // folded in the scaled SiLU-mul. KernelHandle(0) → arm skipped.
191 nvfp4_mmq_nc_k: KernelHandle,
192 nvfp4_mmq_wc_k: KernelHandle,
193 /// M-sized MMQ tiles for DECODE. The 128 tile issues MMAs for all 128 columns
194 /// regardless of m, so at m=16 it discards 112 of them; these size the tile to
195 /// the batch. try_kernel: 0-handle -> dispatch keeps the 128 tile.
196 nvfp4_mmq16_nc_k: KernelHandle,
197 nvfp4_mmq16_wc_k: KernelHandle,
198 nvfp4_mmq32_nc_k: KernelHandle,
199 nvfp4_mmq32_wc_k: KernelHandle,
200 nvfp4_mmq64_nc_k: KernelHandle,
201 nvfp4_mmq64_wc_k: KernelHandle,
202 nvfp4_quant_act_k: KernelHandle,
203 nvfp4_repack_k: KernelHandle,
204 nvfp4_silu_scaled_k: KernelHandle,
205 nvfp4_silu_quant_k: KernelHandle,
206 nvfp4_scale_k: KernelHandle,
207 fp4mmq_gate: std::sync::OnceLock<Fp4MmqWeight>,
208 fp4mmq_up: std::sync::OnceLock<Fp4MmqWeight>,
209 fp4mmq_down: std::sync::OnceLock<Fp4MmqWeight>,
210 // Small-M (DFlash verify M=17) routing companion to `w4a16_gemm_t_k`
211 // (declared above): deep-K variant. w4a16_m17_bench: `w4a16_gemm_t_k64`
212 // wins deep-K down_proj (554 vs 810us at K=17408); the M64-tile
213 // `w4a16_gemm_t` beats M128 tiles at M<=64 (283 vs 324us on gate/up).
214 // KernelHandle(0) → m128 dispatch.
215 w4a16_gemm_t_k64_k: KernelHandle,
216 /// SiLU(gate)*up or GELU(gate)*up depending on activation.
217 act_mul: KernelHandle,
218 /// BF16 dense MLP weights — when `Some`, all forward paths use the
219 /// `dense_gemv_bf16` / `dense_gemm_bf16` kernels instead of w4a16
220 /// NVFP4. Falls back to the NVFP4 weights when `None`. Set via
221 /// `set_bf16_weights`. Used by Gemma-4 dense to avoid the structural
222 /// NVFP4 attention drift on greedy code generation (the fib test's
223 /// broken-indentation pattern).
224 bf16_weights: Option<DenseFfnWeightsBf16>,
225 dense_gemv_bf16_k: KernelHandle,
226 dense_gemm_bf16_k: KernelHandle,
227 // Tensor-core BF16 GEMM (m16n8k16 MMA) for the dense-FFN PREFILL path.
228 // The scalar `dense_gemm_bf16` is ~10x too slow on long prefills (it was
229 // the flat ~155 tok/s prefill bottleneck on Qwen3.6-27B dense NVFP4).
230 // KernelHandle(0) on miss → forward_prefill falls back to the scalar path.
231 // Decode (gemv, M=1) is untouched, so TPOT is unaffected.
232 dense_gemm_tc_k: KernelHandle,
233 /// Native FP8 dense MLP weights — when `Some`, decode/prefill dispatch the
234 /// block-scaled FP8 kernels (`w8a16_gemv` / `w8a16_gemm`) instead of w4a16
235 /// NVFP4. Set via `set_fp8_weights` for native FP8 checkpoints (Qwythos /
236 /// Ornith-FP8). Spec-decode batched paths fall back to dequant — dense
237 /// qwen3_5 has no MTP, so they're never reached.
238 fp8_weights: Option<DenseFfnWeightsFp8>,
239 w8a16_gemv_k: KernelHandle,
240 w8a16_gemm_k: KernelHandle,
241 w8a16_gemv_batch4_k: KernelHandle,
242 w8a16_gemm_pipelined_k: KernelHandle,
243 // Fused FP8 decode GEMVs (gate+up in one launch / silu+down in one launch),
244 // mirroring the NVFP4 w4a16_gemv_dual / w4a16_gemv_silu_input. KernelHandle(0)
245 // on miss → fall back to the 3-launch w8a16_gemv path. Module = .cu file stem.
246 w8a16_gemv_dual_k: KernelHandle,
247 w8a16_gemv_silu_input_k: KernelHandle,
248 // Fast transposed FP8 prefill GEMM (128x128 / 8-warp / two-level FP32 fold).
249 // Preferred over w8a16_gemm when a transposed FP8 weight copy is present.
250 // KernelHandle(0) → fall back to non-transposed w8a16_gemm.
251 w8a16_gemm_t_m128_k: KernelHandle,
252 /// v0 LoRA overlay for gate/up/down. `set_lora_weights` REJECTS layers
253 /// where `fp8_weights`, `bf16_weights` or `q2_weights` are installed (v0
254 /// supports the NVFP4 dispatch path only — those branches early-return
255 /// before the NVFP4 tail where the deltas land; holo is NVFP4 so it is
256 /// unaffected).
257 ///
258 /// M1 (2026-08-19): the deltas are APPLIED. `apply_lora_gate_up` runs
259 /// after the gate/up projection and before `silu_mul`; `apply_lora_down`
260 /// runs after the down projection. Every NVFP4 dispatch this layer can
261 /// take — decode `forward`, `forward_k2`/`k3`/`km`, `forward_prefill`,
262 /// `forward_batched` — calls both, because an adapter that applies on one
263 /// path and not another produces a model that contradicts itself between
264 /// prefill and decode.
265 ///
266 /// Until M1 this field was written by `set_lora_weights` and never read,
267 /// so an adapter targeting gate/up/down loaded successfully and changed
268 /// nothing. On a hybrid like Qwen3.8-27B that is most of the adapter:
269 /// community LoRAs for it put 67-78% of their parameter mass in the FFN.
270 lora: Option<ops::lora_delta::LoraFfnWeights>,
271
272 /// Native keep-packed ternary Q2_0 dense MLP weights (`ATLAS_GGUF_NATIVE_Q2`).
273 /// When installed via `set_q2_weights`, decode dispatches `q2_0_gemv_vec`
274 /// (BF16 activation × packed 2-bit weight, dequant-in-dot-product) — the
275 /// weights stay 2-bit resident (no NVFP4 requant). Highest-priority forward
276 /// branch. Prefill/batched paths for packed-Q2 are a deferred (Tier-2) phase
277 /// and currently bail — dense qwen35 has no MTP so k2/k3 are never reached.
278 q2_weights: Option<DenseFfnWeightsQ2>,
279 q2_0_gemv_k: KernelHandle,
280 // Batched (M=1..8) packed-Q2 decode GEMV handle. The kernel + wrapper are
281 // built and validated (CPU math test), but the batched-decode call site
282 // (spec-decode verify rows) is deferred to the same Tier-2 phase as prefill;
283 // dense qwen35 has no MTP so no batched decode reaches the FFN today.
284 #[allow(dead_code)]
285 q2_0_gemv_batchm_k: KernelHandle,
286 // Load-time packed-Q2 → BF16 dequant kernel (`dequant_gguf_bf16` module).
287 // Used by packed-Q2 PREFILL: dequant each proj into a TRANSIENT BF16 scratch
288 // buffer, run the normal BF16 GEMM, free the scratch — the resident weight
289 // stays 2-bit. Decode uses the native `q2_0_gemv` (no dequant). Tier-1 path.
290 dequant_q2_0_gn_k: KernelHandle,
291 // Native Q2_0 MMQ prefill (Tier-2, `ATLAS_GGUF_NATIVE_Q2_MMQ=1`): keeps the
292 // 2-bit weight packed and runs a tensor-core int8 MMA (dequant-in-register)
293 // against a q8_1 activation — no BF16 weight scratch, no dequant tax, no race.
294 // The q8_1 activation quantizer is SHARED with Q4_K (`q4k_quant_act_k`).
295 // KernelHandle(0) when absent → falls back to the transient-dequant path.
296 q2_0_mmq_nc_k: KernelHandle,
297 q2_0_mmq_wc_k: KernelHandle,
298}
299
300/// M-sized MMQ tiles: **ON by default**, disabled by `ATLAS_NO_MMQ_SMALL_TILE=1`.
301/// Strict `== "1"` on an `ATLAS_NO_*` name — presence flags here are enabled by `=0`.
302fn mmq_small_tile_enabled() -> bool {
303 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
304 *ON.get_or_init(|| std::env::var("ATLAS_NO_MMQ_SMALL_TILE").as_deref() != Ok("1"))
305}
306
307/// The m=64 MMQ tile: **ON by default**, disabled by `ATLAS_NO_MMQ_TILE64=1`. Separate
308/// from `ATLAS_NO_MMQ_SMALL_TILE` so this arm can be A/B'd without also reverting the
309/// already-shipped 16/32 tiles. Strict `== "1"`, matching the sibling above.
310fn mmq_tile64_enabled() -> bool {
311 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
312 *ON.get_or_init(|| std::env::var("ATLAS_NO_MMQ_TILE64").as_deref() != Ok("1"))
313}
314impl DenseFfnLayer {
315 pub fn new(weights: DenseFfnWeights, gpu: &dyn GpuBackend) -> Result<Self> {
316 Self::new_with_activation(weights, FfnActivation::SiLU, gpu)
317 }
318
319 pub fn new_with_activation(
320 weights: DenseFfnWeights,
321 activation: FfnActivation,
322 gpu: &dyn GpuBackend,
323 ) -> Result<Self> {
324 let act_mul = match activation {
325 FfnActivation::SiLU => gpu.kernel("moe_silu_mul", "moe_silu_mul")?,
326 FfnActivation::GeLU => gpu.kernel("gelu", "gelu_mul")?,
327 };
328 // BF16 path kernels — optional (only loaded if available; gemma4
329 // is the only consumer today). `try_kernel` returns
330 // `KernelHandle(0)` on miss so we don't break NVFP4-only models
331 // that were built without these kernels. Module names per
332 // `kernels/gb10/{target}/nvfp4/KERNEL.toml`:
333 // `dense_gemv_bf16 = "gemv"`, `dense_gemm_bf16 = "gemm"`.
334 let dense_gemv_bf16_k = super::try_kernel(gpu, "gemv", "dense_gemv_bf16");
335 let dense_gemm_bf16_k = super::try_kernel(gpu, "gemm", "dense_gemm_bf16");
336 let dense_gemm_tc_k = super::try_kernel(gpu, "gemm_tc", "dense_gemm_tc");
337
338 let layer = Self {
339 weights,
340 activation,
341 w4a16_gemv: gpu.kernel("w4a16_gemv", "w4a16_gemv")?,
342 w4a16_gemv_sw: super::try_kernel(gpu, "w4a16_gemv", "w4a16_gemv_sw"),
343 w4a16_gemv_dual: gpu.kernel("w4a16_gemv_fused", "w4a16_gemv_dual")?,
344 w4a16_gemv_silu_input: gpu.kernel("w4a16_gemv_fused", "w4a16_gemv_silu_input")?,
345 w4a16_gemv_dual_sw: super::try_kernel(gpu, "w4a16_gemv_fused", "w4a16_gemv_dual_sw"),
346 w4a16_gemv_silu_input_sw: super::try_kernel(
347 gpu,
348 "w4a16_gemv_fused",
349 "w4a16_gemv_silu_input_sw",
350 ),
351 w4a16_gemv_dual_batch2: gpu.kernel("w4a16_gemv", "w4a16_gemv_dual_batch2")?,
352 w4a16_gemv_dual_batch3: gpu.kernel("w4a16_gemv", "w4a16_gemv_dual_batch3")?,
353 w4a16_gemv_batch2: gpu.kernel("w4a16_gemv", "w4a16_gemv_batch2")?,
354 w4a16_gemv_batch3: gpu.kernel("w4a16_gemv", "w4a16_gemv_batch3")?,
355 w4a16_batchm: W4a16BatchmTiers::resolve(gpu),
356 w4a16_gemm: gpu.kernel("w4a16", "w4a16_gemm")?,
357 w4a16_gemm_t_m128_k: super::try_kernel(gpu, "w4a16", "w4a16_gemm_t_m128"),
358 w4a16_gemm_t_m128_v2_k: super::w4a16_v2_kernel(gpu),
359 w4a16_gemm_t_m128_bf16_k: super::try_kernel(gpu, "w4a16", "w4a16_gemm_t_m128_bf16"),
360 w4a16_gemm_t_m128_bf16_v2_k: super::try_kernel(
361 gpu,
362 "w4a16",
363 "w4a16_gemm_t_m128_bf16_v2",
364 ),
365 w4a16_gemm_t_k: super::tgemm_kernel(gpu),
366 int8_faith2_k: super::try_kernel(gpu, "w4a16", "int8_gemm_faith2"),
367 int8_faith5_k: super::try_kernel(gpu, "w4a16", "int8_gemm_i32acc"),
368 requant_w_int8_k: super::try_kernel(gpu, "w4a16", "requant_w_nvfp4_int8"),
369 requant_a_int8_k: super::try_kernel(gpu, "w4a16", "requant_a_bf16_int8"),
370 int8_gate: std::sync::OnceLock::new(),
371 int8_up: std::sync::OnceLock::new(),
372 int8_down: std::sync::OnceLock::new(),
373 w4a4_gemm_k: super::try_kernel(gpu, "w4a4", "w4a4_gemm"),
374 quantize_nvfp4_k: super::try_kernel(gpu, "quantize_nvfp4", "quantize_bf16_to_nvfp4"),
375 q4k_mmq_nc_k: super::try_kernel(gpu, "q4k_mmq", "atlas_q4k_mmq128_nc"),
376 q4k_mmq_wc_k: super::try_kernel(gpu, "q4k_mmq", "atlas_q4k_mmq128_wc"),
377 q4k_quant_act_k: super::try_kernel(gpu, "q4k_mmq", "atlas_q8_1_quantize_ds4_bf16"),
378 q4k_quant_w_k: super::try_kernel(gpu, "q4k_quantize", "q4k_quantize"),
379 dequant_nvfp4_bf16_k: super::try_kernel(
380 gpu,
381 "dequant_nvfp4_bf16",
382 "dequant_nvfp4_to_bf16",
383 ),
384 q4k_gate: std::sync::OnceLock::new(),
385 q4k_up: std::sync::OnceLock::new(),
386 q4k_down: std::sync::OnceLock::new(),
387 nvfp4_mmq_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq128_nc"),
388 nvfp4_mmq_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq128_wc"),
389 nvfp4_mmq16_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq16_nc"),
390 nvfp4_mmq16_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq16_wc"),
391 nvfp4_mmq32_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq32_nc"),
392 nvfp4_mmq32_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq32_wc"),
393 nvfp4_mmq64_nc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq64_nc"),
394 nvfp4_mmq64_wc_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_mmq64_wc"),
395 nvfp4_quant_act_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_quantize_bf16"),
396 nvfp4_repack_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_repack"),
397 nvfp4_silu_scaled_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_silu_mul_scaled"),
398 nvfp4_silu_quant_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_silu_mul_quant"),
399 nvfp4_scale_k: super::try_kernel(gpu, "nvfp4_mmq", "atlas_nvfp4_scale_bf16"),
400 fp4mmq_gate: std::sync::OnceLock::new(),
401 fp4mmq_up: std::sync::OnceLock::new(),
402 fp4mmq_down: std::sync::OnceLock::new(),
403 w4a16_gemm_t_k64_k: super::k64_kernel(gpu).unwrap_or(KernelHandle(0)),
404 act_mul,
405 bf16_weights: None,
406 dense_gemv_bf16_k,
407 dense_gemm_bf16_k,
408 dense_gemm_tc_k,
409 fp8_weights: None,
410 w8a16_gemv_k: super::try_kernel(gpu, "w8a16_gemv", "w8a16_gemv"),
411 w8a16_gemm_k: super::try_kernel(gpu, "w8a16_gemm", "w8a16_gemm"),
412 w8a16_gemv_batch4_k: super::try_kernel(gpu, "w8a16_gemv_batch4", "w8a16_gemv_batch4"),
413 w8a16_gemm_pipelined_k: super::try_kernel(
414 gpu,
415 "w8a16_gemm_pipelined",
416 "w8a16_gemm_pipelined",
417 ),
418 w8a16_gemv_dual_k: super::try_kernel(gpu, "w8a16_gemv_fused", "w8a16_gemv_dual"),
419 w8a16_gemv_silu_input_k: super::try_kernel(
420 gpu,
421 "w8a16_gemv_fused",
422 "w8a16_gemv_silu_input",
423 ),
424 w8a16_gemm_t_m128_k: super::try_kernel(gpu, "w8a16_gemm_t_m128", "w8a16_gemm_t_m128"),
425 lora: None,
426 q2_weights: None,
427 // Winner of the decode-GEMV bench: candidate B (vectorized code loads
428 // + smem A-stage, 1 warp/row × 8 rows/CTA). ~268 GB/s (98% of the
429 // 273 GB/s LPDDR5X peak) at gate/up M=1 — 9.5× the original
430 // whole-block-strided `q2_0_gemv`. Same `(code-1)*d` FP32 numerics.
431 q2_0_gemv_k: super::try_kernel(gpu, "q2_0_gemv_vec", "q2_0_gemv_vec"),
432 q2_0_gemv_batchm_k: super::try_kernel(gpu, "q2_0_gemv_vec", "q2_0_gemv_vec_batchm"),
433 dequant_q2_0_gn_k: super::try_kernel(
434 gpu,
435 "dequant_gguf_bf16",
436 "dequant_q2_0_gn_to_bf16",
437 ),
438 // Resolved by `set_q2_weights`, never here: q2_0_mmq ships only
439 // in GGUF-serving targets, and an unconditional probe fails the
440 // boot audit on every dense-FFN model that never installs
441 // packed-Q2 weights.
442 q2_0_mmq_nc_k: KernelHandle(0),
443 q2_0_mmq_wc_k: KernelHandle(0),
444 };
445 Ok(layer)
446 }
447
448 /// Load-time finalize for the Q4_K MMQ prefill path (`ATLAS_FFN_MMQ`). MUST run at
449 /// load, BEFORE the KV cache is sized, so the net FFN footprint is correct when the KV
450 /// cache claims free memory. Order is critical: (1) eagerly materialize the Q4_K weights
451 /// (+9.63 GB) so they are accounted for now rather than lazily on first prefill (which
452 /// would over-subscribe AFTER the KV cache already grabbed the freed `_t` space → decode
453 /// OOM-throttle); (2) free the transposed `_proj_t` copies (−9.63 GB, dead under Q4_K
454 /// prefill — only the unreachable `Some(wt)` arms read them). Net FFN = baseline; decode
455 /// untouched (NVFP4 gemv on the non-`_t` copies). No-op unless Q4_K is active.
456 pub fn finalize_q4k_load(
457 &mut self,
458 gpu: &dyn GpuBackend,
459 h: u32,
460 inter: u32,
461 stream: u64,
462 ) -> Result<()> {
463 // Packed-Q2 (ATLAS_GGUF_NATIVE_Q2) FFN keeps its NVFP4 source weights
464 // NULL — the Q4_K prefill copy is built by dequant-ing those (NULL) NVFP4
465 // blocks, so running it here is a null-ptr kernel launch (CUDA 700).
466 // Packed-Q2 has its own prefill path (transient dequant), so skip.
467 if self.q2_weights.is_some() {
468 return Ok(());
469 }
470 let q4k_active = self.q4k_mmq_nc_k.0 != 0
471 && self.q4k_quant_act_k.0 != 0
472 && self.q4k_quant_w_k.0 != 0
473 && self.dequant_nvfp4_bf16_k.0 != 0
474 && std::env::var_os("ATLAS_FFN_MMQ").is_some();
475 if !q4k_active {
476 return Ok(());
477 }
478 // (1) eagerly materialize the prefill weights BEFORE freeing `_t`, so the KV cache
479 // (sized after load) can't claim the freed space before the weights exist.
480 // gate/up: Q4_K (N=inter,K=h). down: HYBRID → int8 faith2 (N=h,K=inter) for accuracy,
481 // else Q4_K. ensure_int8_weight reads the non-`_t` NVFP4 down_proj (kept for decode gemv).
482 self.ensure_q4k_weight(
483 &self.q4k_gate,
484 gpu,
485 &self.weights.gate_proj,
486 inter,
487 h,
488 stream,
489 )?;
490 self.ensure_q4k_weight(&self.q4k_up, gpu, &self.weights.up_proj, inter, h, stream)?;
491 let down_faith2 = self.int8_faith2_k.0 != 0
492 && self.requant_a_int8_k.0 != 0
493 && std::env::var_os("ATLAS_FFN_MMQ_DOWN_Q4K").is_none();
494 if down_faith2 {
495 self.ensure_int8_weight(
496 &self.int8_down,
497 gpu,
498 &self.weights.down_proj,
499 h,
500 inter,
501 stream,
502 )?;
503 } else {
504 self.ensure_q4k_weight(
505 &self.q4k_down,
506 gpu,
507 &self.weights.down_proj,
508 h,
509 inter,
510 stream,
511 )?;
512 }
513 gpu.synchronize(stream)?;
514 // (2) free the dead transposed copies
515 let mut freed = 0usize;
516 for wt in [
517 &mut self.weights.gate_proj_t,
518 &mut self.weights.up_proj_t,
519 &mut self.weights.down_proj_t,
520 ] {
521 if let Some(w) = wt.as_ref()
522 && !w.weight.is_null()
523 {
524 gpu.free(w.weight)?;
525 gpu.free(w.weight_scale)?;
526 freed += 1;
527 }
528 *wt = None;
529 }
530 if freed > 0 {
531 // Log-once latch (see `atlas_core::scope`). It holds no model-derived
532 // value — the message is rebuilt from the arguments every call — so a
533 // stale entry cannot produce a wrong answer, only a suppressed duplicate
534 // line after a model swap. Scoping it would thread a logging concern
535 // through the call path to prevent one repeated INFO line.
536 // Latched on the BACKEND (`OpCache::once`), which exists by load
537 // time: `finalize_q4k_load` takes the `gpu` it is loading onto. A
538 // static meant only the first model in the process reported the
539 // decision.
540 if gpu.op_cache().once("log:ffn_mmq_freed_twins") {
541 tracing::info!(
542 "[atlas] ATLAS_FFN_MMQ: freed transposed FFN `_t` copies (dead under Q4_K prefill) — Q4_K weights net to ~0 vs NVFP4 baseline"
543 );
544 }
545 }
546 Ok(())
547 }
548
549 /// Eagerly materialize the block_nvfp4 gate/up copies for the `ATLAS_FFN_NVFP4_MMQ`
550 /// W4A4 prefill arm at LOAD time (before KV sizing), then free the now-dead gate/up
551 /// transposed `_t` copies so net FFN footprint stays at the NVFP4 baseline. Down is
552 /// untouched (hybrid: it stays on the default t_m128 path for accuracy → keeps its
553 /// `_t` copy). No-op unless the env + kernels are present.
554 pub fn finalize_nvfp4_mmq_load(
555 &mut self,
556 gpu: &dyn GpuBackend,
557 h: u32,
558 inter: u32,
559 stream: u64,
560 ) -> Result<()> {
561 // Packed-Q2 (ATLAS_GGUF_NATIVE_Q2) FFN keeps its NVFP4 source weights
562 // NULL. This W4A4-MMQ finalize is active by DEFAULT (SiLU + kernels
563 // present) and repacks the NVFP4 gate/up — over NULL pointers that's a
564 // CUDA-700 illegal access. Packed-Q2 uses its own decode/prefill path.
565 if self.q2_weights.is_some() {
566 return Ok(());
567 }
568 let active = self.nvfp4_mmq_nc_k.0 != 0
569 && self.nvfp4_quant_act_k.0 != 0
570 && self.nvfp4_repack_k.0 != 0
571 && self.nvfp4_silu_scaled_k.0 != 0
572 && matches!(self.activation, FfnActivation::SiLU)
573 && std::env::var_os("ATLAS_NO_FFN_NVFP4_MMQ").is_none();
574 if !active {
575 return Ok(());
576 }
577 self.ensure_nvfp4_mmq_weight(
578 &self.fp4mmq_gate,
579 gpu,
580 &self.weights.gate_proj,
581 inter,
582 h,
583 stream,
584 )?;
585 self.ensure_nvfp4_mmq_weight(
586 &self.fp4mmq_up,
587 gpu,
588 &self.weights.up_proj,
589 inter,
590 h,
591 stream,
592 )?;
593 let down_mmq = std::env::var_os("ATLAS_NO_FFN_NVFP4_MMQ_DOWN").is_none();
594 if down_mmq {
595 self.ensure_nvfp4_mmq_weight(
596 &self.fp4mmq_down,
597 gpu,
598 &self.weights.down_proj,
599 h,
600 inter,
601 stream,
602 )?;
603 }
604 gpu.synchronize(stream)?;
605 // Free the dead transposed copies (prefill for those projections now runs on the
606 // MMQ arm; decode reads the non-transposed originals). down_proj_t is freed only
607 // when the down A/B gate is on.
608 let mut down_t = if down_mmq {
609 Some(&mut self.weights.down_proj_t)
610 } else {
611 None
612 };
613 let mut freed = 0usize;
614 for wt in [&mut self.weights.gate_proj_t, &mut self.weights.up_proj_t]
615 .into_iter()
616 .chain(down_t.take())
617 {
618 if let Some(w) = wt.as_ref()
619 && !w.weight.is_null()
620 {
621 gpu.free(w.weight)?;
622 gpu.free(w.weight_scale)?;
623 freed += 1;
624 }
625 *wt = None;
626 }
627 if freed > 0 {
628 // Log-once latch (see `atlas_core::scope`). It holds no model-derived
629 // value — the message is rebuilt from the arguments every call — so a
630 // stale entry cannot produce a wrong answer, only a suppressed duplicate
631 // line after a model swap. Scoping it would thread a logging concern
632 // through the call path to prevent one repeated INFO line.
633 // Latched on the BACKEND (`OpCache::once`), which exists by load
634 // time: `finalize_q4k_load` takes the `gpu` it is loading onto. A
635 // static meant only the first model in the process reported the
636 // decision.
637 if gpu.op_cache().once("log:ffn_fp4mmq_freed_twins") {
638 tracing::info!(
639 "[atlas] ATLAS_FFN_NVFP4_MMQ: freed gate/up `_t` copies (dead under FP4-MMQ prefill) — block_nvfp4 copies net to ~0 vs NVFP4 baseline"
640 );
641 }
642 }
643 Ok(())
644 }
645
646 /// Ensure the block_nvfp4 copy of one NVFP4 projection exists (raw repack of the
647 /// checkpoint's packed E2M1 `[N, K/2]` + E4M3 `[N, K/16]` scales — zero numerics;
648 /// scale2 folded at the SiLU-mul). Cached in `cell` for process lifetime.
649 fn ensure_nvfp4_mmq_weight(
650 &self,
651 cell: &std::sync::OnceLock<Fp4MmqWeight>,
652 gpu: &dyn GpuBackend,
653 src: &QuantizedWeight,
654 n: u32,
655 k: u32,
656 stream: u64,
657 ) -> Result<Fp4MmqWeight> {
658 if let Some(w) = cell.get() {
659 return Ok(*w);
660 }
661 let w = gpu.alloc(ops::nvfp4_mmq_weight_bytes(n, k))?;
662 ops::nvfp4_mmq_repack(
663 gpu,
664 self.nvfp4_repack_k,
665 src.weight,
666 src.weight_scale,
667 w,
668 n,
669 k,
670 stream,
671 )?;
672 let built = Fp4MmqWeight { w };
673 if let Err(dup) = cell.set(built) {
674 gpu.synchronize(stream)?;
675 let _ = gpu.free(dup.w);
676 }
677 Ok(*cell.get().expect("fp4mmq weight cell set above"))
678 }
679
680 /// Install native block-scaled FP8 dense MLP weights. After this call the
681 /// forward paths dispatch `w8a16_gemv` (decode) / `w8a16_gemm` (prefill)
682 /// instead of w4a16 NVFP4. Caller must ensure those kernels are present in
683 /// the target (they are for the qwen3_5/ornith nvfp4 bundle).
684 pub fn set_fp8_weights(&mut self, gate: Fp8Weight, up: Fp8Weight, down: Fp8Weight) {
685 self.fp8_weights = Some(DenseFfnWeightsFp8 {
686 gate_proj: gate,
687 up_proj: up,
688 down_proj: down,
689 });
690 }
691
692 /// Install the startup-static LoRA FFN overlay (gate/up/down deltas).
693 /// Hard-rejects when FP8/BF16 weight overlays are installed — those
694 /// decode branches early-return before the NVFP4 tail where the M1
695 /// delta insertions land, so a permissive install would silently skip
696 /// deltas. holo is NVFP4, so it is unaffected.
697 pub fn set_lora_weights(&mut self, w: ops::lora_delta::LoraFfnWeights) -> Result<()> {
698 anyhow::ensure!(
699 self.fp8_weights.is_none() && self.bf16_weights.is_none(),
700 "LoRA v0 supports only the NVFP4 dense-FFN path (FP8/BF16 weight \
701 overlays installed on this layer)"
702 );
703 // Packed-Q2 has its own gemv/batchm branches that early-return before
704 // the NVFP4 tail where the deltas land, exactly like FP8/BF16. Refusing
705 // here keeps the invariant the M1 apply relies on: if `self.lora` is
706 // Some, EVERY dispatch this layer can take applies it. A silently
707 // skipping path is worse than a refused load — it makes the adapter
708 // active in prefill and absent in decode, which reads as model
709 // weirdness rather than as a missing feature.
710 anyhow::ensure!(
711 self.q2_weights.is_none(),
712 "LoRA v0 supports only the NVFP4 dense-FFN path (packed-Q2 weights \
713 installed on this layer)"
714 );
715 // The decode down delta contracts over silu(gate)*up, which only the
716 // split-SiLU path materialises; `forward` pins that path whenever an
717 // adapter is installed. Refuse here if the layer cannot take it, so
718 // the pin is a guarantee rather than a hope.
719 anyhow::ensure!(
720 self.activation == FfnActivation::SiLU && self.act_mul.0 != 0 && self.w4a16_gemv.0 != 0,
721 "LoRA v0 needs the split-SiLU decode path (SiLU activation + \
722 act_mul + w4a16_gemv kernels); this layer resolved activation \
723 {:?}, act_mul={}, w4a16_gemv={}",
724 self.activation,
725 self.act_mul.0,
726 self.w4a16_gemv.0,
727 );
728 self.lora = Some(w);
729 Ok(())
730 }
731
732 /// M1 gate/up delta: `gate_out += ΔW_gate · x`, `up_out += ΔW_up · x`.
733 ///
734 /// Call AFTER the gate/up projection and BEFORE `silu_mul` — the deltas
735 /// belong to the projections, so they must land while gate/up are still
736 /// separate. Both buffers are the arena's dedicated `expert_gate_out` /
737 /// `expert_up_out` regions, contiguous with row stride `inter*2`, which is
738 /// what `apply_lora_delta`'s contiguity contract requires.
739 ///
740 /// No-op (and no launches) when the layer carries no adapter, so the
741 /// non-LoRA path stays byte-identical.
742 fn apply_lora_gate_up(
743 &self,
744 ctx: &ForwardContext,
745 input: DevicePtr,
746 gate_out: DevicePtr,
747 up_out: DevicePtr,
748 m: u32,
749 stream: u64,
750 ) -> Result<()> {
751 if ops::lora_delta::lora_no_ffn() {
752 return Ok(());
753 }
754 let Some(ref lw) = self.lora else {
755 return Ok(());
756 };
757 for (pair, base) in [(&lw.gate, gate_out), (&lw.up, up_out)] {
758 if let Some(pair) = pair.as_ref() {
759 ops::lora_delta::apply_lora_delta(
760 ctx.gpu,
761 &lw.kernels,
762 pair,
763 input,
764 base,
765 m,
766 ctx.buffers.lora_xa(),
767 ctx.buffers.lora_delta(),
768 stream,
769 )?;
770 }
771 }
772 Ok(())
773 }
774
775 /// M1 down delta: `output += ΔW_down · act`.
776 ///
777 /// Call AFTER the down projection. `act` is the SiLU(gate)*up activation —
778 /// the same tensor the base down projection contracted over, NOT the
779 /// layer input. Every dense path leaves it in the `expert_gate_out`
780 /// region (silu_mul writes in place over gate), contiguous at row stride
781 /// `inter*2`.
782 fn apply_lora_down(
783 &self,
784 ctx: &ForwardContext,
785 act: DevicePtr,
786 output: DevicePtr,
787 m: u32,
788 stream: u64,
789 ) -> Result<()> {
790 if ops::lora_delta::lora_no_ffn() {
791 return Ok(());
792 }
793 let Some(ref lw) = self.lora else {
794 return Ok(());
795 };
796 let Some(ref pair) = lw.down else {
797 return Ok(());
798 };
799 ops::lora_delta::apply_lora_delta(
800 ctx.gpu,
801 &lw.kernels,
802 pair,
803 act,
804 output,
805 m,
806 ctx.buffers.lora_xa(),
807 ctx.buffers.lora_delta(),
808 stream,
809 )
810 }
811
812 /// Install native keep-packed ternary Q2_0 dense MLP weights. After this
813 /// call, decode `forward` dispatches `q2_0_gemv` per projection (weights
814 /// stay 2-bit resident, no NVFP4 requant) as the highest-priority path.
815 /// Caller must ensure the `q2_0_gemv` kernel is present in the target
816 /// (checked at forward time; falls through to a clear error otherwise).
817 /// Prefill for packed-Q2 is a deferred phase — see `forward_prefill`.
818 pub fn set_q2_weights(
819 &mut self,
820 gate: PackedQ2Weight,
821 up: PackedQ2Weight,
822 down: PackedQ2Weight,
823 gpu: &dyn GpuBackend,
824 ) {
825 self.q2_weights = Some(DenseFfnWeightsQ2 {
826 gate_proj: gate,
827 up_proj: up,
828 down_proj: down,
829 });
830 // Resolved here, not in the constructor: these ship only in
831 // GGUF-serving targets and the boot audit fails closed on an
832 // unconditional probe everywhere else.
833 self.q2_0_mmq_nc_k = super::try_kernel(gpu, "q2_0_mmq", "atlas_q2_0_mmq128_nc");
834 self.q2_0_mmq_wc_k = super::try_kernel(gpu, "q2_0_mmq", "atlas_q2_0_mmq128_wc");
835 }
836
837 /// Install BF16 dense MLP weights. After this call, the forward paths
838 /// dispatch to the BF16 GEMV/GEMM kernels instead of w4a16. The
839 /// caller must ensure the BF16 kernels are loaded (see
840 /// `dense_gemv_bf16_k` / `dense_gemm_bf16_k` checks). Small-batch
841 /// paths reuse `forward_prefill` so they cannot enter NVFP4 kernels
842 /// with the null placeholder weights used by BF16-native layers.
843 pub fn set_bf16_weights(&mut self, gate: DenseWeight, up: DenseWeight, down: DenseWeight) {
844 self.bf16_weights = Some(DenseFfnWeightsBf16 {
845 gate_proj: gate,
846 up_proj: up,
847 down_proj: down,
848 });
849 }
850
851 /// Ensure the int8 W4A8 copy of one NVFP4 projection weight exists, building
852 /// it once via `requant_w_nvfp4_int8` and caching it in `cell`. Reads the
853 /// NON-transposed NVFP4 layout (`weight` = packed E2M1 `[N, K/2]`,
854 /// `weight_scale` = per-16 E4M3 `[N, K/16]`, `weight_scale_2` = per-tensor
855 /// F32) — so it is independent of the `*_proj_t` transposed copies. The
856 /// requant launches on `stream`; the subsequent faith2 read is stream-ordered
857 /// after it, so no host sync is needed.
858 fn ensure_int8_weight(
859 &self,
860 cell: &std::sync::OnceLock<Int8Weight>,
861 gpu: &dyn GpuBackend,
862 src: &QuantizedWeight,
863 n: u32,
864 k: u32,
865 stream: u64,
866 ) -> Result<Int8Weight> {
867 if let Some(w) = cell.get() {
868 return Ok(*w);
869 }
870 let (nn, kk) = (n as usize, k as usize);
871 let w_i8 = gpu.alloc(nn * kk)?; // [N, K] int8
872 let w_scale = gpu.alloc(nn * (kk / 32) * 4)?; // [N, K/32] F32
873 ops::requant_w_nvfp4_int8(
874 gpu,
875 self.requant_w_int8_k,
876 src.weight,
877 src.weight_scale,
878 src.weight_scale_2,
879 w_i8,
880 w_scale,
881 n,
882 k,
883 stream,
884 )?;
885 let built = Int8Weight { w_i8, w_scale };
886 // Lost a race (another thread built first): free our duplicate buffers.
887 if let Err(dup) = cell.set(built) {
888 let _ = gpu.free(dup.w_i8);
889 let _ = gpu.free(dup.w_scale);
890 }
891 Ok(*cell.get().expect("int8 weight cell set above"))
892 }
893
894 /// Lazily materialize a Q4_K FFN weight from the NVFP4 source: dequant NVFP4→bf16
895 /// (transient buffer, freed) then quantize bf16→GGML block_q4_K (cached for the
896 /// process lifetime). `src` is the non-transposed NVFP4 weight `[n, k]`.
897 fn ensure_q4k_weight(
898 &self,
899 cell: &std::sync::OnceLock<Q4kWeight>,
900 gpu: &dyn GpuBackend,
901 src: &QuantizedWeight,
902 n: u32,
903 k: u32,
904 stream: u64,
905 ) -> Result<Q4kWeight> {
906 if let Some(w) = cell.get() {
907 return Ok(*w);
908 }
909 // transient bf16 [n, k] (freed after quantize); persistent Q4_K bytes.
910 let bf16_tmp = gpu.alloc((n as usize) * (k as usize) * 2)?;
911 ops::dequant_nvfp4_to_bf16(
912 gpu,
913 self.dequant_nvfp4_bf16_k,
914 src.weight,
915 src.weight_scale,
916 bf16_tmp,
917 src.weight_scale_2,
918 n,
919 k,
920 stream,
921 )?;
922 let w_q4k = gpu.alloc(ops::q4k_weight_bytes(n, k))?;
923 ops::quantize_weight_q4k(gpu, self.q4k_quant_w_k, bf16_tmp, w_q4k, n, k, stream)?;
924 // bf16_tmp consumed by the quantize on `stream`; sync before freeing it.
925 gpu.synchronize(stream)?;
926 let _ = gpu.free(bf16_tmp);
927 let built = Q4kWeight { w_q4k };
928 if let Err(dup) = cell.set(built) {
929 let _ = gpu.free(dup.w_q4k);
930 }
931 Ok(*cell.get().expect("q4k weight cell set above"))
932 }
933
934 /// Single-token decode: 2-3 kernel launches depending on activation.
935 /// SiLU: dual GEMV + SiLU-fused down GEMV (2 launches).
936 /// GELU: dual GEMV + gelu_mul + down GEMV (3 launches, no fused GELU down kernel).
937 pub fn forward(
938 &self,
939 input: DevicePtr,
940 ctx: &ForwardContext,
941 stream: u64,
942 ) -> Result<DevicePtr> {
943 let h = ctx.config.hidden_size as u32;
944 let inter = ctx.config.intermediate_size as u32;
945
946 let gate_out = ctx.buffers.expert_gate_out();
947 let up_out = ctx.buffers.expert_up_out();
948
949 // Native keep-packed Q2_0 dispatch (highest priority). Per-projection
950 // `q2_0_gemv`: BF16 activation × packed 2-bit weight, dequant in the
951 // dot-product — weights never expand to BF16/NVFP4. No fused dual/silu
952 // kernel yet, so this is gate + up + silu_mul + down (4 launches),
953 // mirroring the FP8 non-fused fallback. SiLU only (Ternary-Bonsai is a
954 // Qwen-family SwiGLU); GeLU packed-Q2 is a follow-up.
955 if let Some(ref q2w) = self.q2_weights {
956 if self.q2_0_gemv_k.0 == 0 {
957 anyhow::bail!(
958 "q2_0_gemv kernel missing in this target build — packed-Q2 decode \
959 (ATLAS_GGUF_NATIVE_Q2) is unavailable"
960 );
961 }
962 if self.activation != FfnActivation::SiLU {
963 anyhow::bail!(
964 "packed-Q2 FFN decode supports SiLU only (got {:?})",
965 self.activation
966 );
967 }
968 let output = ctx.buffers.moe_output();
969 ops::q2_0_gemv_vec(
970 ctx.gpu,
971 self.q2_0_gemv_k,
972 input,
973 &q2w.gate_proj,
974 gate_out,
975 stream,
976 )?;
977 ops::q2_0_gemv_vec(
978 ctx.gpu,
979 self.q2_0_gemv_k,
980 input,
981 &q2w.up_proj,
982 up_out,
983 stream,
984 )?;
985 ops::silu_mul(
986 ctx.gpu,
987 self.act_mul,
988 gate_out,
989 up_out,
990 gate_out,
991 inter,
992 stream,
993 )?;
994 ops::q2_0_gemv_vec(
995 ctx.gpu,
996 self.q2_0_gemv_k,
997 gate_out,
998 &q2w.down_proj,
999 output,
1000 stream,
1001 )?;
1002 return Ok(output);
1003 }
1004
1005 // FP8 dispatch: prefer the fused FP8 dual-GEMV (gate+up in one launch) +
1006 // SiLU-fused down GEMV, mirroring the NVFP4 path. Collapses gate+up+
1007 // silu_mul+down (4 launches) to dual+silu (2). Falls back to the
1008 // 3-launch per-projection `w8a16_gemv` path when the fused kernels or a
1009 // non-SiLU activation make the fast path unavailable.
1010 if let Some(ref fp8w) = self.fp8_weights {
1011 let output = ctx.buffers.moe_output();
1012 if self.activation == FfnActivation::SiLU
1013 && self.w8a16_gemv_dual_k.0 != 0
1014 && self.w8a16_gemv_silu_input_k.0 != 0
1015 {
1016 ops::w8a16_gemv_dual(
1017 ctx.gpu,
1018 self.w8a16_gemv_dual_k,
1019 input,
1020 fp8w.gate_proj.weight,
1021 fp8w.gate_proj.row_scale,
1022 gate_out,
1023 fp8w.up_proj.weight,
1024 fp8w.up_proj.row_scale,
1025 up_out,
1026 inter,
1027 h,
1028 stream,
1029 )?;
1030 ops::w8a16_gemv_silu_input(
1031 ctx.gpu,
1032 self.w8a16_gemv_silu_input_k,
1033 gate_out,
1034 up_out,
1035 fp8w.down_proj.weight,
1036 fp8w.down_proj.row_scale,
1037 output,
1038 h,
1039 inter,
1040 stream,
1041 )?;
1042 return Ok(output);
1043 }
1044 ops::w8a16_gemv(
1045 ctx.gpu,
1046 self.w8a16_gemv_k,
1047 input,
1048 fp8w.gate_proj.weight,
1049 fp8w.gate_proj.row_scale,
1050 gate_out,
1051 inter,
1052 h,
1053 stream,
1054 )?;
1055 ops::w8a16_gemv(
1056 ctx.gpu,
1057 self.w8a16_gemv_k,
1058 input,
1059 fp8w.up_proj.weight,
1060 fp8w.up_proj.row_scale,
1061 up_out,
1062 inter,
1063 h,
1064 stream,
1065 )?;
1066 ops::silu_mul(
1067 ctx.gpu,
1068 self.act_mul,
1069 gate_out,
1070 up_out,
1071 gate_out,
1072 inter,
1073 stream,
1074 )?;
1075 ops::w8a16_gemv(
1076 ctx.gpu,
1077 self.w8a16_gemv_k,
1078 gate_out,
1079 fp8w.down_proj.weight,
1080 fp8w.down_proj.row_scale,
1081 output,
1082 h,
1083 inter,
1084 stream,
1085 )?;
1086 return Ok(output);
1087 }
1088
1089 // BF16 dispatch: per-projection GEMV via `dense_gemv_bf16`. We
1090 // don't have a fused dual-BF16-GEMV kernel today; two sequential
1091 // launches are still BF16-precision-correct and only ~10% slower
1092 // than the fused w4a16 path on Gemma-4-31B (the cost is dominated
1093 // by the bigger BF16 weight reads, not launch overhead).
1094 if let Some(ref bf16w) = self.bf16_weights {
1095 ops::dense_gemv(
1096 ctx.gpu,
1097 self.dense_gemv_bf16_k,
1098 input,
1099 &bf16w.gate_proj,
1100 gate_out,
1101 inter,
1102 h,
1103 stream,
1104 )?;
1105 ops::dense_gemv(
1106 ctx.gpu,
1107 self.dense_gemv_bf16_k,
1108 input,
1109 &bf16w.up_proj,
1110 up_out,
1111 inter,
1112 h,
1113 stream,
1114 )?;
1115 ops::silu_mul(
1116 ctx.gpu,
1117 self.act_mul,
1118 gate_out,
1119 up_out,
1120 gate_out,
1121 inter,
1122 stream,
1123 )?;
1124 let output = ctx.buffers.moe_output();
1125 ops::dense_gemv(
1126 ctx.gpu,
1127 self.dense_gemv_bf16_k,
1128 gate_out,
1129 &bf16w.down_proj,
1130 output,
1131 h,
1132 inter,
1133 stream,
1134 )?;
1135 return Ok(output);
1136 }
1137
1138 // ATLAS_DECODE_FFN_VIA_GEMM=1: route decode's M=1 FFN projections
1139 // through the SAME transposed-weight GEMM kernels the DFlash verify
1140 // path uses (`w4a16_prefill_gemm` → w4a16_gemm_t / _t_k64), instead
1141 // of the dedicated GEMV kernels. Purpose: bit-identical FFN numerics
1142 // between serial decode and batched verify — the batch-K vs batch-1
1143 // divergence #218's bisect isolated ("FFN non-associativity") and the
1144 // root cause of the T=0 spec trajectory flips (2026-07-07 session).
1145 // Split SiLU staging already matches prefill SiLU numerics (swiglu
1146 // clamp), so with this arm the whole FFN block is kernel-identical to
1147 // a verify row. Requires the *_proj_t transposed copies (the NVFP4-MMQ
1148 // prefill arm FREES them — disable it if the warn below fires).
1149 // The `OnceLock<bool>` static that lived here is now a field on
1150 // `layers::ops::ModelLevers` — resolved when the model is built and carried
1151 // on `ForwardContext`, because a static outlives the model whose flags it
1152 // encodes.
1153 if ctx.levers.decode_ffn_via_gemm
1154 && self.activation == FfnActivation::SiLU
1155 && self.act_mul.0 != 0
1156 {
1157 let wt_alive =
1158 |w: &Option<QuantizedWeight>| w.as_ref().is_some_and(|w| !w.weight.is_null());
1159 if wt_alive(&self.weights.gate_proj_t) && wt_alive(&self.weights.up_proj_t) {
1160 // Log-once latch (see `atlas_core::scope`). It holds no model-derived
1161 // value — the message is rebuilt from the arguments every call — so a
1162 // stale entry cannot produce a wrong answer, only a suppressed duplicate
1163 // line after a model swap. Scoping it would thread a logging concern
1164 // through the call path to prevent one repeated INFO line.
1165 if ctx.stats.once("log:decode_ffn_via_gemm") {
1166 tracing::info!(
1167 "decode FFN via verify GEMM path (ATLAS_DECODE_FFN_VIA_GEMM=1): \
1168 gate/up/down through w4a16_prefill_gemm at M=1"
1169 );
1170 }
1171 self.w4a16_prefill_gemm(
1172 ctx,
1173 &self.weights.gate_proj,
1174 self.weights.gate_proj_t.as_ref(),
1175 input,
1176 gate_out,
1177 1,
1178 inter,
1179 h,
1180 stream,
1181 )?;
1182 self.w4a16_prefill_gemm(
1183 ctx,
1184 &self.weights.up_proj,
1185 self.weights.up_proj_t.as_ref(),
1186 input,
1187 up_out,
1188 1,
1189 inter,
1190 h,
1191 stream,
1192 )?;
1193 ops::silu_mul(
1194 ctx.gpu,
1195 self.act_mul,
1196 gate_out,
1197 up_out,
1198 gate_out,
1199 inter,
1200 stream,
1201 )?;
1202 let output = ctx.buffers.moe_output();
1203 self.w4a16_prefill_gemm(
1204 ctx,
1205 &self.weights.down_proj,
1206 self.weights.down_proj_t.as_ref(),
1207 gate_out,
1208 output,
1209 1,
1210 h,
1211 inter,
1212 stream,
1213 )?;
1214 return Ok(output);
1215 }
1216 // Log-once latch (see `atlas_core::scope`). It holds no model-derived
1217 // value — the message is rebuilt from the arguments every call — so a
1218 // stale entry cannot produce a wrong answer, only a suppressed duplicate
1219 // line after a model swap. Scoping it would thread a logging concern
1220 // through the call path to prevent one repeated INFO line.
1221 if ctx.stats.once("log:decode_ffn_no_twins") {
1222 tracing::warn!(
1223 "ATLAS_DECODE_FFN_VIA_GEMM=1 requested but transposed FFN copies \
1224 are freed/absent (NVFP4-MMQ prefill arm?) — falling back to GEMV; \
1225 the unification experiment is NOT active"
1226 );
1227 }
1228 }
1229
1230 // Fused gate_proj + up_proj: [1, H] → [1, inter] × 2.
1231 // Single-warp variant (lossless) when the lever is on and the kernel
1232 // resolved; otherwise the 64-thread kernel. Dual and silu-input SW
1233 // are independent — missing silu_input_sw must not skip dual_sw on
1234 // the default split-SiLU path.
1235 let use_dual_sw = ops::use_gemv_sw(ctx.levers.gemv_sw, self.w4a16_gemv_dual_sw);
1236 let use_silu_sw = ops::use_gemv_sw(ctx.levers.gemv_sw, self.w4a16_gemv_silu_input_sw);
1237 if use_dual_sw {
1238 ops::w4a16_gemv_dual_sw(
1239 ctx.gpu,
1240 self.w4a16_gemv_dual_sw,
1241 input,
1242 &self.weights.gate_proj,
1243 gate_out,
1244 &self.weights.up_proj,
1245 up_out,
1246 inter,
1247 h,
1248 stream,
1249 )?;
1250 } else {
1251 ops::w4a16_gemv_dual(
1252 ctx.gpu,
1253 self.w4a16_gemv_dual,
1254 input,
1255 &self.weights.gate_proj,
1256 gate_out,
1257 &self.weights.up_proj,
1258 up_out,
1259 inter,
1260 h,
1261 stream,
1262 )?;
1263 }
1264
1265 let output = ctx.buffers.moe_output();
1266 // Split SiLU+down (DEFAULT; kill-switch ATLAS_NO_DECODE_SPLIT_SILU): the fused
1267 // silu_input kernel recomputes the SiLU transcendentals per OUTPUT ROW (N/4
1268 // blocks × redundant __expf) and measures COMPUTE-bound — ncu: SM 57% vs
1269 // memory 23%, 186 GB/s vs the dual GEMV's 266. Staging silu(gate)*up once
1270 // (one elementwise launch, CUDA graphs amortize it) lets the down GEMV run
1271 // memory-bound like the dual. Also aligns decode with the prefill SiLU
1272 // numerics (swiglu clamp), which the fused kernel lacked.
1273 //
1274 // An installed LoRA adapter PINS this path. The fused `silu_input`
1275 // alternative never materialises silu(gate)*up — it consumes gate and
1276 // up straight into the down GEMV — and the down delta has to contract
1277 // over exactly that activation. Reproducing it into `lora_hact` just
1278 // to feed the delta would compute the SiLU twice for a path that is
1279 // already the default and already the numerically preferred one, so
1280 // the adapter pins it instead. `set_lora_weights` refuses an adapter
1281 // when this path is unavailable on the layer, which makes
1282 // `lora.is_some()` imply the three conditions above.
1283 let split_silu = self.activation == FfnActivation::SiLU
1284 && self.act_mul.0 != 0
1285 && self.w4a16_gemv.0 != 0
1286 && (std::env::var_os("ATLAS_NO_DECODE_SPLIT_SILU").is_none() || self.lora.is_some());
1287 if split_silu {
1288 self.apply_lora_gate_up(ctx, input, gate_out, up_out, 1, stream)?;
1289 ops::silu_mul(
1290 ctx.gpu,
1291 self.act_mul,
1292 gate_out,
1293 up_out,
1294 gate_out,
1295 inter,
1296 stream,
1297 )?;
1298 ops::w4a16_decode_gemv(
1299 ctx.gpu,
1300 self.w4a16_gemv,
1301 self.w4a16_gemv_sw,
1302 ctx.levers.gemv_sw,
1303 gate_out,
1304 &self.weights.down_proj,
1305 output,
1306 h,
1307 inter,
1308 stream,
1309 )?;
1310 self.apply_lora_down(ctx, gate_out, output, 1, stream)?;
1311 return Ok(output);
1312 }
1313 debug_assert!(
1314 self.lora.is_none(),
1315 "LoRA installed but decode took the fused silu_input path, which \
1316 never materialises the activation the down delta contracts over; \
1317 set_lora_weights is supposed to make this unreachable"
1318 );
1319 match self.activation {
1320 FfnActivation::SiLU => {
1321 // Fused SiLU(gate)*up + down_proj: [1, inter] → [1, H]
1322 if use_silu_sw {
1323 ops::w4a16_gemv_silu_input_sw(
1324 ctx.gpu,
1325 self.w4a16_gemv_silu_input_sw,
1326 gate_out,
1327 up_out,
1328 &self.weights.down_proj,
1329 output,
1330 h,
1331 inter,
1332 stream,
1333 )?;
1334 } else {
1335 ops::w4a16_gemv_silu_input(
1336 ctx.gpu,
1337 self.w4a16_gemv_silu_input,
1338 gate_out,
1339 up_out,
1340 &self.weights.down_proj,
1341 output,
1342 h,
1343 inter,
1344 stream,
1345 )?;
1346 }
1347 }
1348 FfnActivation::GeLU => {
1349 // GELU(gate)*up → gate_out, then down_proj GEMV
1350 ops::silu_mul(
1351 ctx.gpu,
1352 self.act_mul,
1353 gate_out,
1354 up_out,
1355 gate_out,
1356 inter,
1357 stream,
1358 )?;
1359 ops::w4a16_decode_gemv(
1360 ctx.gpu,
1361 self.w4a16_gemv,
1362 self.w4a16_gemv_sw,
1363 ctx.levers.gemv_sw,
1364 gate_out,
1365 &self.weights.down_proj,
1366 output,
1367 h,
1368 inter,
1369 stream,
1370 )?;
1371 }
1372 }
1373
1374 Ok(output)
1375 }
1376
1377 /// Packed-Q2 batched decode FFN for `m` concurrent rows (`m >= 2`). Mirrors
1378 /// the single-token `forward` packed-Q2 arm — per-projection keep-packed
1379 /// `q2_0_gemv_vec_batchm` (BF16 `[m,·]` activation × 2-bit weight, dequant in
1380 /// the dot-product, no BF16/NVFP4 expansion), SiLU-mul between gate/up, then
1381 /// down — but with `m` activation rows staged per weight read. This is the
1382 /// correctness path for concurrent decode (C>=2): the NVFP4 `forward_k2/k3`
1383 /// GEMVs read the NULL NVFP4 fallback weights, so packed-Q2 must route here.
1384 /// The wrapper chunks internally for `m > 8`. SiLU only (Ternary-Bonsai is a
1385 /// SwiGLU); output lands in `moe_output` as `[m, h]` row-major.
1386 fn forward_km_q2(
1387 &self,
1388 q2w: &DenseFfnWeightsQ2,
1389 input: DevicePtr,
1390 ctx: &ForwardContext,
1391 m: u32,
1392 stream: u64,
1393 ) -> Result<()> {
1394 if self.q2_0_gemv_batchm_k.0 == 0 {
1395 anyhow::bail!(
1396 "q2_0_gemv_vec_batchm kernel missing in this target build — packed-Q2 \
1397 batched decode (ATLAS_GGUF_NATIVE_Q2, C>=2) is unavailable"
1398 );
1399 }
1400 if self.activation != FfnActivation::SiLU {
1401 anyhow::bail!(
1402 "packed-Q2 FFN batched decode supports SiLU only (got {:?})",
1403 self.activation
1404 );
1405 }
1406 let inter = ctx.config.intermediate_size as u32;
1407 let gate_out = ctx.buffers.expert_gate_out();
1408 let up_out = ctx.buffers.expert_up_out();
1409 let batchm = |w: &PackedQ2Weight, inp: DevicePtr, out: DevicePtr| -> Result<()> {
1410 ops::q2_0_gemv_vec_batchm(ctx.gpu, self.q2_0_gemv_batchm_k, inp, w, out, m, stream)
1411 };
1412 batchm(&q2w.gate_proj, input, gate_out)?;
1413 batchm(&q2w.up_proj, input, up_out)?;
1414 ops::silu_mul(
1415 ctx.gpu,
1416 self.act_mul,
1417 gate_out,
1418 up_out,
1419 gate_out,
1420 m * inter,
1421 stream,
1422 )?;
1423 let output = ctx.buffers.moe_output();
1424 batchm(&q2w.down_proj, gate_out, output)?;
1425 Ok(())
1426 }
1427
1428 /// K=2 speculative: batched GEMV for 2 tokens.
1429 /// 3 launches: dual batch2 (gate+up) + silu_mul + batch2 (down).
1430 pub fn forward_k2(&self, input: DevicePtr, ctx: &ForwardContext, stream: u64) -> Result<()> {
1431 // Packed-Q2: NVFP4 fallback weights are NULL, so the NVFP4 batch2 GEMVs
1432 // below would fault. Route to the keep-packed batchm FFN (m=2).
1433 if let Some(ref q2w) = self.q2_weights {
1434 return self.forward_km_q2(q2w, input, ctx, 2, stream);
1435 }
1436 if native_small_batch_uses_prefill(self.bf16_weights.is_some(), self.fp8_weights.is_some())
1437 {
1438 return self.forward_prefill(input, 2, ctx, stream);
1439 }
1440
1441 let h = ctx.config.hidden_size as u32;
1442 let inter = ctx.config.intermediate_size as u32;
1443
1444 let gate_out = ctx.buffers.expert_gate_out();
1445 let up_out = ctx.buffers.expert_up_out();
1446
1447 // Fused gate+up for 2 tokens
1448 ops::w4a16_gemv_dual_batch2(
1449 ctx.gpu,
1450 self.w4a16_gemv_dual_batch2,
1451 input,
1452 &self.weights.gate_proj,
1453 gate_out,
1454 &self.weights.up_proj,
1455 up_out,
1456 inter,
1457 h,
1458 stream,
1459 )?;
1460 self.apply_lora_gate_up(ctx, input, gate_out, up_out, 2, stream)?;
1461 ops::silu_mul(
1462 ctx.gpu,
1463 self.act_mul,
1464 gate_out,
1465 up_out,
1466 gate_out,
1467 2 * inter,
1468 stream,
1469 )?;
1470 let output = ctx.buffers.moe_output();
1471 ops::w4a16_gemv_batch2(
1472 ctx.gpu,
1473 self.w4a16_gemv_batch2,
1474 gate_out,
1475 &self.weights.down_proj,
1476 output,
1477 h,
1478 inter,
1479 stream,
1480 )?;
1481 self.apply_lora_down(ctx, gate_out, output, 2, stream)?;
1482
1483 Ok(())
1484 }
1485
1486 /// K=3 speculative: batched GEMV for 3 tokens.
1487 /// 3 launches: dual batch3 (gate+up) + silu_mul + batch3 (down).
1488 pub fn forward_k3(&self, input: DevicePtr, ctx: &ForwardContext, stream: u64) -> Result<()> {
1489 // Packed-Q2: route to the keep-packed batchm FFN (m=3); NVFP4 weights null.
1490 if let Some(ref q2w) = self.q2_weights {
1491 return self.forward_km_q2(q2w, input, ctx, 3, stream);
1492 }
1493 if native_small_batch_uses_prefill(self.bf16_weights.is_some(), self.fp8_weights.is_some())
1494 {
1495 return self.forward_prefill(input, 3, ctx, stream);
1496 }
1497
1498 let h = ctx.config.hidden_size as u32;
1499 let inter = ctx.config.intermediate_size as u32;
1500
1501 let gate_out = ctx.buffers.expert_gate_out();
1502 let up_out = ctx.buffers.expert_up_out();
1503
1504 // Fused gate+up for 3 tokens
1505 ops::w4a16_gemv_dual_batch3(
1506 ctx.gpu,
1507 self.w4a16_gemv_dual_batch3,
1508 input,
1509 &self.weights.gate_proj,
1510 gate_out,
1511 &self.weights.up_proj,
1512 up_out,
1513 inter,
1514 h,
1515 stream,
1516 )?;
1517 self.apply_lora_gate_up(ctx, input, gate_out, up_out, 3, stream)?;
1518 ops::silu_mul(
1519 ctx.gpu,
1520 self.act_mul,
1521 gate_out,
1522 up_out,
1523 gate_out,
1524 3 * inter,
1525 stream,
1526 )?;
1527 let output = ctx.buffers.moe_output();
1528 ops::w4a16_gemv_batch3(
1529 ctx.gpu,
1530 self.w4a16_gemv_batch3,
1531 gate_out,
1532 &self.weights.down_proj,
1533 output,
1534 h,
1535 inter,
1536 stream,
1537 )?;
1538 self.apply_lora_down(ctx, gate_out, output, 3, stream)?;
1539
1540 Ok(())
1541 }
1542
1543 /// Batchm-GEMV kernel for `m` verify rows: the narrowest resolved tier in
1544 /// `w4a16_gemv_batch{4,5,6,7,8}` that covers `m`. 0-handle when out of
1545 /// range or absent. See `layers::w4a16_gemv_tiers` for the decision and
1546 /// the `ATLAS_NO_GEMV_EXACT_M_TIERS=1` kill switch.
1547 fn batchm_kernel(&self, m: u32) -> KernelHandle {
1548 self.w4a16_batchm.kernel(m)
1549 }
1550
1551 /// Whether the M-row batched-GEMV verify path is available for `m` rows
1552 /// (batchm kernel present AND NVFP4 weights loaded — the batchm GEMV
1553 /// reads the non-transposed NVFP4 layout).
1554 pub fn can_forward_km(&self, m: u32) -> bool {
1555 self.batchm_kernel(m).0 != 0 && !self.weights.gate_proj.weight.is_null()
1556 }
1557
1558 /// K=m (m<=8) speculative verify: batched GEMV for m tokens.
1559 /// 4 launches: batchm gate + batchm up + silu_mul + batchm down — each
1560 /// projection weight is read ONCE for all m rows at near-peak stream
1561 /// bandwidth. nsys (2026-07-18, M=4): the `forward_prefill` MMQ arm this
1562 /// replaces for the K=4 verify cost 54.8 ms/step across the 64-layer
1563 /// dense FFN stack (~156 GB/s effective at M=4); the batch GEMV family
1564 /// measures ~290 GB/s on the same shapes (w8a16_gemv_batch4 sibling),
1565 /// putting this path at the ~31 ms weight-traffic floor. m=5..8 uses
1566 /// `w4a16_gemv_batch8` (batchm_bench: same weight-streaming bandwidth,
1567 /// removing the M>4 tile-GEMM cliff for chain-verify K=5..8).
1568 pub fn forward_km(
1569 &self,
1570 input: DevicePtr,
1571 m: u32,
1572 ctx: &ForwardContext,
1573 stream: u64,
1574 ) -> Result<()> {
1575 // As in k2/k3, concurrent decode must preserve an installed native
1576 // overlay even when valid, lower-precision NVFP4 fallbacks coexist.
1577 if native_small_batch_uses_prefill(self.bf16_weights.is_some(), self.fp8_weights.is_some())
1578 {
1579 return self.forward_prefill(input, m as usize, ctx, stream);
1580 }
1581 let h = ctx.config.hidden_size as u32;
1582 let inter = ctx.config.intermediate_size as u32;
1583 let kh = self.batchm_kernel(m);
1584
1585 let gate_out = ctx.buffers.expert_gate_out();
1586 let up_out = ctx.buffers.expert_up_out();
1587
1588 ops::w4a16_gemv_batchm(
1589 ctx.gpu,
1590 kh,
1591 input,
1592 &self.weights.gate_proj,
1593 gate_out,
1594 m,
1595 inter,
1596 h,
1597 stream,
1598 )?;
1599 ops::w4a16_gemv_batchm(
1600 ctx.gpu,
1601 kh,
1602 input,
1603 &self.weights.up_proj,
1604 up_out,
1605 m,
1606 inter,
1607 h,
1608 stream,
1609 )?;
1610 self.apply_lora_gate_up(ctx, input, gate_out, up_out, m, stream)?;
1611 ops::silu_mul(
1612 ctx.gpu,
1613 self.act_mul,
1614 gate_out,
1615 up_out,
1616 gate_out,
1617 m * inter,
1618 stream,
1619 )?;
1620 let output = ctx.buffers.moe_output();
1621 ops::w4a16_gemv_batchm(
1622 ctx.gpu,
1623 kh,
1624 gate_out,
1625 &self.weights.down_proj,
1626 output,
1627 m,
1628 h,
1629 inter,
1630 stream,
1631 )?;
1632 self.apply_lora_down(ctx, gate_out, output, m, stream)?;
1633
1634 Ok(())
1635 }
1636
1637 /// N-token prefill: GEMM for all projections.
1638 /// W4A16 prefill/verify GEMM dispatch, routed by (M, K) per
1639 /// w4a16_m17_bench measurements on GB10:
1640 /// - M<=64 (DFlash verify M=17): the M64-tile `w4a16_gemm_t` beats the
1641 /// M128-tile kernels (283 vs 324us on gate/up — 87% of an M128 tile
1642 /// is padding at M=17), and `w4a16_gemm_t_k64` wins deep-K down_proj
1643 /// (554 vs 810us at K=17408, where N/128 CTAs can't fill the GPU and
1644 /// the halved K-loop matters).
1645 /// - M>64 (real prefill): v2 (8-warp) > t_m128 (4-warp), unchanged.
1646 /// - No transposed copy: base `w4a16_gemm` (9-12x the bandwidth floor —
1647 /// last resort).
1648 ///
1649 /// Kill-switch: ATLAS_FFN_SMALLM=0 restores the m128-only dispatch for A/B.
1650 #[allow(clippy::too_many_arguments)]
1651 fn w4a16_prefill_gemm(
1652 &self,
1653 ctx: &ForwardContext,
1654 w: &QuantizedWeight,
1655 wt: Option<&QuantizedWeight>,
1656 input: DevicePtr,
1657 output: DevicePtr,
1658 m: u32,
1659 n: u32,
1660 k: u32,
1661 stream: u64,
1662 ) -> Result<()> {
1663 // The `OnceLock<bool>` static that lived here is now a field on
1664 // `layers::ops::ModelLevers` — resolved when the model is built and carried
1665 // on `ForwardContext`, because a static outlives the model whose flags it
1666 // encodes.
1667 if let Some(wt) = wt {
1668 if m <= 64 && k.is_multiple_of(32) && ctx.levers.ffn_small_m {
1669 if k >= crate::layers::w4a16_k64_min_k()
1670 && k.is_multiple_of(64)
1671 && self.w4a16_gemm_t_k64_k.0 != 0
1672 {
1673 return ops::w4a16_gemm_n128(
1674 ctx.gpu,
1675 self.w4a16_gemm_t_k64_k,
1676 input,
1677 wt,
1678 output,
1679 m,
1680 n,
1681 k,
1682 stream,
1683 );
1684 }
1685 if self.w4a16_gemm_t_k.0 != 0 {
1686 return ops::w4a16_gemm_n128(
1687 ctx.gpu,
1688 self.w4a16_gemm_t_k,
1689 input,
1690 wt,
1691 output,
1692 m,
1693 n,
1694 k,
1695 stream,
1696 );
1697 }
1698 }
1699 if self.w4a16_gemm_t_m128_v2_k.0 != 0 {
1700 return ops::w4a16_gemm_n128_m128_v2(
1701 ctx.gpu,
1702 self.w4a16_gemm_t_m128_v2_k,
1703 input,
1704 wt,
1705 output,
1706 m,
1707 n,
1708 k,
1709 stream,
1710 );
1711 }
1712 if self.w4a16_gemm_t_m128_k.0 != 0 {
1713 return ops::w4a16_gemm_n128_m128(
1714 ctx.gpu,
1715 self.w4a16_gemm_t_m128_k,
1716 input,
1717 wt,
1718 output,
1719 m,
1720 n,
1721 k,
1722 stream,
1723 );
1724 }
1725 }
1726 ops::w4a16_gemm(ctx.gpu, self.w4a16_gemm, input, w, output, m, n, k, stream)
1727 }
1728
1729 /// Timed wrapper around the dense-FFN prefill.
1730 ///
1731 /// ★ THIS PATH HAD NO TIMERS AT ALL, and that hid the largest unexplained
1732 /// number on the board. Profiling nvidia/Gemma-4-31B-IT-NVFP4 at a
1733 /// 4096-token prompt: wall 28,180 ms, while EVERY profiled phase across
1734 /// `ATTN prefill [...]` and `MoE prefill [...]` summed to 3,269.8 ms. 88% of
1735 /// the prefill was invisible — not attributed to something slow, simply not
1736 /// instrumented. `forward_prefill` dispatches ~20 quantization arms and none
1737 /// of them reported elapsed time; only one-shot "which arm was chosen" INFO
1738 /// lines existed.
1739 ///
1740 /// One coarse timer first, deliberately: it answers whether the missing time
1741 /// is here at all before anyone threads timers through twenty arms. Same
1742 /// `<AREA> prefill [phase] N=<n>: <us>µs` shape the attention and MoE paths
1743 /// already emit, so the existing log-summing one-liners pick it up unchanged.
1744 pub fn forward_prefill(
1745 &self,
1746 input: DevicePtr,
1747 num_tokens: usize,
1748 ctx: &ForwardContext,
1749 stream: u64,
1750 ) -> Result<()> {
1751 if !ctx.profile {
1752 return self.forward_prefill_inner(input, num_tokens, ctx, stream);
1753 }
1754 let t0 = std::time::Instant::now();
1755 let r = self.forward_prefill_inner(input, num_tokens, ctx, stream);
1756 // Sync so the figure is the kernel's, not the launch queue's — the
1757 // attention and MoE timers do the same under `ctx.profile`.
1758 ctx.gpu.synchronize(stream)?;
1759 tracing::info!(
1760 " FFN prefill [dense_total] N={}: {}µs",
1761 num_tokens,
1762 t0.elapsed().as_micros()
1763 );
1764 r
1765 }
1766
1767 fn forward_prefill_inner(
1768 &self,
1769 input: DevicePtr,
1770 num_tokens: usize,
1771 ctx: &ForwardContext,
1772 stream: u64,
1773 ) -> Result<()> {
1774 let h = ctx.config.hidden_size as u32;
1775 let inter = ctx.config.intermediate_size as u32;
1776 let m = num_tokens as u32;
1777
1778 let gate_out = ctx.buffers.expert_gate_out();
1779 let up_out = ctx.buffers.expert_up_out();
1780
1781 // Native keep-packed Q2_0 prefill (Tier-1): the resident weight stays
1782 // 2-bit, but prefill has no packed-MMQ kernel yet (that's Tier-2). So we
1783 // dequant each projection into a TRANSIENT BF16 scratch `[N, K]` via the
1784 // load-time `dequant_q2_0_gn_to_bf16` kernel, run the normal BF16
1785 // prefill GEMM (tensor-core when present), then free the scratch. Only a
1786 // per-matmul scratch is BF16 — the WeightStore blocks stay 2-bit. Decode
1787 // still uses the native `q2_0_gemv` (no dequant). SiLU only.
1788 if let Some(ref q2w) = self.q2_weights {
1789 if self.activation != FfnActivation::SiLU {
1790 anyhow::bail!(
1791 "packed-Q2 FFN prefill supports SiLU only (got {:?})",
1792 self.activation
1793 );
1794 }
1795
1796 // Tier-2 native MMQ prefill (ATLAS_GGUF_NATIVE_Q2_MMQ=1): quantize the
1797 // activation to q8_1 ONCE per projection-input (gate/up share `input`;
1798 // down re-quantizes `gate_out`), then run the packed 2-bit MMQ GEMM —
1799 // no BF16 weight dequant, no shared `q2_dequant_scratch`. Requires the
1800 // MMQ kernel + the shared q8_1 quantizer + group-128 weights.
1801 let q2_mmq = self.q2_0_mmq_nc_k.0 != 0
1802 && self.q4k_quant_act_k.0 != 0
1803 && ops::native_q2_mmq_enabled()
1804 && q2w.gate_proj.group == 128
1805 && q2w.up_proj.group == 128
1806 && q2w.down_proj.group == 128;
1807 if q2_mmq {
1808 static Q2MMQ_LOG: std::sync::Once = std::sync::Once::new();
1809 Q2MMQ_LOG.call_once(|| {
1810 eprintln!(
1811 "[atlas] ATLAS_GGUF_NATIVE_Q2_MMQ=1: dense-FFN prefill via native packed Q2_0 MMQ (W2A8, keep-packed)"
1812 );
1813 });
1814 let a_q8 = ctx.buffers.q2_act_q8();
1815 let mmq = |w: &PackedQ2Weight, out: DevicePtr| -> Result<()> {
1816 ops::q2_0_mmq_gemm(
1817 ctx.gpu,
1818 self.q2_0_mmq_nc_k,
1819 self.q2_0_mmq_wc_k,
1820 a_q8,
1821 w.weight,
1822 out,
1823 m,
1824 w.n,
1825 w.k,
1826 stream,
1827 )
1828 };
1829 // gate/up: quantize `input` [m,h] once, feed both.
1830 ops::quantize_act_q8_1(ctx.gpu, self.q4k_quant_act_k, input, a_q8, m, h, stream)?;
1831 mmq(&q2w.gate_proj, gate_out)?;
1832 mmq(&q2w.up_proj, up_out)?;
1833 ops::silu_mul(
1834 ctx.gpu,
1835 self.act_mul,
1836 gate_out,
1837 up_out,
1838 gate_out,
1839 m * inter,
1840 stream,
1841 )?;
1842 // down: quantize `gate_out` [m,inter] (same-stream after silu_mul).
1843 let output = ctx.buffers.moe_output();
1844 ops::quantize_act_q8_1(
1845 ctx.gpu,
1846 self.q4k_quant_act_k,
1847 gate_out,
1848 a_q8,
1849 m,
1850 inter,
1851 stream,
1852 )?;
1853 mmq(&q2w.down_proj, output)?;
1854 return Ok(());
1855 }
1856
1857 // Transient-dequant stopgap (Tier-1): requires the load-time dequant kernel.
1858 if self.dequant_q2_0_gn_k.0 == 0 {
1859 anyhow::bail!(
1860 "dequant_q2_0_gn_to_bf16 kernel missing in this target build — \
1861 packed-Q2 (ATLAS_GGUF_NATIVE_Q2) prefill is unavailable"
1862 );
1863 }
1864 let tc = self.dense_gemm_tc_k.0 != 0;
1865 // Dequant one packed-Q2 projection into the PERSISTENT arena BF16
1866 // scratch `[n, k]`, run the BF16 GEMM (A=`in` [m,k] → out [m,n]). No
1867 // per-matmul alloc/sync/free: the arena buffer is sized to the
1868 // largest packed projection and reused. Gate → up → down run
1869 // sequentially on `stream`, so each GEMM consumes the scratch before
1870 // the next projection's dequant overwrites it (same-stream order).
1871 let scratch = ctx.buffers.q2_dequant_scratch();
1872 let q2_gemm = |w: &PackedQ2Weight, input: DevicePtr, out: DevicePtr| -> Result<()> {
1873 let (n, k) = (w.n, w.k);
1874 debug_assert!(
1875 (n as usize) * (k as usize) * 2 <= ctx.buffers.q2_dequant_scratch_bytes(),
1876 "packed-Q2 FFN dequant scratch too small for [{n},{k}] BF16"
1877 );
1878 ops::dequant_q2_0_gn_to_bf16(
1879 ctx.gpu,
1880 self.dequant_q2_0_gn_k,
1881 w.weight,
1882 scratch,
1883 n,
1884 k,
1885 w.group as u32,
1886 stream,
1887 )?;
1888 let dw = DenseWeight { weight: scratch };
1889 if tc {
1890 ops::dense_gemm_tc(
1891 ctx.gpu,
1892 self.dense_gemm_tc_k,
1893 input,
1894 &dw,
1895 out,
1896 m,
1897 n,
1898 k,
1899 stream,
1900 )?;
1901 } else {
1902 ops::dense_gemm(
1903 ctx.gpu,
1904 self.dense_gemm_bf16_k,
1905 input,
1906 &dw,
1907 out,
1908 m,
1909 n,
1910 k,
1911 stream,
1912 )?;
1913 }
1914 Ok(())
1915 };
1916 q2_gemm(&q2w.gate_proj, input, gate_out)?;
1917 q2_gemm(&q2w.up_proj, input, up_out)?;
1918 ops::silu_mul(
1919 ctx.gpu,
1920 self.act_mul,
1921 gate_out,
1922 up_out,
1923 gate_out,
1924 m * inter,
1925 stream,
1926 )?;
1927 let output = ctx.buffers.moe_output();
1928 q2_gemm(&q2w.down_proj, gate_out, output)?;
1929 return Ok(());
1930 }
1931
1932 // Native FP8: small batches stream each weight once via the existing
1933 // M<=4 GEMV, avoiding padded MMA tiles. Larger prefills prefer a
1934 // transposed copy when available, then the same-format pipelined GEMM.
1935 // Every fallback retains the original E4M3 bytes and FP32 block scales.
1936 if let Some(ref fp8w) = self.fp8_weights {
1937 macro_rules! w8_gemm {
1938 ($w:expr, $wt:expr, $in:expr, $out:expr, $n:expr, $k:expr) => {
1939 match $wt {
1940 _ if (1..=4).contains(&m) && self.w8a16_gemv_batch4_k.0 != 0 => {
1941 ops::w8a16_gemv_batch4(
1942 ctx.gpu,
1943 self.w8a16_gemv_batch4_k,
1944 $in,
1945 $w.weight,
1946 $w.row_scale,
1947 $out,
1948 m,
1949 $n,
1950 $k,
1951 stream,
1952 )?
1953 }
1954 Some(wt) if self.w8a16_gemm_t_m128_k.0 != 0 => {
1955 let wt: Fp8WeightTransposed = wt;
1956 ops::w8a16_gemm_n128_m128(
1957 ctx.gpu,
1958 self.w8a16_gemm_t_m128_k,
1959 $in,
1960 wt.weight_t,
1961 wt.scale_t,
1962 $out,
1963 m,
1964 $n,
1965 $k,
1966 stream,
1967 )?
1968 }
1969 _ if self.w8a16_gemm_pipelined_k.0 != 0 => ops::w8a16_gemm_pipelined(
1970 ctx.gpu,
1971 self.w8a16_gemm_pipelined_k,
1972 $in,
1973 $w.weight,
1974 $w.row_scale,
1975 $out,
1976 m,
1977 $n,
1978 $k,
1979 stream,
1980 )?,
1981 _ => ops::w8a16_gemm(
1982 ctx.gpu,
1983 self.w8a16_gemm_k,
1984 $in,
1985 $w.weight,
1986 $w.row_scale,
1987 $out,
1988 m,
1989 $n,
1990 $k,
1991 stream,
1992 )?,
1993 }
1994 };
1995 }
1996 let gate_t: Option<Fp8WeightTransposed> = None;
1997 let up_t: Option<Fp8WeightTransposed> = None;
1998 let down_t: Option<Fp8WeightTransposed> = None;
1999 w8_gemm!(fp8w.gate_proj, gate_t, input, gate_out, inter, h);
2000 w8_gemm!(fp8w.up_proj, up_t, input, up_out, inter, h);
2001 ops::silu_mul(
2002 ctx.gpu,
2003 self.act_mul,
2004 gate_out,
2005 up_out,
2006 gate_out,
2007 m * inter,
2008 stream,
2009 )?;
2010 let output = ctx.buffers.moe_output();
2011 w8_gemm!(fp8w.down_proj, down_t, gate_out, output, h, inter);
2012 return Ok(());
2013 }
2014
2015 // BF16 prefill dispatch. Prefer the tensor-core m16n8k16 MMA kernel
2016 // (`dense_gemm_tc`, 3-5x+ over scalar) — the scalar `dense_gemm_bf16`
2017 // was the flat ~155 tok/s prefill bottleneck on Qwen3.6-27B dense
2018 // NVFP4 (FFN = ~83% of prefill). Falls back to scalar if the TC
2019 // kernel isn't loaded for this target. Decode (gemv, M=1) is a
2020 // separate path, so TPOT is unaffected; BF16 MMA preserves coherence.
2021 if let Some(ref bf16w) = self.bf16_weights {
2022 let tc = self.dense_gemm_tc_k.0 != 0;
2023 // helper: cuBLASLt when enabled (the big win at prefill M), else the
2024 // tensor-core MMA kernel, else scalar. dense_gemm_tc is ~1.4 TFLOP/s
2025 // on the large dense-FFN shapes (e.g. Laguna layer-0 gate/up/down at
2026 // N=12288/3072, K=3072) — nsys measured its 3 launches at ~100 ms
2027 // EACH = 33% of the whole C=1 prefill. cuBLASLt runs the identical
2028 // BF16×BF16→FP32 GEMM at 90+ TFLOP/s (~65× faster), the same path
2029 // q/k/v/o and the head-gate already use. Gated on ATLAS_CUBLAS_GEMM.
2030 macro_rules! ffn_gemm {
2031 ($a:expr, $b:expr, $c:expr, $n:expr, $k:expr) => {
2032 if ctx.dispatch.cublas_gemm {
2033 ops::cublas_bf16_proj_dense($a, $b.weight, $c, m, $n, $k, stream)?;
2034 } else if tc {
2035 ops::dense_gemm_tc(
2036 ctx.gpu,
2037 self.dense_gemm_tc_k,
2038 $a,
2039 $b,
2040 $c,
2041 m,
2042 $n,
2043 $k,
2044 stream,
2045 )?;
2046 } else {
2047 ops::dense_gemm(
2048 ctx.gpu,
2049 self.dense_gemm_bf16_k,
2050 $a,
2051 $b,
2052 $c,
2053 m,
2054 $n,
2055 $k,
2056 stream,
2057 )?;
2058 }
2059 };
2060 }
2061 ffn_gemm!(input, &bf16w.gate_proj, gate_out, inter, h);
2062 ffn_gemm!(input, &bf16w.up_proj, up_out, inter, h);
2063 ops::silu_mul(
2064 ctx.gpu,
2065 self.act_mul,
2066 gate_out,
2067 up_out,
2068 gate_out,
2069 m * inter,
2070 stream,
2071 )?;
2072 let output = ctx.buffers.moe_output();
2073 ffn_gemm!(gate_out, &bf16w.down_proj, output, h, inter);
2074 return Ok(());
2075 }
2076
2077 // Prefill: prefer the 128x128 cp.async-pipelined `w4a16_gemm_t_m128`
2078 // (the kernel attention/SSM use) over the M64xN64 base `w4a16_gemm`
2079 // (~10 TFLOPS, the flat ~155 tok/s bottleneck). That kernel needs the
2080 // TRANSPOSED weight layout, so we use the `*_proj_t` copies built at
2081 // load (decode keeps the non-transposed weights via gemv → TPOT/
2082 // coherence unaffected). Falls back to base when no transposed copy /
2083 // kernel is present.
2084 // LOSSLESS prefill opt-in: when ATLAS_BF16_TC_PREFILL is set AND the
2085 // BF16 128x128 kernel is present, route prefill GEMMs through the
2086 // bit-equivalent BF16 tensor-core path instead of the default FP8-E4M3
2087 // `t_m128`. The FP8 crush is fast but perturbs generation (measured
2088 // length-truncations / accuracy risk on Qwen3.6-27B); the BF16 variant
2089 // keeps the same 128x128 cp.async speed at base-kernel precision.
2090 // Unset (default) → every arm below is byte-for-byte the prior behavior
2091 // (PCND: explicit opt-in, no silent default change). Read once per call.
2092 // Env read only here; the usable gate (`bf16_tc_prefill`) is derived
2093 // below AFTER v1/v2 selection, from the handle actually launched.
2094 // Gating on v1's handle while dispatching v2 admitted launches of a
2095 // kernel this target may not carry.
2096 let bf16_tc_env = std::env::var_os("ATLAS_BF16_TC_PREFILL").is_some();
2097 // FP8 M64 fast-prefill opt-in: route prefill GEMMs through the m16n8k32
2098 // e4m3 M64 kernel (~1.47x vs v2 BF16, smem-relieved). Lossy (cosine 0.9997)
2099 // → highest priority when set, so it overrides the BF16/FP8 t_m128 arms.
2100 // PCND: explicit opt-in, default off = byte-for-byte prior behavior.
2101 let fp8_m64_prefill =
2102 self.w4a16_gemm_t_k.0 != 0 && std::env::var_os("ATLAS_FP8_M64_PREFILL").is_some();
2103 // int8 W4A8 fast-prefill opt-in (ATLAS_INT8_PREFILL): route prefill GEMMs
2104 // through the validated requant→`int8_gemm_faith2` pipeline (cosine
2105 // 0.999978 vs the host full-precision dequant GEMM). HIGHEST priority when
2106 // set, so it overrides every other prefill arm. Needs both operands int8:
2107 // the NVFP4 weights are requanted to int8 once (cached, see
2108 // `ensure_int8_weight`) and the BF16 activations are requanted every call
2109 // into the shared scratch (`ensure_int8_scratch`). LOSSY (perf gate, not
2110 // bit-identical) — the _2.5h IoU gate is the final arbiter.
2111 // PCND: explicit opt-in, default off = byte-for-byte prior behavior; the
2112 // arm is a no-op (and no buffers are built) unless the kernels are loaded.
2113 let int8_prefill =
2114 self.int8_faith2_k.0 != 0 && std::env::var_os("ATLAS_INT8_PREFILL").is_some();
2115 if int8_prefill {
2116 // Log-once latch (see `atlas_core::scope`). It holds no model-derived
2117 // value — the message is rebuilt from the arguments every call — so a
2118 // stale entry cannot produce a wrong answer, only a suppressed duplicate
2119 // line after a model swap. Scoping it would thread a logging concern
2120 // through the call path to prevent one repeated INFO line.
2121 if ctx.stats.once("log:ffn_int8_prefill") {
2122 tracing::info!(
2123 "[atlas] ATLAS_INT8_PREFILL=1: dense-FFN prefill via int8_gemm_faith2 (W4A8 requant→int8 MMA, lossy ~0.99998 cosine)"
2124 );
2125 }
2126 }
2127 // NVFP4 W4A4 MMQ prefill (ATLAS_FFN_NVFP4_MMQ) — vendored llama Blackwell
2128 // block-scale FP4 MMA, gate/up ONLY (hybrid: down stays on the default t_m128
2129 // path — SiLU(gate)*up is heavy-tailed and accuracy-critical). SiLU models only
2130 // (the scale2 fold lives in the scaled SiLU-mul). Mutually exclusive with
2131 // ATLAS_FFN_MMQ (both use the shared ffn_act_q8 scratch); this arm wins.
2132 //
2133 // An installed LoRA adapter turns this arm OFF. The MMQ path leaves
2134 // gate_out/up_out holding UNSCALED products and folds each
2135 // projection's `weight_scale_2` later, inside the SiLU-mul kernel. A
2136 // LoRA delta is a true-valued quantity, so adding it to those buffers
2137 // would put it through a scale2 multiply that does not belong to it —
2138 // silently wrong output rather than a failure. Folding scale2 into the
2139 // delta instead would mean reproducing the quant layout's arithmetic
2140 // in the adapter path, which is a much larger commitment than the
2141 // ~8% prefill this arm is worth (measured 831 vs 767 tok/s at 8K).
2142 // Correctness first; making LoRA and MMQ coexist is its own change.
2143 let fp4mmq_prefill = self.nvfp4_mmq_nc_k.0 != 0
2144 && self.nvfp4_quant_act_k.0 != 0
2145 && self.nvfp4_silu_scaled_k.0 != 0
2146 && matches!(self.activation, FfnActivation::SiLU)
2147 && self.lora.is_none()
2148 && std::env::var_os("ATLAS_NO_FFN_NVFP4_MMQ").is_none();
2149 if fp4mmq_prefill {
2150 // Log-once latch (see `atlas_core::scope`). It holds no model-derived
2151 // value — the message is rebuilt from the arguments every call — so a
2152 // stale entry cannot produce a wrong answer, only a suppressed duplicate
2153 // line after a model swap. Scoping it would thread a logging concern
2154 // through the call path to prevent one repeated INFO line.
2155 if ctx.stats.once("log:ffn_fp4_mmq_prefill") {
2156 tracing::info!(
2157 "[atlas] ATLAS_FFN_NVFP4_MMQ=1: dense-FFN gate/up prefill via vendored llama NVFP4 W4A4 MMQ (block-scale FP4 MMA, ~80 TFLOP/s vs t_m128 ~51)"
2158 );
2159 }
2160 }
2161 // Down-projection MMQ arm (DEFAULT ON; kill-switch ATLAS_NO_FFN_NVFP4_MMQ_DOWN=1): route down through
2162 // the same MMQ arm (t_m128 runs the narrow-N down at only ~34 TFLOP/s in-model).
2163 // Accuracy note: down W4A4 cosine 0.9961 (random) — better than the previously
2164 // coherence-validated all-W4A4 config (0.991) — but still the heavy-tailed
2165 // projection, so it stays a SEPARATE opt-in gate.
2166 let fp4mmq_down = fp4mmq_prefill
2167 && self.nvfp4_scale_k.0 != 0
2168 && std::env::var_os("ATLAS_NO_FFN_NVFP4_MMQ_DOWN").is_none();
2169 // HYBRID: route the accuracy-critical down_proj OFF Q4_K onto the near-lossless faith2
2170 // NVFP4 path (W4A8 requant, cos 0.99998). down=SiLU(gate)*up is heavy-tailed; Q4_K
2171 // superblock scaling clips it (BFCL `multiple` -4.0%; llama promotes only down→Q6_K for
2172 // this reason). gate/up stay on Q4_K. Default ON when MMQ active; ATLAS_FFN_MMQ_DOWN_Q4K=1
2173 // = lossy all-Q4_K (A/B only). Defined here (self-fields+env, no q4k_prefill var dep) so the
2174 // int8 scratch below can size for the hybrid down.
2175 let down_faith2 = self.q4k_mmq_nc_k.0 != 0
2176 && self.q4k_quant_act_k.0 != 0
2177 && self.q4k_quant_w_k.0 != 0
2178 && self.dequant_nvfp4_bf16_k.0 != 0
2179 && self.int8_faith2_k.0 != 0
2180 && self.requant_a_int8_k.0 != 0
2181 && !fp4mmq_prefill
2182 && std::env::var_os("ATLAS_FFN_MMQ").is_some()
2183 && std::env::var_os("ATLAS_FFN_MMQ_DOWN_Q4K").is_none();
2184 // Pre-allocate (or reuse) the activation-requant scratch once per call,
2185 // sized to the largest projection K (= max(h, inter)) so the per-GEMM
2186 // arms never trigger a mid-call grow/sync. NULL when the int8 path is off.
2187 // Shared, arena-owned activation-requant scratch (sized once for
2188 // max_batch_tokens × max(h, inter) in BufferSizes::from_config). Replaces
2189 // the former per-DenseFfnLayer grow-on-demand allocator that leaked
2190 // ~286MB × 64 layers on the MMQ prefill path.
2191 let (int8_a_i8, int8_a_scale) = if int8_prefill || down_faith2 {
2192 (ctx.buffers.ffn_act_a(), ctx.buffers.ffn_act_scale())
2193 } else {
2194 (DevicePtr::NULL, DevicePtr::NULL)
2195 };
2196 // W4A4 native-FP4 prefill (ATLAS_FP4_PREFILL) — HIGHEST priority. NVFP4 weights
2197 // used directly (no requant); BF16 activations quantized to NVFP4 each GEMM into
2198 // the shared scratch. Native FP4 tensor cores (sm_121a). Lossy (cos ~0.99 vs fp32).
2199 let fp4_prefill = self.w4a4_gemm_k.0 != 0
2200 && self.quantize_nvfp4_k.0 != 0
2201 && std::env::var_os("ATLAS_FP4_PREFILL").is_some();
2202 if fp4_prefill {
2203 // Log-once latch (see `atlas_core::scope`). It holds no model-derived
2204 // value — the message is rebuilt from the arguments every call — so a
2205 // stale entry cannot produce a wrong answer, only a suppressed duplicate
2206 // line after a model swap. Scoping it would thread a logging concern
2207 // through the call path to prevent one repeated INFO line.
2208 if ctx.stats.once("log:ffn_fp4_prefill") {
2209 tracing::info!(
2210 "[atlas] ATLAS_FP4_PREFILL=1: dense-FFN prefill via w4a4_gemm (native FP4 MMA sm_121a, W4A4)"
2211 );
2212 }
2213 }
2214 // NVFP4 packed [m,K/2] + scale [m,K/16] both fit within the shared int8
2215 // buffers (a_i8 [m,K] ⊇ packed; a_scale [m,(K/32)*4] ⊇ scale). FP4-prefill
2216 // is a standalone A/B flag, never co-active with the int8/Q4_K down path.
2217 let (nvfp4_a_packed, nvfp4_a_scale) = if fp4_prefill {
2218 (ctx.buffers.ffn_act_a(), ctx.buffers.ffn_act_scale())
2219 } else {
2220 (DevicePtr::NULL, DevicePtr::NULL)
2221 };
2222 // Q4_K MMQ prefill (ATLAS_FFN_MMQ) — vendored llama Q4_K W4A8 GEMM. Highest priority
2223 // when enabled. Lossy (Q4_K weight format ≠ NVFP4); gate via BFCL before relying on it.
2224 let q4k_prefill = self.q4k_mmq_nc_k.0 != 0
2225 && self.q4k_quant_act_k.0 != 0
2226 && self.q4k_quant_w_k.0 != 0
2227 && self.dequant_nvfp4_bf16_k.0 != 0
2228 && !fp4mmq_prefill
2229 && std::env::var_os("ATLAS_FFN_MMQ").is_some();
2230 if q4k_prefill {
2231 // Log-once latch (see `atlas_core::scope`). It holds no model-derived
2232 // value — the message is rebuilt from the arguments every call — so a
2233 // stale entry cannot produce a wrong answer, only a suppressed duplicate
2234 // line after a model swap. Scoping it would thread a logging concern
2235 // through the call path to prevent one repeated INFO line.
2236 if ctx.stats.once("log:ffn_q4k_prefill") {
2237 tracing::info!(
2238 "[atlas] ATLAS_FFN_MMQ=1: dense-FFN prefill via vendored llama Q4_K MMQ (W4A8, +25%/+10% gate·down vs faith2)"
2239 );
2240 }
2241 }
2242 let q4k_a = if q4k_prefill {
2243 ctx.buffers.ffn_act_q8()
2244 } else {
2245 DevicePtr::NULL
2246 };
2247 // FP4-MMQ y scratch: block_fp4_mmq activations, in the SAME shared arena buffer
2248 // (fp4_act_scratch_bytes ≤ q8_1_scratch_bytes; mutually exclusive with q4k_prefill).
2249 let fp4_y = if fp4mmq_prefill {
2250 ctx.buffers.ffn_act_q8()
2251 } else {
2252 DevicePtr::NULL
2253 };
2254 // A/B escape hatch (benchmark only): force the proven v1 BF16 kernel even
2255 // when v2 is loaded, so v1-vs-v2 prefill TTFT can be compared in one
2256 // binary. Default unset → prefer v2 (the faster, bit-identical variant).
2257 let use_v2 = self.w4a16_gemm_t_m128_bf16_v2_k.0 != 0
2258 && std::env::var_os("ATLAS_DISABLE_PREFILL_V2").is_none();
2259 let bf16_kernel = if use_v2 {
2260 self.w4a16_gemm_t_m128_bf16_v2_k
2261 } else {
2262 self.w4a16_gemm_t_m128_bf16_k
2263 };
2264 // Final gate: the flag is honored only when the SELECTED kernel is
2265 // loaded (v2 when preferred, else v1) — not v1's handle unconditionally.
2266 let bf16_tc_prefill = bf16_kernel.0 != 0 && bf16_tc_env;
2267
2268 macro_rules! w4_gemm {
2269 ($w:expr, $wt:expr, $cell:expr, $qcell:expr, $fp4cell:expr, $allow_fp4:expr, $in:expr, $out:expr, $n:expr, $k:expr, $allow_q4k:expr) => {
2270 match $wt {
2271 // NVFP4 W4A4 MMQ prefill (ATLAS_FFN_NVFP4_MMQ) — HIGHEST priority.
2272 // `$allow_fp4` = fp4mmq_prefill for gate/up, fp4mmq_down for down.
2273 // Activation pre-quantized into `fp4_y` by the caller; the output is
2274 // missing ×scale2, folded downstream (scaled SiLU-mul / scale_bf16).
2275 _ if $allow_fp4 => {
2276 let _ = $in;
2277 let qw =
2278 self.ensure_nvfp4_mmq_weight($fp4cell, ctx.gpu, $w, $n, $k, stream)?;
2279 // Size the M tile to the batch when the batch is small and
2280 // the small-tile entries are present. m must be <= mmq_x or
2281 // grid.y>1 re-streams the weights per tile.
2282 let (tk_nc, tk_wc, tile) = if m <= 16
2283 && self.nvfp4_mmq16_nc_k.0 != 0
2284 && mmq_small_tile_enabled()
2285 {
2286 (self.nvfp4_mmq16_nc_k, self.nvfp4_mmq16_wc_k, 16u32)
2287 } else if m <= 32
2288 && self.nvfp4_mmq32_nc_k.0 != 0
2289 && mmq_small_tile_enabled()
2290 {
2291 (self.nvfp4_mmq32_nc_k, self.nvfp4_mmq32_wc_k, 32u32)
2292 } else if m <= 64
2293 && self.nvfp4_mmq64_nc_k.0 != 0
2294 && mmq_small_tile_enabled()
2295 && mmq_tile64_enabled()
2296 {
2297 (self.nvfp4_mmq64_nc_k, self.nvfp4_mmq64_wc_k, 64u32)
2298 } else {
2299 (self.nvfp4_mmq_nc_k, self.nvfp4_mmq_wc_k, 128u32)
2300 };
2301 ops::nvfp4_mmq_gemm_tiled(
2302 ctx.gpu, tk_nc, tk_wc, tile, fp4_y, qw.w, $out, m, $n, $k, stream,
2303 )?;
2304 }
2305 // Q4_K MMQ prefill (ATLAS_FFN_MMQ) — next priority, gated per-GEMM by
2306 // `$allow_q4k` (false for down in the hybrid → falls to the faith2 arm).
2307 // Activation `$in` is pre-quantized to q8_1 in `q4k_a` by the caller.
2308 _ if q4k_prefill && $allow_q4k => {
2309 let qw = self.ensure_q4k_weight($qcell, ctx.gpu, $w, $n, $k, stream)?;
2310 ops::q4k_mmq_gemm(
2311 ctx.gpu,
2312 self.q4k_mmq_nc_k,
2313 self.q4k_mmq_wc_k,
2314 q4k_a,
2315 qw.w_q4k,
2316 $out,
2317 m,
2318 $n,
2319 $k,
2320 stream,
2321 )?;
2322 }
2323 // W4A4 native-FP4 prefill (ATLAS_FP4_PREFILL) — HIGHEST priority.
2324 // The activation is PRE-quantized into the NVFP4 scratch by the caller
2325 // (`input` once for gate+up which share it; `gate_out` for down) — opt #1,
2326 // avoids the redundant re-quant. This arm just runs w4a4_gemm against the
2327 // native NVFP4 weight `$w` (no requant). sm_121a FP4 MMA.
2328 _ if fp4_prefill => {
2329 let _ = $in;
2330 ops::w4a4_gemm(
2331 ctx.gpu,
2332 self.w4a4_gemm_k,
2333 nvfp4_a_packed,
2334 nvfp4_a_scale,
2335 $w,
2336 $out,
2337 m,
2338 $n,
2339 $k,
2340 stream,
2341 )?;
2342 }
2343 // int8 W4A8 fast prefill (ATLAS_INT8_PREFILL) — next priority.
2344 // Independent of `$wt`/the transposed copies: requant reads the
2345 // non-transposed NVFP4 `$w` directly. Builds (once) + caches the
2346 // int8 weight in `$cell`, then requant_a + faith2 via the shared
2347 // scratch. Lossy (cosine ~0.99998). Also the HYBRID down path
2348 // (down_faith2 && !$allow_q4k): down falls here instead of Q4_K.
2349 _ if int8_prefill || (down_faith2 && !$allow_q4k) => {
2350 let iw = self.ensure_int8_weight($cell, ctx.gpu, $w, $n, $k, stream)?;
2351 // faith5 (ATLAS_INT8_FAITH5=1): int32 per-sb accumulation
2352 // breaks the MMA→scale dependency chain. Same kernel signature
2353 // + grid/block as faith2 — just a different KernelHandle.
2354 let int8_kernel = if self.int8_faith5_k.0 != 0
2355 && std::env::var_os("ATLAS_INT8_FAITH5").is_some()
2356 {
2357 self.int8_faith5_k
2358 } else {
2359 self.int8_faith2_k
2360 };
2361 ops::int8_gemm_faith2_prefill(
2362 ctx.gpu,
2363 int8_kernel,
2364 self.requant_a_int8_k,
2365 $in,
2366 iw.w_i8,
2367 iw.w_scale,
2368 int8_a_i8,
2369 int8_a_scale,
2370 $out,
2371 m,
2372 $n,
2373 $k,
2374 stream,
2375 )?;
2376 }
2377 // Lossless opt-in: BF16 128x128 tensor-core prefill (bit-equivalent
2378 // to base `w4a16_gemm`). Preferred over the FP8 t_m128/v2 paths only
2379 // when ATLAS_BF16_TC_PREFILL is set and the kernel is loaded. Within
2380 // the lossless path, prefer the higher-occupancy v2 kernel (3 CTAs/SM,
2381 // bit-identical to v1) when it is loaded; else the proven v1 kernel.
2382 // Both go through the same launch helper (identical grid/block/args).
2383 // FP8 M64 fast prefill (ATLAS_FP8_M64_PREFILL) — highest priority,
2384 // M64 grid via the w4a16_gemm_n128 launcher.
2385 Some(wt) if fp8_m64_prefill => ops::w4a16_gemm_n128(
2386 ctx.gpu,
2387 self.w4a16_gemm_t_k,
2388 $in,
2389 &wt,
2390 $out,
2391 m,
2392 $n,
2393 $k,
2394 stream,
2395 )?,
2396 // v2's COMPILED signature carries a 9th param, `ldb`
2397 // (transposed-B row stride; == N for the FFN twins, which
2398 // are built unpadded). It MUST go through the `_ldb`
2399 // launcher: the 8-arg helper leaves cuLaunchKernel reading
2400 // one-past-the-end of the param array for `ldb` —
2401 // CUDA_ERROR_INVALID_VALUE or a host SIGSEGV depending on
2402 // the neighboring heap word. v1 takes exactly 8 params and
2403 // stays on the 8-arg helper.
2404 Some(wt) if bf16_tc_prefill && use_v2 => ops::w4a16_gemm_n128_m128_bf16_ldb(
2405 ctx.gpu,
2406 bf16_kernel,
2407 $in,
2408 &wt,
2409 $out,
2410 m,
2411 $n,
2412 $k,
2413 $n,
2414 stream,
2415 )?,
2416 Some(wt) if bf16_tc_prefill => ops::w4a16_gemm_n128_m128_bf16(
2417 ctx.gpu,
2418 bf16_kernel,
2419 $in,
2420 &wt,
2421 $out,
2422 m,
2423 $n,
2424 $k,
2425 stream,
2426 )?,
2427 // Small-M routing (DFlash verify, M<=64): delegate to
2428 // `w4a16_prefill_gemm`, which picks `w4a16_gemm_t` /
2429 // `w4a16_gemm_t_k64` per the w4a16_m17_bench numbers and
2430 // falls back to the same v2/m128 kernels below.
2431 // ATLAS_FFN_SMALLM=0 disables. Sits after the opt-in
2432 // quant arms so explicit MMQ/int8/FP8 experiments keep
2433 // priority.
2434 Some(wt) if m <= 64 => {
2435 self.w4a16_prefill_gemm(ctx, $w, Some(&wt), $in, $out, m, $n, $k, stream)?
2436 }
2437 // Prefer v2 (8-warp) > t_m128 (4-warp) > scalar-tile base.
2438 Some(wt) if self.w4a16_gemm_t_m128_v2_k.0 != 0 => ops::w4a16_gemm_n128_m128_v2(
2439 ctx.gpu,
2440 self.w4a16_gemm_t_m128_v2_k,
2441 $in,
2442 &wt,
2443 $out,
2444 m,
2445 $n,
2446 $k,
2447 stream,
2448 )?,
2449 Some(wt) if self.w4a16_gemm_t_m128_k.0 != 0 => ops::w4a16_gemm_n128_m128(
2450 ctx.gpu,
2451 self.w4a16_gemm_t_m128_k,
2452 $in,
2453 &wt,
2454 $out,
2455 m,
2456 $n,
2457 $k,
2458 stream,
2459 )?,
2460 _ => {
2461 ops::w4a16_gemm(ctx.gpu, self.w4a16_gemm, $in, $w, $out, m, $n, $k, stream)?
2462 }
2463 }
2464 };
2465 }
2466
2467 // W4A4 opt #1: quantize the gate/up SHARED input `[M, H]` to NVFP4 ONCE
2468 // (gate and up both read it) instead of per-GEMM. Reused by both arms below.
2469 if fp4_prefill {
2470 ops::quantize_bf16_to_nvfp4(
2471 ctx.gpu,
2472 self.quantize_nvfp4_k,
2473 input,
2474 nvfp4_a_packed,
2475 nvfp4_a_scale,
2476 m,
2477 h,
2478 stream,
2479 )?;
2480 }
2481 // Q4_K opt: quantize the gate/up SHARED input `[M, H]` to q8_1 ONCE (both read it).
2482 if q4k_prefill {
2483 ops::quantize_act_q8_1(ctx.gpu, self.q4k_quant_act_k, input, q4k_a, m, h, stream)?;
2484 }
2485 // FP4-MMQ: quantize the gate/up SHARED input `[M, H]` to block_fp4_mmq ONCE.
2486 if fp4mmq_prefill {
2487 ops::nvfp4_mmq_quantize_act(
2488 ctx.gpu,
2489 self.nvfp4_quant_act_k,
2490 input,
2491 fp4_y,
2492 m,
2493 h,
2494 stream,
2495 )?;
2496 }
2497 // Per-projection timers. `dense_total` localised 23.7 s of a 28.2 s
2498 // Gemma-4-31B prefill to this function; these say WHICH of the three
2499 // projections it is. Roofline for that shape: 3 x 5376 x 21504 x 4012 tok
2500 // x 60 layers = 167 TFLOP, so 23.7 s is ~7 TFLOP/s against a bf16
2501 // tensor-core peak two orders higher — a hypothesis these numbers test
2502 // rather than assume.
2503 // ★ PER-STEP, NOT CUMULATIVE. The first version of this timer measured
2504 // elapsed-since-one-start at each of the three call sites, so `up_proj`
2505 // included `gate_proj` and `down_proj` included both — and the summed
2506 // "total profiled" then exceeded the wall clock, which is the tell.
2507 macro_rules! ffn_step {
2508 ($label:expr, $t0:expr) => {
2509 if ctx.profile {
2510 ctx.gpu.synchronize(stream)?;
2511 tracing::info!(
2512 " FFN prefill [{}] N={}: {}µs",
2513 $label,
2514 num_tokens,
2515 $t0.elapsed().as_micros()
2516 );
2517 #[allow(unused_assignments)]
2518 {
2519 $t0 = std::time::Instant::now();
2520 }
2521 }
2522 };
2523 }
2524 #[allow(unused_mut, unused_assignments)]
2525 let mut t_ffn = std::time::Instant::now();
2526 // gate_proj GEMM: [M, H] → [M, inter]
2527 w4_gemm!(
2528 &self.weights.gate_proj,
2529 self.weights.gate_proj_t,
2530 &self.int8_gate,
2531 &self.q4k_gate,
2532 &self.fp4mmq_gate,
2533 fp4mmq_prefill,
2534 input,
2535 gate_out,
2536 inter,
2537 h,
2538 true
2539 );
2540 ffn_step!("gate_proj", t_ffn);
2541 // up_proj GEMM: [M, H] → [M, inter]
2542 w4_gemm!(
2543 &self.weights.up_proj,
2544 self.weights.up_proj_t,
2545 &self.int8_up,
2546 &self.q4k_up,
2547 &self.fp4mmq_up,
2548 fp4mmq_prefill,
2549 input,
2550 up_out,
2551 inter,
2552 h,
2553 true
2554 );
2555 ffn_step!("up_proj", t_ffn);
2556
2557 // LoRA gate/up deltas land here: the projections are complete and, with
2558 // the MMQ arm disabled above, gate_out/up_out hold true-valued BF16 —
2559 // so the delta adds in the same units it was trained in.
2560 self.apply_lora_gate_up(ctx, input, gate_out, up_out, m, stream)?;
2561 // activation(gate) * up for all M tokens (SiLU or GELU)
2562 let fused_down_quant = fp4mmq_down && self.nvfp4_silu_quant_k.0 != 0;
2563 if fused_down_quant {
2564 // Fused SiLU-mul + quantize straight into the down MMQ's y-format: the
2565 // [M, inter] bf16 intermediate is never written or re-read (that round-trip
2566 // is why the unfused down arm measured neutral). scale2 folds happen inside,
2567 // pre-clamp — identical math to the two-step path below.
2568 ops::nvfp4_silu_mul_quant(
2569 ctx.gpu,
2570 self.nvfp4_silu_quant_k,
2571 gate_out,
2572 up_out,
2573 fp4_y,
2574 self.weights.gate_proj.weight_scale_2,
2575 self.weights.up_proj.weight_scale_2,
2576 m,
2577 inter,
2578 stream,
2579 )?;
2580 } else if fp4mmq_prefill {
2581 // FP4-MMQ outputs are missing the per-tensor FP32 scale2 (the hardware MMA
2582 // applies only the per-16 e4m3 scales) — fold it here, before the nonlinearity.
2583 ops::nvfp4_silu_mul_scaled(
2584 ctx.gpu,
2585 self.nvfp4_silu_scaled_k,
2586 gate_out,
2587 up_out,
2588 gate_out,
2589 self.weights.gate_proj.weight_scale_2,
2590 self.weights.up_proj.weight_scale_2,
2591 m * inter,
2592 stream,
2593 )?;
2594 } else {
2595 ops::silu_mul(
2596 ctx.gpu,
2597 self.act_mul,
2598 gate_out,
2599 up_out,
2600 gate_out,
2601 m * inter,
2602 stream,
2603 )?;
2604 }
2605
2606 // W4A4 opt #1: quantize the down input (SiLU(gate)*up, `[M, inter]`) to NVFP4.
2607 if fp4_prefill {
2608 ops::quantize_bf16_to_nvfp4(
2609 ctx.gpu,
2610 self.quantize_nvfp4_k,
2611 gate_out,
2612 nvfp4_a_packed,
2613 nvfp4_a_scale,
2614 m,
2615 inter,
2616 stream,
2617 )?;
2618 }
2619 // Q4_K opt: quantize the down input (SiLU(gate)*up, `[M, inter]`) to q8_1.
2620 // Skip when the hybrid routes down to faith2 (it does its own int8 requant).
2621 if q4k_prefill && !down_faith2 {
2622 ops::quantize_act_q8_1(
2623 ctx.gpu,
2624 self.q4k_quant_act_k,
2625 gate_out,
2626 q4k_a,
2627 m,
2628 inter,
2629 stream,
2630 )?;
2631 }
2632 // FP4-MMQ down (two-step fallback, only when the fused kernel is absent):
2633 // quantize the down input (SiLU(gate)*up, `[M, inter]`) to block_fp4_mmq.
2634 if fp4mmq_down && !fused_down_quant {
2635 ops::nvfp4_mmq_quantize_act(
2636 ctx.gpu,
2637 self.nvfp4_quant_act_k,
2638 gate_out,
2639 fp4_y,
2640 m,
2641 inter,
2642 stream,
2643 )?;
2644 }
2645 // down_proj GEMM: [M, inter] → [M, H]
2646 // ($fp4cell is a placeholder — the FP4-MMQ arm is gated off by `false` below;
2647 // down stays on the default path in the FP4-MMQ hybrid.)
2648 let output = ctx.buffers.moe_output();
2649 w4_gemm!(
2650 &self.weights.down_proj,
2651 self.weights.down_proj_t,
2652 &self.int8_down,
2653 &self.q4k_down,
2654 &self.fp4mmq_down,
2655 fp4mmq_down,
2656 gate_out,
2657 output,
2658 h,
2659 inter,
2660 false
2661 );
2662 ffn_step!("down_proj", t_ffn);
2663 // FP4-MMQ down: fold the down-projection's per-tensor scale2 (no SiLU-mul here;
2664 // the consumer is the residual add).
2665 if fp4mmq_down {
2666 ops::nvfp4_scale_bf16(
2667 ctx.gpu,
2668 self.nvfp4_scale_k,
2669 output,
2670 self.weights.down_proj.weight_scale_2,
2671 m * h,
2672 stream,
2673 )?;
2674 }
2675 // AFTER the scale2 fold, not before: that fold scales the base
2676 // projection's output, and the delta is not part of that product.
2677 // `gate_out` holds silu(gate)*up — the activation the base down GEMM
2678 // just contracted over — because the fused silu+quant arm is off
2679 // whenever an adapter is installed.
2680 self.apply_lora_down(ctx, gate_out, output, m, stream)?;
2681
2682 Ok(())
2683 }
2684
2685 /// Batched forward (per-token loop). Used by forward_batched in model loop.
2686 pub fn forward_batched(
2687 &self,
2688 input: DevicePtr,
2689 num_tokens: usize,
2690 ctx: &ForwardContext,
2691 stream: u64,
2692 ) -> Result<()> {
2693 self.forward_prefill(input, num_tokens, ctx, stream)
2694 }
2695}
2696
2697/// Native BF16/FP8 overlays take precedence over any NVFP4 fallback weights.
2698/// Small batches must use the same format-aware dispatcher as prefill.
2699fn native_small_batch_uses_prefill(has_bf16: bool, has_fp8: bool) -> bool {
2700 has_bf16 || has_fp8
2701}
2702
2703#[cfg(test)]
2704#[path = "dense_ffn_mmq_tests.rs"]
2705mod mmq_tests;
2706
2707#[cfg(test)]
2708#[path = "dense_ffn_native_batch_tests.rs"]
2709mod native_batch_tests;
2710
2711#[cfg(test)]
2712#[path = "dense_ffn_kernel_tests.rs"]
2713mod kernel_tests;
2714
2715#[cfg(test)]
2716mod tests {
2717 use super::native_small_batch_uses_prefill;
2718
2719 #[test]
2720 fn native_weight_presence_requires_prefill_dispatch() {
2721 assert!(native_small_batch_uses_prefill(true, false));
2722 assert!(native_small_batch_uses_prefill(false, true));
2723 assert!(native_small_batch_uses_prefill(true, true));
2724 assert!(!native_small_batch_uses_prefill(false, false));
2725 }
2726}