memra_engine/fp8_ffi.rs
1//! FP8-ACT PREFILL (MEMRA_PP_FP8=1): cuBLASLt FP8-E4M3 TN GEMM for the F8-E4M3-origin projections.
2//!
3//! Probe verdict 2026-07-08 (probe/fp8_lt_prefill.cu, JSONL row in research/tune-data): cuBLASLt
4//! FP8 GEMM runs 620-795 TF at the 27B prefill shapes vs 47-72 TF for the qmatvec_gemm_q8_0 class
5//! those weights ride today (46.5% of pp GPU time) — projected ~1.85x pp from the F8-native
6//! layers alone. The weight side is EXACT: the checkpoint's raw e4m3 bytes + per-tensor f32
7//! weight_scale are stashed at load next to the Q8_0 re-encode (`GpuTensor::Quant { fp8 }`,
8//! following the `cutlass` optional-operand precedent). The only new rounding vs today is the
9//! ACTIVATION: f32 -> e4m3 with ONE per-batch scalar scale (amax/448) instead of q8_1's per-32
10//! int8 — finer mantissa lost, coarser scale granularity; the run-gen argmax gate arbitrates.
11//!
12//! Dispatch: `matmul`/`matmul_pre` m>=16 arms ONLY (prefill). Decode (m<16) keeps the Q8_0
13//! dp4a/MMVQ chain bit-for-bit — the spec-exactness law is untouched, and the m=K+1 verify tier
14//! (m<=9) never reaches this path.
15//!
16//! All device work (amax reduce, scale finalize, e4m3 quantize, cublasLtMatmul) runs on the one
17//! `gpu.stream` inside a single C-ABI call (cu/fp8_prefill.cu) — no host sync anywhere: the act
18//! scale is folded with weight_scale into a device scalar fed to the GEMM's B_SCALE_POINTER
19//! (per-token OUTER_VEC B-scales are NOT supported on sm_120 — probed; scalar scales verified
20//! exact there).
21
22use cudarc::driver::{CudaSlice, DevicePtr, DevicePtrMut};
23
24unsafe extern "C" {
25 /// One FP8 prefill GEMM: quantize act f32->e4m3 (per-batch scalar) + cublasLtMatmul TN.
26 /// Returns 0 on success (see cu/fp8_prefill.cu for the error-code bands).
27 fn memra_fp8_pp_gemm(
28 w_e4m3: *const core::ffi::c_void,
29 x_f32: *const f32,
30 xq_e4m3: *mut core::ffi::c_void,
31 scales: *mut f32,
32 y_f32: *mut f32,
33 m: i32,
34 n: i32,
35 k: i32,
36 w_scale: f32,
37 ws: *mut core::ffi::c_void,
38 ws_bytes: usize,
39 stream: *mut core::ffi::c_void,
40 ) -> i32;
41}
42
43/// `MEMRA_PP_FP8=1` gate (default OFF), read once. Gates BOTH the loader stash (model.rs) and the
44/// prefill dispatch — unset means zero VRAM / zero dispatch change.
45pub fn pp_fp8_enabled() -> bool {
46 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
47 *ON.get_or_init(|| {
48 std::env::var("MEMRA_PP_FP8")
49 .map(|v| v == "1")
50 .unwrap_or(false)
51 })
52}
53
54/// F8-E4M3-origin safetensors projections load as RAW e4m3 (QT_F8_E4M3) instead of the Q8_0
55/// re-encode. NEW NUMERIC CONFIG: decode reads the checkpoint's own e4m3 precision (the Q8_0
56/// re-encode was a lossy extra hop) via qmatvec_e4m3_mmvq; prefill (m>=16) rides the cuBLASLt FP8
57/// GEMM on the SAME resident bytes — one weight copy total (frees the ~GBs the MEMRA_PP_FP8 stash
58/// duplicated, no budget cap needed). Superset relationship: with this on, MEMRA_PP_FP8 and its
59/// budget are irrelevant for F8-origin tensors (they never surface as Q8_0, so the stash arm never
60/// fires).
61///
62/// DEFAULT ON since lane/fp8-decode-v1 (2026-08-05); `MEMRA_ST_E4M3=0` is the rollback seam back to
63/// the Q8_0 slab. Flipped on the 27B FP8-ST receipts in `research/fp8dec-20260805/`: decode +2.58pp
64/// with non-overlapping distributions (N=5 interleaved, one binary), 430 MiB freed at a measured
65/// byte ratio of exactly 1.06250 (= theory, so single residency and no duplicate copy), teacher-
66/// forced exactness 2/128 near-tie flips with LOWER NLL on the reference's own tape than the slab
67/// arm scores on it, kernel-check ALL GREEN, run-spec K=1..8 8/8 PASS, serve-st-gate 0 failed.
68///
69/// SCOPE — the flip only reaches the per-tensor scalar-scale class. `find_fp8_native` returns
70/// `blk: Some(grid)` for the block-128 class and `None` for per-row, and the resident arm in
71/// model.rs additionally requires `blk.is_none()`, so both of those classes still take the Q8_0
72/// re-encode. Nothing here changes GGUF: `TensorSource::find_fp8_native` is None for GGUF sources.
73pub fn st_e4m3_enabled() -> bool {
74 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
75 *ON.get_or_init(|| std::env::var("MEMRA_ST_E4M3").as_deref() != Ok("0"))
76}
77
78/// Native residency for the BLOCK-128 e4m3 scale class (`QT_F8_E4M3_BLK`, lane/fp8-blk128-decode
79/// 2026-08-05): the same mechanism as the per-tensor class, behind the same `MEMRA_ST_E4M3=0`
80/// rollback seam.
81pub fn st_e4m3_blk_enabled() -> bool {
82 st_e4m3_enabled()
83}
84
85/// A block-128 tensor that PASSED every shape precondition for native residency but carried e4m3
86/// NaN codes, so it fell through to the Q8_0 floor. Counted because "0 tensors resident as
87/// F8_E4M3_BLK" is otherwise ambiguous between "not a block-128 checkpoint", "env off", and "the
88/// bytes were ineligible" — three facts demanding three different responses.
89static BLK_NATIVE_NAN_REFUSED: std::sync::atomic::AtomicUsize =
90 std::sync::atomic::AtomicUsize::new(0);
91
92pub fn note_blk_native_nan_refused() {
93 BLK_NATIVE_NAN_REFUSED.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
94}
95
96/// Tensors declined by the native block-128 arm's NaN precondition this process.
97pub fn blk_native_nan_refused() -> usize {
98 BLK_NATIVE_NAN_REFUSED.load(std::sync::atomic::Ordering::Relaxed)
99}
100
101/// Resident scratch for the FP8 prefill GEMM (mirrors `CutlassScratch`): the quantized activation
102/// (grown to the largest m*k seen), the 4-float scale block ([0]=amax, [1]=quant mul, [2]=folded
103/// B_SCALE — the GEMM desc holds a POINTER to slot 2, so the buffer must be resident/stable), and
104/// the cuBLASLt workspace (64MB, the probe's size). Single GPU worker => no concurrent use; the
105/// Mutex guards lazy build/grow only (matches moe_cache / cutlass_scratch).
106pub struct Fp8Scratch {
107 pub xq: CudaSlice<u8>,
108 pub scales: CudaSlice<f32>,
109 pub ws: CudaSlice<u8>,
110 cap_xq: usize,
111}
112
113/// cuBLASLt workspace size — same 64MB the probe ran its heuristics with.
114const FP8_WS_BYTES: usize = 64 << 20;
115
116impl crate::Engine {
117 /// FP8 prefill GEMM for a weight carrying the fp8 operand: y[m,out] = x[m,in] @ (e4m3 W)^T
118 /// with the per-batch act scale and per-tensor weight_scale folded in-GEMM. Returns None when
119 /// the env is off or the weight has no fp8 operand (caller falls through to the Q8_0 path).
120 pub fn try_fp8_gemm(
121 &self,
122 w: &crate::model::GpuTensor,
123 x: &CudaSlice<f32>,
124 m: usize,
125 ) -> Result<Option<CudaSlice<f32>>, Box<dyn std::error::Error>> {
126 use crate::model::GpuTensor;
127 // H100 (sm_90) is the first-class FP8 arch: cuBLASLt e4m3 GEMM is native there,
128 // so the Hopper-MMA lane re-admits this path (Phase A5, ARCHITECTURE-H100.md).
129 if crate::portable_mma_gated() {
130 return Ok(None);
131 }
132 // Two e4m3 operand sources, one GEMM:
133 // * QT_F8_E4M3 (MEMRA_ST_E4M3): the RESIDENT decode bytes ARE the raw checkpoint e4m3 —
134 // prefill rides them directly (one copy, no budget). Unconditional: this dtype has no
135 // other prefill GEMM class, so the FP8 path is inherent to the config, not a flag.
136 // * fp8 stash (MEMRA_PP_FP8=1): the Q8_0-decode config's optional duplicate operand.
137 // Block-128 stash operands (blk: Some, Qwen official FP8) are SKIPPED: this GEMM
138 // feeds ONE folded scalar via B_SCALE_POINTER; a block grid through it would apply
139 // scale 1.0 to every tile. The block-scaled GEMM is P1 (probe/fp8_lt_blk_probe.cu
140 // arbitrates cuBLASLt BLK128x128 vs a scale-fold pre-pass on sm_120).
141 let (w_bytes, w_scale, ne) = match w {
142 GpuTensor::Quant {
143 qtype,
144 bytes,
145 scale,
146 ne,
147 ..
148 } if *qtype == crate::QT_F8_E4M3 => (bytes, *scale, ne),
149 GpuTensor::Quant {
150 fp8: Some(f8), ne, ..
151 } if pp_fp8_enabled() && f8.blk.is_none() => (&f8.bytes, f8.scale, ne),
152 _ => return Ok(None),
153 };
154 let (in_f, out_f) = (ne[0] as usize, ne[1] as usize);
155
156 // lazy build / grow the resident scratch to this m*k
157 let need_xq = m * in_f;
158 let mut guard = self.fp8_scratch.lock().unwrap();
159 if guard.is_none() {
160 *guard = Some(Fp8Scratch {
161 xq: self.alloc_u8_uninit(need_xq)?,
162 scales: self.alloc_uninit::<f32>(4)?,
163 ws: self.alloc_u8_uninit(FP8_WS_BYTES)?,
164 cap_xq: need_xq,
165 });
166 }
167 let s = guard.as_mut().unwrap();
168 if need_xq > s.cap_xq {
169 s.xq = self.alloc_u8_uninit(need_xq)?;
170 s.cap_xq = need_xq;
171 }
172
173 let mut y = self.uninit(m * out_f)?; // full-overwrite GEMM output: skip memset
174 let rc = {
175 let stream = self.gpu.stream();
176 // Hold every SyncOnDrop guard across the FFI call (same pattern as cutlass_ffi);
177 // the block scope drops them before `y` is returned.
178 let (w_p, _gw) = w_bytes.device_ptr(&stream);
179 let (x_p, _gx) = x.device_ptr(&stream);
180 let (q_p, _gq) = s.xq.device_ptr_mut(&stream);
181 let (sc_p, _gs) = s.scales.device_ptr_mut(&stream);
182 let (y_p, _gy) = y.device_ptr_mut(&stream);
183 let (ws_p, _gws) = s.ws.device_ptr_mut(&stream);
184 unsafe {
185 memra_fp8_pp_gemm(
186 w_p as *const core::ffi::c_void,
187 x_p as *const f32,
188 q_p as *mut core::ffi::c_void,
189 sc_p as *mut f32,
190 y_p as *mut f32,
191 m as i32,
192 out_f as i32,
193 in_f as i32,
194 w_scale,
195 ws_p as *mut core::ffi::c_void,
196 FP8_WS_BYTES,
197 stream.cu_stream() as *mut core::ffi::c_void,
198 )
199 }
200 };
201 if rc != 0 {
202 return Err(format!(
203 "memra_fp8_pp_gemm rc={rc} (m={m} n={out_f} k={in_f}; 1xxxx=cudaError quant chain, \
204 2xxxx=no cublasLt algo, 3xxxx=matmul status)"
205 )
206 .into());
207 }
208 Ok(Some(y))
209 }
210}
211
212// ============================================================================================
213// P1 option (b) — PER-BLOCK FP8 MMQ prefill (cu/mmq_fp8_blk.cu, lane/fp8-mmq)
214// ============================================================================================
215
216/// `MEMRA_FP8_MMQ=1` gate for the per-block MMQ tile's **STASH** operand source (default OFF;
217/// lane/fp8-mmq 2026-08-04): a SECOND e4m3 copy uploaded next to an already-resident Q8_0 slab,
218/// spending from `MEMRA_PP_FP8_BUDGET_MB`.
219///
220/// This is the third and only exact-AND-fast option from P1-VERDICT.md. cuBLASLt cannot take the
221/// grid at all on sm_120; ARM A's per-tensor fold is fast but diverges at greedy pos 20; ARM B' is
222/// exact but lands on the Q8_0 MMQ. This arm consumes the checkpoint's e4m3 bytes and the
223/// per-[128x128] f32 grid directly, with no re-quantization on either operand's weight side.
224///
225/// STAYS DEFAULT OFF for the stash source, and the reason is the v2 verdict, not inertia: against a
226/// floor whose Q8_0 slab is ALREADY RESIDENT the tile is 0.85-1.09x GEMM-only, so paying a full
227/// duplicate weight copy to reach it is not a win (`lane/fp8-mmq-v2` LANE-VERDICT.jsonl). This
228/// function is ALSO what admits the stash at load (`model.rs`), so it must stay an explicit opt-in:
229/// the native-resident flip below must not silently start duplicating Q8_0 tensors.
230pub fn fp8_mmq_enabled() -> bool {
231 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
232 *ON.get_or_init(|| {
233 std::env::var("MEMRA_FP8_MMQ")
234 .map(|v| v == "1")
235 .unwrap_or(false)
236 })
237}
238
239/// Same tile, **NATIVE-RESIDENT** operand source: a `QT_F8_E4M3_BLK` tensor's own `blk` grid, the
240/// checkpoint's single copy with no slab and no stash. DEFAULT ON since lane/fp8-blk128-decode
241/// (2026-08-05); `MEMRA_FP8_MMQ=0` is the narrow seam back to dequant-per-call.
242///
243/// WHY THE SAME TILE DEFAULTS DIFFERENTLY BY SOURCE — the denominator differs, so the sign does.
244/// With a stash the floor already has its Q8_0 slab resident and the comparison is tile vs tile
245/// (v2: 0.85-1.09x, i.e. not worth a duplicate copy). On the native-resident class the floor must
246/// also CREATE that slab on every prefill call — 27.9 ms/pass of dequant after the vector rewrite,
247/// 14.19 GB of extra weight traffic — so the tile only has to not be 27.9 ms worse than a kernel it
248/// trails by at most ~15% on a subset of shapes. Measured 3-arm interleaved on the 27B block-128
249/// checkpoint (research/fp8blk-20260805/, N=3, one lock hold, one md5-pinned binary):
250/// slab 1540.5 / dequant-per-call 1449.1 / **this tile 1553.3** tok/s, min(C) 1552.4 > max(A) 1541.1
251/// (non-overlapping) = +0.83% pp512 AHEAD of the Q8_0 floor instead of -5.8% behind it.
252///
253/// EXACTNESS, the condition the flip was deferred on (6b741068: "the default flip waits on this
254/// arm's own exactness cells ... rather than inheriting the dequant arm's"). Branch-(b): per-block
255/// f8f6f4 MMA is not the Q8_0 re-encode's arithmetic, so bit-identity is the wrong bar. Measured on
256/// `prime_cache` — the class that actually dispatches this kernel — with a dispatch ledger on every
257/// arm (624 = 208 projections x 3 passes, full coverage) and an A==B bit-identical control proving
258/// the instrument can see zero where zero is: argmax UNCHANGED and the top-10 order identical to the
259/// floor's, rms_rel 2.5e-2 on an rms-2.7155 logit vector, and teacher-forced NLL on the prompt's own
260/// continuation LOWER than the floor's (2.764722 vs 2.787267) on a tape neither arm produced.
261/// `MEMRA_ST_E4M3=0` also disables this route, by removing the native operand
262/// it consumes — this seam exists for the narrower question (keep native decode residency, revert
263/// only the prefill route).
264fn fp8_blk_mmq_native_policy(value: Option<&str>, sm100_dry_build: bool) -> bool {
265 if sm100_dry_build {
266 value == Some("1")
267 } else {
268 value != Some("0")
269 }
270}
271
272pub fn fp8_blk_mmq_native_enabled() -> bool {
273 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
274 *ON.get_or_init(|| {
275 // Dry-built B200 tcgen05 path: explicit ON until real-silicon exactness/serve receipts.
276 fp8_blk_mmq_native_policy(
277 std::env::var("MEMRA_FP8_MMQ").ok().as_deref(),
278 cfg!(memra_sm100_tcgen05),
279 )
280 })
281}
282
283#[cfg(test)]
284mod b200_dry_policy_tests {
285 use super::fp8_blk_mmq_native_policy;
286
287 #[test]
288 fn sm100_dry_build_requires_literal_one() {
289 assert!(!fp8_blk_mmq_native_policy(None, true));
290 assert!(!fp8_blk_mmq_native_policy(Some("0"), true));
291 assert!(fp8_blk_mmq_native_policy(Some("1"), true));
292 assert!(!fp8_blk_mmq_native_policy(Some("yes"), true));
293 }
294
295 #[test]
296 fn qualified_arches_keep_the_existing_default_on_policy() {
297 assert!(fp8_blk_mmq_native_policy(None, false));
298 assert!(!fp8_blk_mmq_native_policy(Some("0"), false));
299 assert!(fp8_blk_mmq_native_policy(Some("1"), false));
300 assert!(fp8_blk_mmq_native_policy(Some("yes"), false));
301 }
302
303 #[test]
304 fn rust_cfg_tracks_the_baked_cuda_arch() {
305 assert_eq!(
306 cfg!(memra_sm100_tcgen05),
307 env!("MEMRA_BUILT_CUDA_ARCH") == "100a"
308 );
309 }
310}
311
312/// Per-tensor e4m3-NaN verdicts, keyed by the weight's device pointer. The hardware MMA reads
313/// magnitude 0x7F as NaN while the host / ARM B' reference decodes it to 0.0, so a tensor
314/// containing any must NOT ride this kernel. The scan is a full pass over the weight, so it runs
315/// ONCE per tensor (first prefill dispatch) and the verdict is cached — never per-GEMM.
316static FP8_MMQ_NAN_OK: std::sync::Mutex<Option<std::collections::HashMap<u64, bool>>> =
317 std::sync::Mutex::new(None);
318
319impl crate::Engine {
320 /// PER-BLOCK FP8 MMQ prefill GEMM for a weight carrying a block-128 fp8 operand:
321 /// y[m,out] = x[m,in] @ (e4m3 W)^T with each [128x128] weight block scaled by its own f32.
322 /// Returns None when the env is off, the weight has no block-128 fp8 operand, the shape is
323 /// unsupported, or the NaN precondition fails (caller falls through to the Q8_0 floor).
324 pub fn try_fp8_blk_mmq(
325 &self,
326 w: &crate::model::GpuTensor,
327 x: &CudaSlice<f32>,
328 m: usize,
329 ) -> Result<Option<CudaSlice<f32>>, Box<dyn std::error::Error>> {
330 use crate::model::GpuTensor;
331 // Entry counter BEFORE the env gate: a ledger of all zeros is otherwise ambiguous between
332 // "the flag was not seen" and "no prefill GEMM ever reached this hook".
333 FP8_MMQ_ENTRIES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
334 if crate::portable_mma_gated() {
335 FP8_MMQ_GATE_OFF.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
336 return Ok(None);
337 }
338 // TWO OPERAND SOURCES, in this order, EACH WITH ITS OWN DEFAULT (2026-08-05):
339 //
340 // (1) the `fp8` STASH — a SECOND e4m3 copy uploaded alongside a resident Q8_0 slab under
341 // `MEMRA_PP_FP8` / `MEMRA_PP_FP8_BUDGET_MB`. This is what the v1/v2 MMQ lanes measured,
342 // and it stays behind `MEMRA_FP8_MMQ=1` (`fp8_mmq_enabled`): against a floor whose slab
343 // is already resident the tile is 0.85-1.09x, which does not pay for a duplicate copy.
344 //
345 // (2) the `blk` RESIDENCY field on a `QT_F8_E4M3_BLK` tensor (lane/fp8-blk128-decode) —
346 // the checkpoint-native single copy, no slab and no stash. Same bytes, same grid, same
347 // layout contract, so the kernel cannot tell them apart; only the owner differs. This
348 // source is DEFAULT ON (`fp8_blk_mmq_native_enabled`), because its floor must build the
349 // Q8_0 slab every call (27.9 ms/pass) and the tile measured +0.83% pp512 ahead of it
350 // with non-overlapping distributions.
351 //
352 // The gate is therefore checked PER SOURCE, after the operand is known — not once up front.
353 // Checking it before the match would make the native flip also flip the stash, and
354 // `fp8_mmq_enabled` is what admits that stash at LOAD time (model.rs), so a shared gate
355 // would silently start duplicating every Q8_0 tensor with an fp8 sibling.
356 //
357 // Why (1) first: when a stash exists the tensor is ALSO a Q8_0 slab, and the stash is the
358 // operand that arm's budget accounting owns. A `QT_F8_E4M3_BLK` tensor never has a stash
359 // (its residency arm sets `fp8: None`), so the two cases are disjoint in practice and the
360 // order only fixes a hypothetical.
361 let (f8_bytes, f8_scale, blk, ne) = match w {
362 GpuTensor::Quant {
363 fp8: Some(f8), ne, ..
364 } if f8.blk.is_some() => {
365 if !fp8_mmq_enabled() {
366 FP8_MMQ_GATE_OFF.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
367 return Ok(None);
368 }
369 (&f8.bytes, f8.scale, f8.blk.as_ref().unwrap(), ne)
370 }
371 GpuTensor::Quant {
372 bytes,
373 qtype,
374 scale,
375 blk: Some(g),
376 ne,
377 ..
378 } if *qtype == crate::QT_F8_E4M3_BLK => {
379 if !fp8_blk_mmq_native_enabled() {
380 FP8_MMQ_GATE_OFF.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
381 return Ok(None);
382 }
383 (bytes, *scale, g, ne)
384 }
385 // A zero dispatch count is ambiguous on its own: no block operand resident looks
386 // exactly like a shape refusal. Count the no-operand case separately so the receipt
387 // says WHICH, and never per-GEMM-log (this fires on every projection of every layer).
388 _ => {
389 FP8_MMQ_NO_OPERAND.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
390 return Ok(None);
391 }
392 };
393 if ne.len() != 2 {
394 FP8_MMQ_BAD_SHAPE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
395 return Ok(None);
396 }
397 let (in_f, out_f) = (ne[0] as usize, ne[1] as usize);
398 // in_f % 16: the kernel's 16B tile-copy line. blk grid dims must match the shape — a
399 // mismatch means the operand and the grid came from different tensors; refuse rather than
400 // index a wrong block.
401 if in_f % 16 != 0
402 || blk.rows != out_f.div_ceil(128)
403 || blk.cols != in_f.div_ceil(128)
404 || f8_bytes.len() < out_f * in_f
405 || x.len() < m * in_f
406 {
407 FP8_MMQ_BAD_SHAPE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
408 return Ok(None);
409 }
410 // The per-tensor scale must be the block class's identity (source.rs sets 1.0 alongside a
411 // grid); anything else would mean a second, unapplied scale factor.
412 if f8_scale != 1.0 {
413 FP8_MMQ_BAD_SCALE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
414 return Ok(None);
415 }
416
417 // One-time NaN precondition per tensor (cached by device pointer).
418 {
419 let key = {
420 let stream = self.gpu.stream();
421 let (p, _g) = f8_bytes.device_ptr(&stream);
422 p
423 };
424 let mut guard = FP8_MMQ_NAN_OK.lock().unwrap();
425 let map = guard.get_or_insert_with(std::collections::HashMap::new);
426 let ok = match map.get(&key) {
427 Some(v) => *v,
428 None => {
429 let v = self.fp8_blk_nan_count(f8_bytes)? == 0;
430 map.insert(key, v);
431 v
432 }
433 };
434 if !ok {
435 FP8_MMQ_NAN_REFUSED.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
436 return Ok(None);
437 }
438 }
439
440 let y = self.qmatvec_mmq_fp8_blk(f8_bytes, &blk.scales, x, m, in_f, out_f)?;
441 FP8_MMQ_HITS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
442 Ok(Some(y))
443 }
444}
445
446/// Dispatch counter for this kernel. A model-level exactness or perf result is only evidence if
447/// the kernel actually RAN — a silently-refused precondition (no block operand made resident, the
448/// stash budget spent before the tensor, a NaN code present) looks exactly like "bit-identical to
449/// the floor" and "no perf change". `MEMRA_FP8_MMQ_STATS=1` prints the count at process exit so
450/// every such run carries its own proof of coverage.
451static FP8_MMQ_HITS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
452
453/// Refusal counters, one per precondition. Without these a `dispatches: 0` receipt says only
454/// "the kernel did not run", which is the same string for "no block operand was ever made
455/// resident" (budget spent / loader arm not taken / not a block-128 checkpoint) and for "the
456/// operand was there but the shape or the NaN scan rejected it". Those demand opposite fixes, so
457/// the receipt has to distinguish them.
458static FP8_MMQ_NO_OPERAND: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
459static FP8_MMQ_BAD_SHAPE: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
460static FP8_MMQ_BAD_SCALE: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
461static FP8_MMQ_NAN_REFUSED: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
462static FP8_MMQ_ENTRIES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
463static FP8_MMQ_GATE_OFF: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
464
465/// Number of prefill GEMMs that went through the per-block FP8 MMQ tile so far this process.
466pub fn fp8_mmq_hits() -> usize {
467 FP8_MMQ_HITS.load(std::sync::atomic::Ordering::Relaxed)
468}
469
470/// `entries, gate_off, hits, no_operand, bad_shape, bad_scale, nan_refused` — the full ledger.
471/// `entries` is incremented before any guard, so `entries == 0` means no prefill GEMM reached the
472/// hook at all (a dispatch-wiring fact), while `gate_off == entries` means the flag was not seen.
473pub fn fp8_mmq_ledger() -> (usize, usize, usize, usize, usize, usize, usize) {
474 use std::sync::atomic::Ordering::Relaxed;
475 (
476 FP8_MMQ_ENTRIES.load(Relaxed),
477 FP8_MMQ_GATE_OFF.load(Relaxed),
478 FP8_MMQ_HITS.load(Relaxed),
479 FP8_MMQ_NO_OPERAND.load(Relaxed),
480 FP8_MMQ_BAD_SHAPE.load(Relaxed),
481 FP8_MMQ_BAD_SCALE.load(Relaxed),
482 FP8_MMQ_NAN_REFUSED.load(Relaxed),
483 )
484}
485
486// ============================================================================================
487// ARM B' — device-side block-128 FP8 -> Q8_0 dequant pass (cu/fp8_blk_dequant.cu)
488// ============================================================================================
489
490unsafe extern "C" {
491 /// Q8_0 slab bytes for an `[out_dim x in_dim]` weight (0 = bad dims).
492 fn memra_fp8_blk_q8_0_bytes(out_dim: i32, in_dim: i32) -> usize;
493 /// One device pass: e4m3 codes + block-128 f32 scale grid -> Q8_0 blocks.
494 /// rc: 0 ok, 1 bad dims, else a cudaError_t.
495 fn memra_fp8_blk_dequant_q8_0(
496 f8_weights: *const core::ffi::c_void,
497 blk_scales: *const f32,
498 out_q8: *mut core::ffi::c_void,
499 out_dim: i32,
500 in_dim: i32,
501 stream: *mut core::ffi::c_void,
502 ) -> i32;
503}
504
505/// `MEMRA_FP8_BLK_GPU=1` gate (default OFF; ARM B', lane fp8-gemm-arm 2026-08-03): block-128
506/// FP8 safetensors weights dequant to Q8_0 ON THE GPU at load instead of host-dequant +
507/// host-re-encode. Bit-parity with the CPU path is a kernel-check gate (`fp8-blk-gpu` arm) and
508/// a real-checkpoint argmax gate — the flag exists because the CPU path stays default until
509/// both are green on the 5090.
510pub fn fp8_blk_gpu_enabled() -> bool {
511 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
512 *ON.get_or_init(|| {
513 std::env::var("MEMRA_FP8_BLK_GPU")
514 .map(|v| v == "1")
515 .unwrap_or(false)
516 })
517}
518
519impl crate::Engine {
520 /// Q8_0 slab byte count for an `[out_f, in_f]` block-128 FP8 weight.
521 pub fn fp8_blk_q8_0_bytes(out_f: usize, in_f: usize) -> usize {
522 unsafe { memra_fp8_blk_q8_0_bytes(out_f as i32, in_f as i32) }
523 }
524
525 /// ARM B' load-time pass: upload the raw e4m3 codes + the block-128 scale grid, dequant on
526 /// the GPU, and return the Q8_0 slab (byte-identical to the host re-encode). `f8` is the
527 /// checkpoint's row-major `[out_f x in_f]` codes; `grid` is the row-major
528 /// `[ceil(out_f/128) x ceil(in_f/128)]` f32 scale grid (F8BlockGrid order, verbatim).
529 pub fn fp8_blk_dequant_q8_0(
530 &self,
531 f8: &[u8],
532 grid: &[f32],
533 out_f: usize,
534 in_f: usize,
535 ) -> Result<CudaSlice<u8>, Box<dyn std::error::Error>> {
536 let (rows, cols) = (out_f.div_ceil(128), in_f.div_ceil(128));
537 if f8.len() != out_f * in_f {
538 return Err(format!(
539 "fp8_blk_dequant_q8_0: f8 len {} != out_f*in_f {}",
540 f8.len(),
541 out_f * in_f
542 )
543 .into());
544 }
545 if grid.len() != rows * cols {
546 return Err(format!(
547 "fp8_blk_dequant_q8_0: grid len {} != rows*cols {rows}*{cols}",
548 grid.len()
549 )
550 .into());
551 }
552 let need = Self::fp8_blk_q8_0_bytes(out_f, in_f);
553 if need == 0 {
554 return Err(format!(
555 "fp8_blk_dequant_q8_0: bad dims out_f={out_f} in_f={in_f} (in_f must be %32)"
556 )
557 .into());
558 }
559 let src = self.htod_bytes(f8)?;
560 let scales = self.htod(grid)?;
561 let dst = self.fp8_blk_dequant_q8_0_dev(&src, &scales, out_f, in_f)?;
562 self.gpu.stream().synchronize()?;
563 Ok(dst)
564 }
565
566 /// DEVICE-RESIDENT twin of `fp8_blk_dequant_q8_0` (lane/fp8-blk128-decode): identical kernel,
567 /// identical output bytes, but the e4m3 codes and the scale grid are ALREADY on the device and
568 /// there is no trailing `synchronize`.
569 ///
570 /// Both differences matter to its caller (`try_e4m3_blk_prefill`, per prefill call rather than
571 /// once per load): the host arm's two htods would re-upload a weight that is already resident,
572 /// and its `synchronize` would stall the CUDA owner thread on every prefill projection. Stream
573 /// ordering is sufficient without it — the dequant and the Q8_0 GEMM that consumes `dst` are
574 /// issued to the SAME stream, so the GEMM cannot observe a partially written slab.
575 pub fn fp8_blk_dequant_q8_0_dev(
576 &self,
577 f8: &CudaSlice<u8>,
578 grid: &CudaSlice<f32>,
579 out_f: usize,
580 in_f: usize,
581 ) -> Result<CudaSlice<u8>, Box<dyn std::error::Error>> {
582 let (rows, cols) = (out_f.div_ceil(128), in_f.div_ceil(128));
583 if f8.len() < out_f * in_f {
584 return Err(format!(
585 "fp8_blk_dequant_q8_0_dev: f8 len {} < out_f*in_f {}",
586 f8.len(),
587 out_f * in_f
588 )
589 .into());
590 }
591 if grid.len() < rows * cols {
592 return Err(format!(
593 "fp8_blk_dequant_q8_0_dev: grid len {} < rows*cols {rows}*{cols}",
594 grid.len()
595 )
596 .into());
597 }
598 let need = Self::fp8_blk_q8_0_bytes(out_f, in_f);
599 if need == 0 {
600 return Err(format!(
601 "fp8_blk_dequant_q8_0_dev: bad dims out_f={out_f} in_f={in_f} (in_f must be %32)"
602 )
603 .into());
604 }
605 let mut dst = self.alloc_u8_uninit(need)?;
606 let rc = {
607 let stream = self.gpu.stream();
608 let (s_p, _gs) = f8.device_ptr(&stream);
609 let (g_p, _gg) = grid.device_ptr(&stream);
610 let (d_p, _gd) = dst.device_ptr_mut(&stream);
611 unsafe {
612 memra_fp8_blk_dequant_q8_0(
613 s_p as *const core::ffi::c_void,
614 g_p as *const f32,
615 d_p as *mut core::ffi::c_void,
616 out_f as i32,
617 in_f as i32,
618 stream.cu_stream() as *mut core::ffi::c_void,
619 )
620 }
621 };
622 if rc != 0 {
623 return Err(format!(
624 "memra_fp8_blk_dequant_q8_0 rc={rc} (out_f={out_f} in_f={in_f}; 1=bad dims, \
625 else cudaError_t)"
626 )
627 .into());
628 }
629 Ok(dst)
630 }
631}