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