Skip to main content

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/// `MEMRA_ST_E4M3=1` gate (default OFF; lane e4m3dec 2026-07-08): F8-E4M3-origin safetensors
55/// projections load as RAW e4m3 (QT_F8_E4M3) instead of the Q8_0 re-encode. NEW NUMERIC CONFIG:
56/// decode reads the checkpoint's own e4m3 precision (the Q8_0 re-encode was a lossy extra hop) via
57/// qmatvec_e4m3_mmvq; prefill (m>=16) rides the cuBLASLt FP8 GEMM on the SAME resident bytes —
58/// one weight copy total (frees the ~GBs the MEMRA_PP_FP8 stash duplicated, no budget cap needed).
59/// Superset relationship: with this on, MEMRA_PP_FP8 and its budget are irrelevant for F8-origin
60/// tensors (they never surface as Q8_0, so the stash arm never fires).
61pub fn st_e4m3_enabled() -> bool {
62    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
63    *ON.get_or_init(|| {
64        std::env::var("MEMRA_ST_E4M3")
65            .map(|v| v == "1")
66            .unwrap_or(false)
67    })
68}
69
70/// Resident scratch for the FP8 prefill GEMM (mirrors `CutlassScratch`): the quantized activation
71/// (grown to the largest m*k seen), the 4-float scale block ([0]=amax, [1]=quant mul, [2]=folded
72/// B_SCALE — the GEMM desc holds a POINTER to slot 2, so the buffer must be resident/stable), and
73/// the cuBLASLt workspace (64MB, the probe's size). Single GPU worker => no concurrent use; the
74/// Mutex guards lazy build/grow only (matches moe_cache / cutlass_scratch).
75pub struct Fp8Scratch {
76    pub xq: CudaSlice<u8>,
77    pub scales: CudaSlice<f32>,
78    pub ws: CudaSlice<u8>,
79    cap_xq: usize,
80}
81
82/// cuBLASLt workspace size — same 64MB the probe ran its heuristics with.
83const FP8_WS_BYTES: usize = 64 << 20;
84
85impl crate::Engine {
86    /// FP8 prefill GEMM for a weight carrying the fp8 operand: y[m,out] = x[m,in] @ (e4m3 W)^T
87    /// with the per-batch act scale and per-tensor weight_scale folded in-GEMM. Returns None when
88    /// the env is off or the weight has no fp8 operand (caller falls through to the Q8_0 path).
89    pub fn try_fp8_gemm(
90        &self,
91        w: &crate::model::GpuTensor,
92        x: &CudaSlice<f32>,
93        m: usize,
94    ) -> Result<Option<CudaSlice<f32>>, Box<dyn std::error::Error>> {
95        use crate::model::GpuTensor;
96        // H100 (sm_90) is the first-class FP8 arch: cuBLASLt e4m3 GEMM is native there,
97        // so the Hopper-MMA lane re-admits this path (Phase A5, ARCHITECTURE-H100.md).
98        if crate::portable_mma_gated() {
99            return Ok(None);
100        }
101        // Two e4m3 operand sources, one GEMM:
102        //  * QT_F8_E4M3 (MEMRA_ST_E4M3): the RESIDENT decode bytes ARE the raw checkpoint e4m3 —
103        //    prefill rides them directly (one copy, no budget). Unconditional: this dtype has no
104        //    other prefill GEMM class, so the FP8 path is inherent to the config, not a flag.
105        //  * fp8 stash (MEMRA_PP_FP8=1): the Q8_0-decode config's optional duplicate operand.
106        //    Block-128 stash operands (blk: Some, Qwen official FP8) are SKIPPED: this GEMM
107        //    feeds ONE folded scalar via B_SCALE_POINTER; a block grid through it would apply
108        //    scale 1.0 to every tile. The block-scaled GEMM is P1 (probe/fp8_lt_blk_probe.cu
109        //    arbitrates cuBLASLt BLK128x128 vs a scale-fold pre-pass on sm_120).
110        let (w_bytes, w_scale, ne) = match w {
111            GpuTensor::Quant {
112                qtype,
113                bytes,
114                scale,
115                ne,
116                ..
117            } if *qtype == crate::QT_F8_E4M3 => (bytes, *scale, ne),
118            GpuTensor::Quant {
119                fp8: Some(f8), ne, ..
120            } if pp_fp8_enabled() && f8.blk.is_none() => (&f8.bytes, f8.scale, ne),
121            _ => return Ok(None),
122        };
123        let (in_f, out_f) = (ne[0] as usize, ne[1] as usize);
124
125        // lazy build / grow the resident scratch to this m*k
126        let need_xq = m * in_f;
127        let mut guard = self.fp8_scratch.lock().unwrap();
128        if guard.is_none() {
129            *guard = Some(Fp8Scratch {
130                xq: self.alloc_u8_uninit(need_xq)?,
131                scales: self.alloc_uninit::<f32>(4)?,
132                ws: self.alloc_u8_uninit(FP8_WS_BYTES)?,
133                cap_xq: need_xq,
134            });
135        }
136        let s = guard.as_mut().unwrap();
137        if need_xq > s.cap_xq {
138            s.xq = self.alloc_u8_uninit(need_xq)?;
139            s.cap_xq = need_xq;
140        }
141
142        let mut y = self.uninit(m * out_f)?; // full-overwrite GEMM output: skip memset
143        let rc = {
144            let stream = self.gpu.stream();
145            // Hold every SyncOnDrop guard across the FFI call (same pattern as cutlass_ffi);
146            // the block scope drops them before `y` is returned.
147            let (w_p, _gw) = w_bytes.device_ptr(&stream);
148            let (x_p, _gx) = x.device_ptr(&stream);
149            let (q_p, _gq) = s.xq.device_ptr_mut(&stream);
150            let (sc_p, _gs) = s.scales.device_ptr_mut(&stream);
151            let (y_p, _gy) = y.device_ptr_mut(&stream);
152            let (ws_p, _gws) = s.ws.device_ptr_mut(&stream);
153            unsafe {
154                memra_fp8_pp_gemm(
155                    w_p as *const core::ffi::c_void,
156                    x_p as *const f32,
157                    q_p as *mut core::ffi::c_void,
158                    sc_p as *mut f32,
159                    y_p as *mut f32,
160                    m as i32,
161                    out_f as i32,
162                    in_f as i32,
163                    w_scale,
164                    ws_p as *mut core::ffi::c_void,
165                    FP8_WS_BYTES,
166                    stream.cu_stream() as *mut core::ffi::c_void,
167                )
168            }
169        };
170        if rc != 0 {
171            return Err(format!(
172                "memra_fp8_pp_gemm rc={rc} (m={m} n={out_f} k={in_f}; 1xxxx=cudaError quant chain, \
173                 2xxxx=no cublasLt algo, 3xxxx=matmul status)"
174            )
175            .into());
176        }
177        Ok(Some(y))
178    }
179}
180
181// ============================================================================================
182// P1 option (b) — PER-BLOCK FP8 MMQ prefill (cu/mmq_fp8_blk.cu, lane/fp8-mmq)
183// ============================================================================================
184
185/// `MEMRA_FP8_MMQ=1` gate (default OFF; lane/fp8-mmq 2026-08-04): block-128 FP8 prefill GEMMs run
186/// through memra's OWN per-block MMQ tile instead of falling to the Q8_0 floor.
187///
188/// This is the third and only exact-AND-fast option from P1-VERDICT.md. cuBLASLt cannot take the
189/// grid at all on sm_120; ARM A's per-tensor fold is fast but diverges at greedy pos 20; ARM B' is
190/// exact but lands on the Q8_0 MMQ. This arm consumes the checkpoint's e4m3 bytes and the
191/// per-[128x128] f32 grid directly, with no re-quantization on either operand's weight side.
192///
193/// Default OFF until the model-level battery is green on the target rig — same posture the
194/// W4A8/F8F4 seams shipped with.
195pub fn fp8_mmq_enabled() -> bool {
196    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
197    *ON.get_or_init(|| {
198        std::env::var("MEMRA_FP8_MMQ")
199            .map(|v| v == "1")
200            .unwrap_or(false)
201    })
202}
203
204/// Per-tensor e4m3-NaN verdicts, keyed by the weight's device pointer. The hardware MMA reads
205/// magnitude 0x7F as NaN while the host / ARM B' reference decodes it to 0.0, so a tensor
206/// containing any must NOT ride this kernel. The scan is a full pass over the weight, so it runs
207/// ONCE per tensor (first prefill dispatch) and the verdict is cached — never per-GEMM.
208static FP8_MMQ_NAN_OK: std::sync::Mutex<Option<std::collections::HashMap<u64, bool>>> =
209    std::sync::Mutex::new(None);
210
211impl crate::Engine {
212    /// PER-BLOCK FP8 MMQ prefill GEMM for a weight carrying a block-128 fp8 operand:
213    /// y[m,out] = x[m,in] @ (e4m3 W)^T with each [128x128] weight block scaled by its own f32.
214    /// Returns None when the env is off, the weight has no block-128 fp8 operand, the shape is
215    /// unsupported, or the NaN precondition fails (caller falls through to the Q8_0 floor).
216    pub fn try_fp8_blk_mmq(
217        &self,
218        w: &crate::model::GpuTensor,
219        x: &CudaSlice<f32>,
220        m: usize,
221    ) -> Result<Option<CudaSlice<f32>>, Box<dyn std::error::Error>> {
222        use crate::model::GpuTensor;
223        // Entry counter BEFORE the env gate: a ledger of all zeros is otherwise ambiguous between
224        // "the flag was not seen" and "no prefill GEMM ever reached this hook".
225        FP8_MMQ_ENTRIES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
226        if !fp8_mmq_enabled() || crate::portable_mma_gated() {
227            FP8_MMQ_GATE_OFF.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
228            return Ok(None);
229        }
230        let (f8, ne) = match w {
231            GpuTensor::Quant {
232                fp8: Some(f8), ne, ..
233            } if f8.blk.is_some() => (f8, ne),
234            // A zero dispatch count is ambiguous on its own: no block operand resident looks
235            // exactly like a shape refusal. Count the no-operand case separately so the receipt
236            // says WHICH, and never per-GEMM-log (this fires on every projection of every layer).
237            _ => {
238                FP8_MMQ_NO_OPERAND.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
239                return Ok(None);
240            }
241        };
242        if ne.len() != 2 {
243            FP8_MMQ_BAD_SHAPE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
244            return Ok(None);
245        }
246        let (in_f, out_f) = (ne[0] as usize, ne[1] as usize);
247        // in_f % 16: the kernel's 16B tile-copy line. blk grid dims must match the shape — a
248        // mismatch means the operand and the grid came from different tensors; refuse rather than
249        // index a wrong block.
250        let blk = f8.blk.as_ref().unwrap();
251        if in_f % 16 != 0
252            || blk.rows != out_f.div_ceil(128)
253            || blk.cols != in_f.div_ceil(128)
254            || f8.bytes.len() < out_f * in_f
255            || x.len() < m * in_f
256        {
257            FP8_MMQ_BAD_SHAPE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
258            return Ok(None);
259        }
260        // The per-tensor scale must be the block class's identity (source.rs sets 1.0 alongside a
261        // grid); anything else would mean a second, unapplied scale factor.
262        if f8.scale != 1.0 {
263            FP8_MMQ_BAD_SCALE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
264            return Ok(None);
265        }
266
267        // One-time NaN precondition per tensor (cached by device pointer).
268        {
269            let key = {
270                let stream = self.gpu.stream();
271                let (p, _g) = f8.bytes.device_ptr(&stream);
272                p as u64
273            };
274            let mut guard = FP8_MMQ_NAN_OK.lock().unwrap();
275            let map = guard.get_or_insert_with(std::collections::HashMap::new);
276            let ok = match map.get(&key) {
277                Some(v) => *v,
278                None => {
279                    let v = self.fp8_blk_nan_count(&f8.bytes)? == 0;
280                    map.insert(key, v);
281                    v
282                }
283            };
284            if !ok {
285                FP8_MMQ_NAN_REFUSED.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
286                return Ok(None);
287            }
288        }
289
290        let y = self.qmatvec_mmq_fp8_blk(&f8.bytes, &blk.scales, x, m, in_f, out_f)?;
291        FP8_MMQ_HITS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
292        Ok(Some(y))
293    }
294}
295
296/// Dispatch counter for this kernel. A model-level exactness or perf result is only evidence if
297/// the kernel actually RAN — a silently-refused precondition (no block operand made resident, the
298/// stash budget spent before the tensor, a NaN code present) looks exactly like "bit-identical to
299/// the floor" and "no perf change". `MEMRA_FP8_MMQ_STATS=1` prints the count at process exit so
300/// every such run carries its own proof of coverage.
301static FP8_MMQ_HITS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
302
303/// Refusal counters, one per precondition. Without these a `dispatches: 0` receipt says only
304/// "the kernel did not run", which is the same string for "no block operand was ever made
305/// resident" (budget spent / loader arm not taken / not a block-128 checkpoint) and for "the
306/// operand was there but the shape or the NaN scan rejected it". Those demand opposite fixes, so
307/// the receipt has to distinguish them.
308static FP8_MMQ_NO_OPERAND: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
309static FP8_MMQ_BAD_SHAPE: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
310static FP8_MMQ_BAD_SCALE: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
311static FP8_MMQ_NAN_REFUSED: std::sync::atomic::AtomicUsize =
312    std::sync::atomic::AtomicUsize::new(0);
313static FP8_MMQ_ENTRIES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
314static FP8_MMQ_GATE_OFF: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
315
316/// Number of prefill GEMMs that went through the per-block FP8 MMQ tile so far this process.
317pub fn fp8_mmq_hits() -> usize {
318    FP8_MMQ_HITS.load(std::sync::atomic::Ordering::Relaxed)
319}
320
321/// `entries, gate_off, hits, no_operand, bad_shape, bad_scale, nan_refused` — the full ledger.
322/// `entries` is incremented before any guard, so `entries == 0` means no prefill GEMM reached the
323/// hook at all (a dispatch-wiring fact), while `gate_off == entries` means the flag was not seen.
324pub fn fp8_mmq_ledger() -> (usize, usize, usize, usize, usize, usize, usize) {
325    use std::sync::atomic::Ordering::Relaxed;
326    (
327        FP8_MMQ_ENTRIES.load(Relaxed),
328        FP8_MMQ_GATE_OFF.load(Relaxed),
329        FP8_MMQ_HITS.load(Relaxed),
330        FP8_MMQ_NO_OPERAND.load(Relaxed),
331        FP8_MMQ_BAD_SHAPE.load(Relaxed),
332        FP8_MMQ_BAD_SCALE.load(Relaxed),
333        FP8_MMQ_NAN_REFUSED.load(Relaxed),
334    )
335}
336
337// ============================================================================================
338// ARM B' — device-side block-128 FP8 -> Q8_0 dequant pass (cu/fp8_blk_dequant.cu)
339// ============================================================================================
340
341unsafe extern "C" {
342    /// Q8_0 slab bytes for an `[out_dim x in_dim]` weight (0 = bad dims).
343    fn memra_fp8_blk_q8_0_bytes(out_dim: i32, in_dim: i32) -> usize;
344    /// One device pass: e4m3 codes + block-128 f32 scale grid -> Q8_0 blocks.
345    /// rc: 0 ok, 1 bad dims, else a cudaError_t.
346    fn memra_fp8_blk_dequant_q8_0(
347        f8_weights: *const core::ffi::c_void,
348        blk_scales: *const f32,
349        out_q8: *mut core::ffi::c_void,
350        out_dim: i32,
351        in_dim: i32,
352        stream: *mut core::ffi::c_void,
353    ) -> i32;
354}
355
356/// `MEMRA_FP8_BLK_GPU=1` gate (default OFF; ARM B', lane fp8-gemm-arm 2026-08-03): block-128
357/// FP8 safetensors weights dequant to Q8_0 ON THE GPU at load instead of host-dequant +
358/// host-re-encode. Bit-parity with the CPU path is a kernel-check gate (`fp8-blk-gpu` arm) and
359/// a real-checkpoint argmax gate — the flag exists because the CPU path stays default until
360/// both are green on the 5090.
361pub fn fp8_blk_gpu_enabled() -> bool {
362    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
363    *ON.get_or_init(|| {
364        std::env::var("MEMRA_FP8_BLK_GPU")
365            .map(|v| v == "1")
366            .unwrap_or(false)
367    })
368}
369
370impl crate::Engine {
371    /// Q8_0 slab byte count for an `[out_f, in_f]` block-128 FP8 weight.
372    pub fn fp8_blk_q8_0_bytes(out_f: usize, in_f: usize) -> usize {
373        unsafe { memra_fp8_blk_q8_0_bytes(out_f as i32, in_f as i32) }
374    }
375
376    /// ARM B' load-time pass: upload the raw e4m3 codes + the block-128 scale grid, dequant on
377    /// the GPU, and return the Q8_0 slab (byte-identical to the host re-encode). `f8` is the
378    /// checkpoint's row-major `[out_f x in_f]` codes; `grid` is the row-major
379    /// `[ceil(out_f/128) x ceil(in_f/128)]` f32 scale grid (F8BlockGrid order, verbatim).
380    pub fn fp8_blk_dequant_q8_0(
381        &self,
382        f8: &[u8],
383        grid: &[f32],
384        out_f: usize,
385        in_f: usize,
386    ) -> Result<CudaSlice<u8>, Box<dyn std::error::Error>> {
387        let (rows, cols) = (out_f.div_ceil(128), in_f.div_ceil(128));
388        if f8.len() != out_f * in_f {
389            return Err(format!(
390                "fp8_blk_dequant_q8_0: f8 len {} != out_f*in_f {}",
391                f8.len(),
392                out_f * in_f
393            )
394            .into());
395        }
396        if grid.len() != rows * cols {
397            return Err(format!(
398                "fp8_blk_dequant_q8_0: grid len {} != rows*cols {rows}*{cols}",
399                grid.len()
400            )
401            .into());
402        }
403        let need = Self::fp8_blk_q8_0_bytes(out_f, in_f);
404        if need == 0 {
405            return Err(format!(
406                "fp8_blk_dequant_q8_0: bad dims out_f={out_f} in_f={in_f} (in_f must be %32)"
407            )
408            .into());
409        }
410        let src = self.htod_bytes(f8)?;
411        let scales = self.htod(grid)?;
412        let mut dst = self.alloc_u8_uninit(need)?;
413        let rc = {
414            let stream = self.gpu.stream();
415            let (s_p, _gs) = src.device_ptr(&stream);
416            let (g_p, _gg) = scales.device_ptr(&stream);
417            let (d_p, _gd) = dst.device_ptr_mut(&stream);
418            unsafe {
419                memra_fp8_blk_dequant_q8_0(
420                    s_p as *const core::ffi::c_void,
421                    g_p as *const f32,
422                    d_p as *mut core::ffi::c_void,
423                    out_f as i32,
424                    in_f as i32,
425                    stream.cu_stream() as *mut core::ffi::c_void,
426                )
427            }
428        };
429        if rc != 0 {
430            return Err(format!(
431                "memra_fp8_blk_dequant_q8_0 rc={rc} (out_f={out_f} in_f={in_f}; 1=bad dims, \
432                 else cudaError_t)"
433            )
434            .into());
435        }
436        self.gpu.stream().synchronize()?;
437        Ok(dst)
438    }
439}