memra_engine/mmq_ffi.rs
1//! FFI to the MMQ prefill GEMMs (cu/mmq_fp4.cu + cu/mmq_q45k.cu) — vendored floor kernels.
2//!
3//! NVFP4: the 5150-pp512 kernel from llama.cpp, ggml-decoupled into a static lib with a C-ABI host
4//! launcher. The launcher quantizes the f32 activation to block_fp4_mmq internally (llama's 2-level
5//! FP8-e8m0/UE4M3 scale = the accurate W4A8-via-FP8 path that fixes memra's W4A4 maxdiff 1.46), then
6//! launches the native mxf4nvf4 block-scale tensor-core mma.
7//!
8//! Q4_K/Q5_K: llama's k-quant int8-MMA MMQ (dequant to int8 at tile-load, q8_1 DS4 activation with
9//! the (d, sum) pair that feeds the k-quant min-offset term, shared m16n8k32 s8 mma inner loop).
10//! Replaces the hand-rolled qmatvec_gemm k-quant GEMMs that dominate prefill (32% + 28% busy).
11//!
12//! All dispatched behind MEMRA_MMQ=1. Always built (no external deps) — unlike cutlass_ffi which is
13//! MEMRA_CUTLASS-gated.
14
15use crate::Engine;
16use cudarc::driver::{CudaSlice, DevicePtr, DevicePtrMut};
17
18/// Quantize-once seam state (see `Engine::mmq_act_begin`): window epoch + one cached
19/// (epoch, act_ptr, m, in_f, D4 scratch) slot. Slot drops (freeing the scratch) on each new window.
20static MMQ_ACT_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
21#[allow(clippy::type_complexity)]
22static MMQ_ACT_SLOT: std::sync::Mutex<Option<(u64, u64, usize, usize, CudaSlice<u8>)>> =
23 std::sync::Mutex::new(None);
24/// Stream-k fixup scratch (lazy; sized once per process — one slot per SM).
25static MMQ_FIXUP_SLOT: std::sync::Mutex<Option<cudarc::driver::CudaSlice<u8>>> =
26 std::sync::Mutex::new(None);
27
28unsafe extern "C" {
29 /// Bytes needed for the block_fp4_mmq activation scratch for (in_f, n_tokens).
30 pub fn memra_mmq_nvfp4_act_bytes(in_f: i32, n_tokens: i32) -> usize;
31 /// Run the NVFP4 W4A4 MMQ prefill GEMM. y[n_tokens, out_f] = act[n_tokens, in_f] @ W[out_f, in_f]^T.
32 /// W_nvfp4_blocks : raw memra NVFP4 weight rows (block_nvfp4 36B blocks, in_f/64 per row).
33 /// act_f32 : f32 activation [n_tokens, in_f] (contiguous).
34 /// y : f32 output [n_tokens, out_f].
35 /// act_scratch : pre-alloc'd quant buffer >= memra_mmq_nvfp4_act_bytes(in_f, n_tokens).
36 /// Returns 0 on success, else (1000 + cudaError).
37 pub fn memra_mmq_nvfp4(
38 w_nvfp4_blocks: *const core::ffi::c_void,
39 act_f32: *const f32,
40 y: *mut f32,
41 in_f: i32,
42 out_f: i32,
43 n_tokens: i32,
44 act_scratch: *mut core::ffi::c_void,
45 stream: *mut core::ffi::c_void,
46 out_scale: f32,
47 ) -> i32;
48 /// Same as `memra_mmq_nvfp4`, plus the activation-quantizer selector.
49 /// per_token_scale = 1: two-level scaling (per-token row amax folded into the GEMM epilogue
50 /// + per-sub-block UE4M3). This is what `memra_mmq_nvfp4` does.
51 /// per_token_scale = 0: the v1 sub-block-only quantizer, retained as the numeric oracle so
52 /// kernel-check can measure what the row scale bought, and as the rollback seam.
53 pub fn memra_mmq_nvfp4_ex(
54 w_nvfp4_blocks: *const core::ffi::c_void,
55 act_f32: *const f32,
56 y: *mut f32,
57 in_f: i32,
58 out_f: i32,
59 n_tokens: i32,
60 act_scratch: *mut core::ffi::c_void,
61 stream: *mut core::ffi::c_void,
62 out_scale: f32,
63 per_token_scale: i32,
64 ) -> i32;
65 /// Same as `memra_mmq_nvfp4_ex`, plus the residual high-precision channel count.
66 /// residual_k = 0: off.
67 /// residual_k > 0: the k largest-magnitude activation channels (ranked across the batch) are
68 /// zeroed before quantization and their exact f32 contribution is added back as a rank-k
69 /// correction. Requires per_token_scale = 1. Clamped to MMQ_MAX_RESIDUAL_K (64).
70 pub fn memra_mmq_nvfp4_ex2(
71 w_nvfp4_blocks: *const core::ffi::c_void,
72 act_f32: *const f32,
73 y: *mut f32,
74 in_f: i32,
75 out_f: i32,
76 n_tokens: i32,
77 act_scratch: *mut core::ffi::c_void,
78 stream: *mut core::ffi::c_void,
79 out_scale: f32,
80 per_token_scale: i32,
81 residual_k: i32,
82 ) -> i32;
83 /// Bytes needed for the block_q8_1_mmq activation scratch for the NVFP4 W4A8 path.
84 pub fn memra_mmq_nvfp4_w4a8_act_bytes(in_f: i32, n_tokens: i32) -> usize;
85 /// Run the NVFP4 W4A8 MMQ prefill GEMM (STAGE 2 accuracy-safe rung). Same fast MMQ tile as
86 /// memra_mmq_nvfp4 (W4A4) but the non-Blackwell int8 pair: weight FP4 LUT-dequantized to int8 at
87 /// tile-load, activation stays q8_1 int8 (D4, the same quant class as the default int8 GEMM).
88 /// `rp`: 0 = GGUF 36B-block weight layout, 1 = A6 split-plane repack (the resident decode
89 /// layout). The rp tile loader is a pure address remap of the GGUF loader (same dequant math,
90 /// same FP op order) — output is bit-identical either way.
91 /// Same contract as memra_mmq_nvfp4 otherwise. Returns 0 or (1000 + cudaError).
92 pub fn memra_mmq_nvfp4_w4a8(
93 w_nvfp4_blocks: *const core::ffi::c_void,
94 act_f32: *const f32,
95 y: *mut f32,
96 in_f: i32,
97 out_f: i32,
98 n_tokens: i32,
99 act_scratch: *mut core::ffi::c_void,
100 stream: *mut core::ffi::c_void,
101 out_scale: f32,
102 rp: i32,
103 ) -> i32;
104 /// Bytes for the block_e4m3_mmq activation scratch (footprint-identical to block_q8_1_mmq).
105 pub fn memra_mmq_nvfp4_f8f4_act_bytes(in_f: i32, n_tokens: i32) -> usize;
106 /// R-B W4A8-FP8 MMQ prefill GEMM (research/prefill-mxf8f6f4-design.md): NVFP4 per-16 scales
107 /// fold into e4m3 weight VALUES at tile load; e4m3 activations; ONE kind::f8f6f4 m16n8k32
108 /// MMA (381-TF class) where the int8 path issues two imma k16. NEW NUMERIC CONFIG — own
109 /// battery. Same contract/rp semantics as memra_mmq_nvfp4_w4a8. Returns 0 / 1000+cudaError /
110 /// 2000+cudaError.
111 pub fn memra_mmq_nvfp4_f8f4(
112 w_nvfp4_blocks: *const core::ffi::c_void,
113 act_f32: *const f32,
114 y: *mut f32,
115 in_f: i32,
116 out_f: i32,
117 n_tokens: i32,
118 act_scratch: *mut core::ffi::c_void,
119 stream: *mut core::ffi::c_void,
120 out_scale: f32,
121 rp: i32,
122 ) -> i32;
123 /// Bytes for the per-block FP8 MMQ activation scratch (delegates to the F8F4 sizing — the
124 /// two arms deliberately share ONE activation format, `block_e4m3_mmq`).
125 pub fn memra_mmq_fp8_blk_act_bytes(in_f: i32, n_tokens: i32) -> usize;
126 /// Scale-grid dims for an [out_f x in_f] block-128 FP8 tensor (ceil-div by 128).
127 pub fn memra_mmq_fp8_blk_scale_rows(out_f: i32) -> i32;
128 pub fn memra_mmq_fp8_blk_scale_cols(in_f: i32) -> i32;
129 /// PER-BLOCK FP8 MMQ prefill GEMM (cu/mmq_fp8_blk.cu, P1 option (b)): consumes the
130 /// Qwen-official e4m3 weight bytes + the per-[128x128] f32 scale grid DIRECTLY. The weight
131 /// side is never re-quantized (the checkpoint bytes are the MMA A operand), so unlike ARM A's
132 /// per-tensor fold there is no precision loss; unlike ARM B' it does not land on the Q8_0
133 /// floor. `blk_scales` is device f32 [ceil(out_f/128) x ceil(in_f/128)], row-major.
134 /// Requires in_f % 16 == 0. Returns 0 / 1 (bad dims) / 1000+cudaError / 2000+cudaError.
135 pub fn memra_mmq_fp8_blk(
136 w_e4m3: *const core::ffi::c_void,
137 blk_scales: *const f32,
138 act_f32: *const f32,
139 y: *mut f32,
140 in_f: i32,
141 out_f: i32,
142 n_tokens: i32,
143 act_scratch: *mut core::ffi::c_void,
144 stream: *mut core::ffi::c_void,
145 out_scale: f32,
146 ) -> i32;
147 /// Count e4m3 NaN codes (magnitude 0x7F) in a device weight buffer. Those decode to NaN in
148 /// hardware but to 0.0 in the host/ARM B' convention, so a tensor containing any must NOT
149 /// ride `memra_mmq_fp8_blk`. `out_count` is a device u32 (zeroed by the call).
150 pub fn memra_fp8_blk_count_nan(
151 w_e4m3: *const core::ffi::c_void,
152 nbytes: usize,
153 out_count: *mut u32,
154 stream: *mut core::ffi::c_void,
155 ) -> i32;
156 /// Bytes needed for the block_q8_1_mmq activation scratch (shared by Q4_K and Q5_K).
157 pub fn memra_mmq_q45k_act_bytes(in_f: i32, n_tokens: i32) -> usize;
158 /// Run the Q4_K W4A8 MMQ prefill GEMM. Same contract as memra_mmq_nvfp4 (raw ggml block_q4_K
159 /// weight rows, in_f/256 144B superblocks per row). Returns 0 or (1000 + cudaError).
160 pub fn memra_mmq_q4_K(
161 w_q4k_blocks: *const core::ffi::c_void,
162 act_f32: *const f32,
163 y: *mut f32,
164 in_f: i32,
165 out_f: i32,
166 n_tokens: i32,
167 act_scratch: *mut core::ffi::c_void,
168 stream: *mut core::ffi::c_void,
169 ) -> i32;
170 /// Run the Q5_K W4A8 MMQ prefill GEMM (176B superblocks). Same contract as memra_mmq_q4_K.
171 pub fn memra_mmq_q5_K(
172 w_q5k_blocks: *const core::ffi::c_void,
173 act_f32: *const f32,
174 y: *mut f32,
175 in_f: i32,
176 out_f: i32,
177 n_tokens: i32,
178 act_scratch: *mut core::ffi::c_void,
179 stream: *mut core::ffi::c_void,
180 ) -> i32;
181
182 /// Bytes needed for the block_q8_1_mmq (D4) activation scratch for the Q8_0 MMQ path.
183 pub fn memra_mmq_q8_0_act_bytes(in_f: i32, n_tokens: i32) -> usize;
184 /// Run the Q8_0 int8-MMA MMQ prefill GEMM (MEMRA_PP_Q8MMQ). Conventional xy-tiling only (no fixup
185 /// scratch). Weight = raw ggml block_q8_0 rows (34B blocks, in_f/32 per row); activation is
186 /// quantized internally to q8_1 D4. Requires in_f % 32 == 0. Returns 0 or (1000 + cudaError).
187 pub fn memra_mmq_q8_0(
188 w_q8_0_blocks: *const core::ffi::c_void,
189 act_f32: *const f32,
190 y: *mut f32,
191 in_f: i32,
192 out_f: i32,
193 n_tokens: i32,
194 act_scratch: *mut core::ffi::c_void,
195 stream: *mut core::ffi::c_void,
196 ) -> i32;
197
198 // ---- Q1 accumulator instrument (cu/mmq_q8_0_f32acc.cu, lane/fp8-v3-gate) ----
199 // The Q8_0 MMQ floor's GEMM with the accumulator as its ONE free variable: arm S32 is the
200 // floor's `mma...s32.s8.s8.s32`, arm F32 is the same m16n8k32 shape and the same A/B/D fragment
201 // ABI with `mma...kind::f8f6f4...f32.e4m3.e4m3.f32` — the op cu/mmq_fp8_blk.cu accumulates in.
202 // Both take a PRE-QUANTIZED block_q8_1_mmq activation buffer, so the measurement is GEMM-only
203 // and cannot differ by a quantizer. Research instrument only: no dispatch seam, and neither arm's
204 // output is a numeric claim (see the TU header).
205 /// Activation-scratch bytes for the accumulator instrument (same padding rule as the floor).
206 pub fn memra_accprobe_act_bytes(in_f: i32, n_tokens: i32) -> usize;
207 /// ARM S32 — the floor's GEMM verbatim, s32 accumulate. Returns 0, 1, or 1000+cudaError.
208 pub fn memra_accprobe_gemm_s32(
209 w_q8_0_blocks: *const core::ffi::c_void,
210 act_q: *const core::ffi::c_void,
211 y: *mut f32,
212 in_f: i32,
213 out_f: i32,
214 n_tokens: i32,
215 stream: *mut core::ffi::c_void,
216 ) -> i32;
217 /// ARM F32 — byte-identical kernel, f32 accumulate over the e4m3 reading of the same bytes.
218 pub fn memra_accprobe_gemm_f32(
219 w_q8_0_blocks: *const core::ffi::c_void,
220 act_q: *const core::ffi::c_void,
221 y: *mut f32,
222 in_f: i32,
223 out_f: i32,
224 n_tokens: i32,
225 stream: *mut core::ffi::c_void,
226 ) -> i32;
227
228 /// Bytes needed for the block_q8_1_mmq (D4) activation scratch for the Q4_0 MMQ path.
229 pub fn memra_mmq_q4_0_act_bytes(in_f: i32, n_tokens: i32) -> usize;
230 /// Run the Q4_0 int8-MMA MMQ prefill GEMM (MEMRA_PP_Q4MMQ). Nibbles dequant to int8 at
231 /// tile-load (the -8 zero-point folds into the quants, D4 epilogue — same accuracy class as
232 /// the Q8_0 MMQ). `rp`: 0 = raw ggml 18B blocks, 1 = MEMRA_Q4RP split-plane repack (qs plane +
233 /// fp16 d plane) — pure address remap, bit-identical output either way. Requires
234 /// in_f % 32 == 0. Returns 0 or (1000 + cudaError).
235 pub fn memra_mmq_q4_0(
236 w_q4_0: *const core::ffi::c_void,
237 act_f32: *const f32,
238 y: *mut f32,
239 in_f: i32,
240 out_f: i32,
241 n_tokens: i32,
242 act_scratch: *mut core::ffi::c_void,
243 stream: *mut core::ffi::c_void,
244 rp: i32,
245 ) -> i32;
246 /// Quantize-only entry (quantize-once seam): f32 activation -> block_q8_1_mmq scratch.
247 pub fn memra_mmq_q4_0_quant_act(
248 act_f32: *const f32,
249 act_scratch: *mut core::ffi::c_void,
250 in_f: i32,
251 n_tokens: i32,
252 stream: *mut core::ffi::c_void,
253 ) -> i32;
254 /// GEMM-only entry: consumes a pre-quantized scratch (from memra_mmq_q4_0_quant_act).
255 pub fn memra_mmq_q4_0_gemm(
256 w_q4_0: *const core::ffi::c_void,
257 act_scratch: *const core::ffi::c_void,
258 y: *mut f32,
259 in_f: i32,
260 out_f: i32,
261 n_tokens: i32,
262 stream: *mut core::ffi::c_void,
263 rp: i32,
264 ) -> i32;
265 /// Stream-k fixup scratch bytes (one [MMQ_X x MMQ_Y] f32 slot per SM).
266 pub fn memra_mmq_q4_0_fixup_bytes() -> usize;
267 /// Force the CLC work-stealing arm: 1 = on, 0 = off (static grid), -1 = MEMRA_MMQ_CLC env
268 /// default. Schedule-only swap of the xy-tiling kernel — bit-identical output by
269 /// construction (perf-frontier lever #1). Returns 1 when the CLC kernel is compiled in
270 /// (SM_100+ gencode), 0 on sm_89/90a builds (force is a no-op there; static grid always).
271 pub fn memra_mmq_q4_0_set_clc(force: i32) -> i32;
272 /// Stream-k GEMM entry: deterministic form selection, with the SK form itself
273 /// falling back to tiling when wave efficiency is at least 90%.
274 pub fn memra_mmq_q4_0_gemm_sk(
275 w_q4_0: *const core::ffi::c_void,
276 act_scratch: *const core::ffi::c_void,
277 y: *mut f32,
278 fixup_scratch: *mut core::ffi::c_void,
279 in_f: i32,
280 out_f: i32,
281 n_tokens: i32,
282 stream: *mut core::ffi::c_void,
283 rp: i32,
284 ) -> i32;
285
286 // ---- IQ3_S / IQ4_XS expert-segmented int8-MMA MMQ (cu/mmq_iq_experts.cu, MEMRA_MOE_MMA) ----
287 /// Bytes for the token-major block_q8_1_mmq activation scratch (in_f, n_tokens).
288 pub fn memra_mmq_iq_experts_act_bytes(in_f: i32, n_tokens: i32) -> usize;
289 /// Quantize token-major f32 activation [n_tokens, in_f] -> block_q8_1_mmq (D4). Returns 0 or 1000+err.
290 pub fn memra_mmq_iq_quantize_act(
291 act_f32: *const f32,
292 act_scratch: *mut core::ffi::c_void,
293 in_f: i32,
294 n_tokens: i32,
295 stream: *mut core::ffi::c_void,
296 ) -> i32;
297 /// Fused act-epilogue: silu/gelu(gate)*up + q8_1_mmq (D4) quantize in ONE launch — no f32 act
298 /// buffer. gate/up pair-major [n_tokens, in_f]; scratch identical to memra_mmq_iq_quantize_act.
299 /// act_kind: 0=silu*mul, 1=gelu_tanh*mul. Byte-identical to the two-pass path (kernel-check gated).
300 pub fn memra_mmq_iq_fused_act_quant(
301 gate: *const f32,
302 up: *const f32,
303 act_scratch: *mut core::ffi::c_void,
304 in_f: i32,
305 n_tokens: i32,
306 act_kind: i32,
307 stream: *mut core::ffi::c_void,
308 ) -> i32;
309 /// Expert-segmented IQ MMA MMQ. Same CSR shape as moe_pairs_matvec_q8_dec: `table` = [3,n_expert]
310 /// device slab ptrs, CSR ex_ids/ex_off/ex_pairs group pairs by expert, pair_tok gathers the
311 /// activation row. y = [n_pairs, out_f] pair-major. `act_scratch` pre-quantized over n_tokens.
312 /// qtype: 5=IQ4_XS, 6=IQ3_S. Returns 0 or 1000+cudaError.
313 /// Dense-trunk IQ4_XS MMQ (lane/kquant-tile-loaders): the dense analog of the expert
314 /// kernel for non-expert IQ4_XS 2-D matmuls (the KAT-Coder trunk class). Quantizes the
315 /// f32 activation to D4 q8_1_mmq internally; `act_scratch` sized by
316 /// `memra_mmq_iq_experts_act_bytes`. Requires in_f % 256 == 0.
317 pub fn memra_mmq_iq4xs_dense(
318 w_blocks: *const core::ffi::c_void,
319 act_f32: *const f32,
320 y: *mut f32,
321 in_f: i32,
322 out_f: i32,
323 n_tokens: i32,
324 row_bytes: i64,
325 act_scratch: *mut core::ffi::c_void,
326 stream: *mut core::ffi::c_void,
327 ) -> i32;
328 pub fn memra_mmq_iq_experts(
329 table: *const u64,
330 proj: i32,
331 n_expert: i32,
332 ex_ids: *const i32,
333 ex_off: *const i32,
334 ex_pairs: *const i32,
335 pair_tok: *const i32,
336 act_scratch: *const core::ffi::c_void,
337 y: *mut f32,
338 in_f: i32,
339 out_f: i32,
340 n_active: i32,
341 n_tokens: i32,
342 qtype: i32,
343 row_bytes: i64,
344 stream: *mut core::ffi::c_void,
345 ) -> i32;
346
347 // ---- MoE grouped f16 GEMM (cu/moe_f16_grouped.cu, round 46 arc 2) ----
348 pub fn memra_moe_f16g_dequant(
349 table: *const u64,
350 proj: i32,
351 n_expert: i32,
352 ex_ids: *const i32,
353 w_f16: *mut core::ffi::c_void,
354 in_f: i32,
355 out_f: i32,
356 n_active: i32,
357 qtype: i32,
358 row_bytes: i64,
359 stream: *mut core::ffi::c_void,
360 ) -> i32;
361 pub fn memra_moe_f16g_gather_act(
362 x: *const f32,
363 pair_tok_or_null: *const i32,
364 act_f16: *mut core::ffi::c_void,
365 row_scale: *mut f32,
366 in_f: i32,
367 n_pairs: i32,
368 stream: *mut core::ffi::c_void,
369 ) -> i32;
370 pub fn memra_moe_f16g_h2f_scaled(
371 src_f16: *const core::ffi::c_void,
372 dst: *mut f32,
373 row_scale: *const f32,
374 ncols: i32,
375 nrows: i32,
376 stream: *mut core::ffi::c_void,
377 ) -> i32;
378 pub fn memra_moe_f16g_gemm(
379 w_f16: *const core::ffi::c_void,
380 act_f16: *const core::ffi::c_void,
381 y_f16: *mut core::ffi::c_void,
382 ex_off_host: *const i32,
383 n_active: i32,
384 in_f: i32,
385 out_f: i32,
386 stream: *mut core::ffi::c_void,
387 ) -> i32;
388 pub fn memra_moe_f16g_h2f(
389 src_f16: *const core::ffi::c_void,
390 dst: *mut f32,
391 n: usize,
392 stream: *mut core::ffi::c_void,
393 ) -> i32;
394 // Single-kernel grouped GEMM (MEMRA_MOE_F16G=2, rounds 49+51): on OUR stream, f32 C with
395 // the act row-scale folded in — no cublas internal-stream race, no sync. Round 51 runs it
396 // as a persistent problem-visitor over the real tiles with two tile forms (32x64 tail
397 // / 128x64x64 3-stage): shape_sel < 0 = the round-49 grid-scan kernel (rollback
398 // arm); else groups with m_e >= cross ride the 128 form. ex_off_host sizes the visitor
399 // grids host-side (the offsets are already there at the call site — no extra transfer).
400 // tail != 0 (lane/sk-tail-form): sub-cross groups ride the DEEP tail (32x64x64 3-stage);
401 // 0 = the round-51 2-stage 32x64x32 (MEMRA_F16G_TAIL=0 rollback). Byte-identical arms.
402 pub fn memra_moe_f16g_gemm_sk(
403 w_f16: *const core::ffi::c_void,
404 act_f16: *const core::ffi::c_void,
405 y_f32: *mut f32,
406 row_scale: *const f32,
407 ex_off_dev: *const i32,
408 ex_off_host: *const i32,
409 n_active: i32,
410 max_m: i32,
411 in_f: i32,
412 out_f: i32,
413 shape_sel: i32,
414 cross: i32,
415 tail: i32,
416 stream: *mut core::ffi::c_void,
417 ) -> i32;
418 // DIRECT-FROM-QUANT sk visitor grouped GEMM (lane/kquant-tile-loaders + iq-direct-loaders):
419 // the visitor forms with the B (weight) tiles dequanted in-register from the expert
420 // superblocks — no f16 dequant workspace pass. Bit-identical to the workspace path by
421 // construction (kernel-check "f16g-kq-direct"). qtype: QT_Q4_K | QT_Q6_K | QT_IQ4_XS |
422 // QT_IQ3_S; rc=2 = not admitted here (caller keeps the dequant-workspace path).
423 // tail: as memra_moe_f16g_gemm_sk.
424 pub fn memra_moe_kq_gemm_sk(
425 table: *const u64,
426 proj: i32,
427 n_expert: i32,
428 ex_ids: *const i32,
429 act_f16: *const core::ffi::c_void,
430 y_f32: *mut f32,
431 row_scale: *const f32,
432 ex_off_dev: *const i32,
433 ex_off_host: *const i32,
434 n_active: i32,
435 max_m: i32,
436 in_f: i32,
437 out_f: i32,
438 qtype: i32,
439 cross: i32,
440 tail: i32,
441 row_bytes: i64,
442 stream: *mut core::ffi::c_void,
443 ) -> i32;
444}
445
446/// W4A8-MMQ DEFAULT-FLIP seam (2026-07-05): the vendored MMQ prefill suite is DEFAULT-ON — NVFP4
447/// takes the W4A8 MMQ tile (same int8 accuracy class as the int8 GEMM it replaces, all exactness
448/// gates hold, ~1.9x pp512; the rp tile-loader arm coexists with the A6 split-plane repack) and
449/// Q4_K/Q5_K take the vendored k-quant int8-MMA MMQ (also int8-class; gated with W4A8 in the same
450/// battery — the predecessor's `MEMRA_MMQ_W4A8=1` arm engaged BOTH, this flip preserves exactly
451/// that measured config). `MEMRA_MMQ_W4A8=0` = escape hatch back to the int8 GEMM prefill
452/// everywhere. `MEMRA_MMQ=1` additionally switches GGUF-layout NVFP4 to the W4A4 mxf4nvf4 tile
453/// (speed/accuracy tradeoff opt-in, unchanged).
454pub fn mmq_w4a8_enabled() -> bool {
455 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
456 *ON.get_or_init(|| {
457 std::env::var("MEMRA_MMQ_W4A8")
458 .map(|v| v != "0")
459 .unwrap_or(true)
460 })
461}
462
463/// Residual high-precision activation channels for the W4A4 MMQ prefill path.
464/// `MEMRA_MMQ_RESIDUAL_K=<k>` keeps the k largest-magnitude activation channels out of the e2m1
465/// quantized path and adds their exact f32 contribution back as a rank-k correction. k=0 (default)
466/// is off; the kernel clamps to MMQ_MAX_RESIDUAL_K (64).
467///
468/// Read LIVE per call, not OnceLock'd, for the same reason `MEMRA_MMQ` is: the W4A4 exactness gate
469/// sweeps arms inside ONE process against ONE set of loaded weights, and a cached first read would
470/// pin every later arm to whatever the first one saw.
471pub fn mmq_residual_k() -> i32 {
472 std::env::var("MEMRA_MMQ_RESIDUAL_K")
473 .ok()
474 .and_then(|v| v.parse::<i32>().ok())
475 .unwrap_or(0)
476 .clamp(0, 64)
477}
478
479/// Q8_0 MMQ prefill seam (lane/ppmmq lever 2, DEFAULT ON since 2026-07-09 — `MEMRA_PP_Q8MMQ=0`
480/// reverts): routes Q8_0 dense
481/// projections (m>=16) through the vendored int8-MMA MMQ (cu/mmq_q8_0.cu) instead of the hand-rolled
482/// `qmatvec_gemm_q8_0` tiling GEMM. Its own numeric config (MMA f32 reduction order != the tiling
483/// GEMM's) — gated with the full exactness battery. Default OFF until the battery is green.
484pub fn mmq_q8_enabled() -> bool {
485 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
486 // Promotion battery (2026-07-09): argmax MATCH on 35B p1/p2/p3 + 9B p2/p3 (p4-16k OOMs
487 // identically with and without the flag — pre-existing gate capacity limit, not this seam);
488 // kernel-check ALL GREEN; run-spec K=1..8 PASS on 9B+35B. 35B pp 2456->3069 free-clock.
489 *ON.get_or_init(|| {
490 std::env::var("MEMRA_PP_Q8MMQ")
491 .map(|v| v != "0")
492 .unwrap_or(true)
493 })
494}
495
496/// IQ4_XS dense-trunk MMQ prefill seam (lane/kquant-tile-loaders, 2026-08-02): routes
497/// NON-expert IQ4_XS 2-D projections (m>=16) through the vendored-machinery int8-MMA dense
498/// MMQ (cu/mmq_iq_experts.cu `mmq_iq4xs_dense_kernel`) instead of the per-column dp4a grid
499/// — the KAT-Coder prefill wall (0.169x vs llama; zero weight reuse across tokens,
500/// research/kat-anomaly-20260802 §6). Its own numeric config (MMA reduction order) — gated
501/// with the full exactness battery. m=1..15 decode/verify keep dp4a (dispatch parity).
502/// `MEMRA_PP_IQMMQ=0` reverts.
503pub fn mmq_iq4xs_enabled() -> bool {
504 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
505 *ON.get_or_init(|| {
506 std::env::var("MEMRA_PP_IQMMQ")
507 .map(|v| v != "0")
508 .unwrap_or(true)
509 })
510}
511
512/// Q4_0 MMQ prefill seam (gemma-4-12B lane, 2026-07-22): routes Q4_0 dense projections (m>=16)
513/// through the vendored int8-MMA MMQ (cu/mmq_q4_0.cu) instead of the hand-rolled
514/// `qmatvec_gemm_q4_0[_rp]` tiling GEMM (measured 77% of the 12B prime pass). Its own numeric
515/// config (MMA f32 reduction order != the tiling GEMM's) — gated with the full exactness battery
516/// before default-flip; `MEMRA_PP_Q4MMQ=0` reverts.
517pub fn mmq_q4_enabled() -> bool {
518 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
519 *ON.get_or_init(|| {
520 std::env::var("MEMRA_PP_Q4MMQ")
521 .map(|v| v != "0")
522 .unwrap_or(true)
523 })
524}
525
526impl Engine {
527 /// True if `w` should take a vendored MMQ GEMM under the current env policy (see
528 /// `mmq_w4a8_enabled`): NVFP4 needs in_f % 64 == 0, Q4_K/Q5_K need in_f % 256 == 0.
529 pub fn mmq_supports(&self, w: &crate::model::GpuTensor) -> bool {
530 use crate::model::GpuTensor;
531 if crate::portable_mma_gated() {
532 return false;
533 }
534 let mmq_opt_in = std::env::var("MEMRA_MMQ").is_ok();
535 match w {
536 // A6 split-plane repacked NVFP4: ONLY the W4A8 loader has an rp arm (pure address
537 // remap, bit-identical output — mmq_nvfp4_w4a8.cu load_tiles_nvfp4_w4a8<is_rp>).
538 // The W4A4 loader (mmq_fp4.cu load_tiles_nvfp4_nvfp4) reads 36B GGUF blocks only,
539 // so an rp weight with W4A8 disabled falls through to the rp-ported int8 GEMM.
540 // NVFP4 W4A8/W4A4 launchers use .kind::f8f6f4 / mxf4nvf4 tile MMA — sm_100a+/
541 // sm_120a-only. On every portable build (incl. the 90a Hopper-MMA lane) they are
542 // fail-closed link stubs (build.rs), so never offer them here.
543 GpuTensor::Quant { qtype, rp, .. } if *qtype == crate::QT_NVFP4 && *rp => {
544 !cfg!(memra_portable_cuda) && mmq_w4a8_enabled() && w.in_features() % 64 == 0
545 }
546 // GGUF-layout NVFP4 (MEMRA_RP=0): W4A8 (default-on) or the explicit W4A4 opt-in.
547 GpuTensor::Quant { qtype, .. } if *qtype == crate::QT_NVFP4 => {
548 !cfg!(memra_portable_cuda)
549 && (mmq_w4a8_enabled() || mmq_opt_in)
550 && w.in_features() % 64 == 0
551 }
552 GpuTensor::Quant { qtype, .. }
553 if *qtype == crate::QT_Q4_K || *qtype == crate::QT_Q5_K =>
554 {
555 (mmq_w4a8_enabled() || mmq_opt_in) && w.in_features() % 256 == 0
556 }
557 // Q8_0 dense projections (35B attn/ssm/shexp): opt-in only (MEMRA_PP_Q8MMQ=1), its own
558 // numeric config vs qmatvec_gemm_q8_0. in_f % 256 == 0: MMQ_ITER_K=256 loads 8-block
559 // groups, so a non-multiple row would read a garbage weight tail (fp16 d bytes can be
560 // NaN-pattern, and NaN * 0-padded-activation = NaN — the 26B ffn_down lesson).
561 GpuTensor::Quant { qtype, .. } if *qtype == crate::QT_Q8_0 => {
562 mmq_q8_enabled() && w.in_features() % 256 == 0
563 }
564 // Q4_0 dense projections (gemma QAT ggufs): MEMRA_PP_Q4MMQ seam. Both weight layouts
565 // (raw 18B blocks and the MEMRA_Q4RP split-plane repack) have loader arms. Same
566 // in_f % 256 == 0 tail rule as Q8_0 (26B ffn_down in_f=2112 NaN'd on the %32 gate);
567 // non-multiples fall back to the hand-rolled qmatvec_gemm_q4_0[_rp].
568 GpuTensor::Quant { qtype, .. } if *qtype == crate::QT_Q4_0 => {
569 mmq_q4_enabled() && w.in_features() % 256 == 0
570 }
571 // IQ4_XS dense projections (KAT-Coder trunk): m>=16 prefill only — decode and
572 // spec-verify (m<16) keep the qmatvec_iq4_XS_dp4a per-column program (the
573 // kat-anomaly dispatch-parity law). Requires the dp4a fast path itself enabled:
574 // MEMRA_IQ_FAST=0 (the Stage-A oracle rollback) must also kill this arm so the
575 // rollback stays a full-path seam. in_f % 256: MMQ_ITER_K walks whole superblocks.
576 GpuTensor::Quant { qtype, .. } if *qtype == crate::QT_IQ4_XS => {
577 mmq_iq4xs_enabled() && Self::iq_fast_enabled() && w.in_features() % 256 == 0
578 }
579 _ => false,
580 }
581 }
582
583 /// Unified vendored-MMQ dispatch: routes to the NVFP4 or Q4_K/Q5_K launcher by qtype.
584 /// Caller MUST have checked `mmq_supports(w)`. `x` is the RAW f32 activation.
585 pub fn qmatvec_mmq(
586 &self,
587 w: &crate::model::GpuTensor,
588 x: &CudaSlice<f32>,
589 m: usize,
590 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
591 use crate::model::GpuTensor;
592 let (in_f, out_f) = (w.in_features(), w.out_features());
593 let GpuTensor::Quant {
594 bytes,
595 scale,
596 qtype,
597 rp,
598 ..
599 } = w
600 else {
601 return Err("qmatvec_mmq: not a Quant tensor".into());
602 };
603 // NVFP4 tile choice: W4A8 (accuracy-safe int8 pair, DEFAULT since the flip) vs W4A4
604 // (mxf4nvf4 mma, explicit MEMRA_MMQ=1 speed/accuracy tradeoff). An rp weight ALWAYS takes
605 // W4A8 — only its loader has the split-plane arm (pure address remap, bit-identical).
606 // Explicit MEMRA_MMQ_W4A8=1 still overrides a simultaneous MEMRA_MMQ=1 (predecessor rule).
607 let w4a8_explicit = std::env::var("MEMRA_MMQ_W4A8")
608 .map(|v| v != "0")
609 .unwrap_or(false);
610 let use_w4a8 =
611 *rp || w4a8_explicit || (mmq_w4a8_enabled() && std::env::var("MEMRA_MMQ").is_err());
612 match *qtype {
613 // STAGE 2: the accuracy-safe int8 W4A8 MMQ tile (weight FP4->int8 dequant + q8_1
614 // activation) — handles BOTH weight layouts (rp = A6 split-plane vs GGUF blocks).
615 q if q == crate::QT_NVFP4 && use_w4a8 => {
616 self.qmatvec_mmq_nvfp4_w4a8(bytes, x, m, in_f, out_f, *scale, *rp)
617 }
618 q if q == crate::QT_NVFP4 => self.qmatvec_mmq_nvfp4(bytes, x, m, in_f, out_f, *scale),
619 q if q == crate::QT_Q4_K || q == crate::QT_Q5_K => {
620 let mut y = self.qmatvec_mmq_q45k_raw(bytes, x, m, in_f, out_f, q)?;
621 if *scale != 1.0 {
622 self.scale_inplace(&mut y, *scale, m * out_f)?;
623 }
624 Ok(y)
625 }
626 q if q == crate::QT_Q8_0 => {
627 // wgmma arm (sm_90a, task 8): OPT-IN via MEMRA_WGMMA=1 — v0 measured 3845
628 // vs MMQ 8692 tok/s pp512 (2026-07-26 N=5), so MMQ stays the default until
629 // the pipelined wgmma wins. Reads the rp4 split-plane mirror + the engine's
630 // q8_1 activation planes. Same numeric class as MMQ (exact s32 per 32-block,
631 // one f32 fold per block, ascending K) — kernel-check tolerance-gated.
632 if cfg!(memra_hopper_mma) && out_f % 64 == 0 && crate::wgmma_gemm_enabled() {
633 if let GpuTensor::Quant { rp4: Some(m4), .. } = w {
634 let (aq, ad) = self.quantize_q8_1(x, m, in_f)?;
635 let mut y =
636 self.qmatvec_gemm_q8_0_wgmma_raw(m4, &aq, &ad, m, in_f, out_f)?;
637 if *scale != 1.0 {
638 self.scale_inplace(&mut y, *scale, m * out_f)?;
639 }
640 return Ok(y);
641 }
642 }
643 let mut y = self.qmatvec_mmq_q8_0_raw(bytes, x, m, in_f, out_f)?;
644 if *scale != 1.0 {
645 self.scale_inplace(&mut y, *scale, m * out_f)?;
646 }
647 Ok(y)
648 }
649 q if q == crate::QT_Q4_0 => {
650 let mut y = self.qmatvec_mmq_q4_0_raw(bytes, x, m, in_f, out_f, *rp)?;
651 if *scale != 1.0 {
652 self.scale_inplace(&mut y, *scale, m * out_f)?;
653 }
654 Ok(y)
655 }
656 q if q == crate::QT_IQ4_XS => {
657 let GpuTensor::Quant { row_bytes, .. } = w else {
658 unreachable!()
659 };
660 let mut y = self.qmatvec_mmq_iq4xs_raw(bytes, x, m, in_f, out_f, *row_bytes)?;
661 if *scale != 1.0 {
662 self.scale_inplace(&mut y, *scale, m * out_f)?;
663 }
664 Ok(y)
665 }
666 q => Err(format!("qmatvec_mmq: unsupported qtype {q}").into()),
667 }
668 }
669
670 /// Bare IQ4_XS dense MMQ launch (no macro-scale) — also the kernel_check gate entry.
671 pub fn qmatvec_mmq_iq4xs_raw(
672 &self,
673 bytes: &CudaSlice<u8>,
674 x: &CudaSlice<f32>,
675 m: usize,
676 in_f: usize,
677 out_f: usize,
678 row_bytes: usize,
679 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
680 assert!(
681 in_f % 256 == 0,
682 "MMQ IQ4_XS requires in_f % 256 == 0, got {in_f}"
683 );
684 let act_bytes = unsafe { memra_mmq_iq_experts_act_bytes(in_f as i32, m as i32) };
685 let mut scratch = self.alloc_uninit::<u8>(act_bytes)?;
686 let mut y = self.alloc_uninit::<f32>(m * out_f)?;
687 {
688 let stream = self.gpu.stream();
689 let (w_p, _gw) = bytes.device_ptr(&stream);
690 let (x_p, _gx) = x.device_ptr(&stream);
691 let (y_p, _gy) = y.device_ptr_mut(&stream);
692 let (s_p, _gs) = scratch.device_ptr_mut(&stream);
693 let rc = unsafe {
694 memra_mmq_iq4xs_dense(
695 w_p as *const core::ffi::c_void,
696 x_p as *const f32,
697 y_p as *mut f32,
698 in_f as i32,
699 out_f as i32,
700 m as i32,
701 row_bytes as i64,
702 s_p as *mut core::ffi::c_void,
703 stream.cu_stream() as *mut core::ffi::c_void,
704 )
705 };
706 if rc != 0 {
707 return Err(format!("memra_mmq_iq4xs_dense rc={rc}").into());
708 }
709 }
710 Ok(y)
711 }
712
713 /// Bare Q4_K/Q5_K MMQ launch (no macro-scale) — also the kernel_check accuracy-gate entry.
714 /// Conventional xy-tiling only (the vendored stream-K arm — MEMRA_MMQ_STREAMK — was removed
715 /// 2026-07-08: 1.11x per-GEMM but its k-split f32 reorder flipped the model argmax gate;
716 /// rig5090.jsonl 2026-07-03 has the record).
717 pub fn qmatvec_mmq_q45k_raw(
718 &self,
719 bytes: &CudaSlice<u8>,
720 x: &CudaSlice<f32>,
721 m: usize,
722 in_f: usize,
723 out_f: usize,
724 qtype: i32,
725 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
726 assert!(
727 in_f % 256 == 0,
728 "MMQ Q4_K/Q5_K requires in_f % 256 == 0, got {in_f}"
729 );
730 let act_bytes = unsafe { memra_mmq_q45k_act_bytes(in_f as i32, m as i32) };
731 let mut scratch = self.alloc_uninit::<u8>(act_bytes)?;
732 let mut y = self.alloc_uninit::<f32>(m * out_f)?;
733 {
734 let stream = self.gpu.stream();
735 let (w_p, _gw) = bytes.device_ptr(&stream);
736 let (x_p, _gx) = x.device_ptr(&stream);
737 let (y_p, _gy) = y.device_ptr_mut(&stream);
738 let (s_p, _gs) = scratch.device_ptr_mut(&stream);
739 let launcher = if qtype == crate::QT_Q4_K {
740 memra_mmq_q4_K
741 } else {
742 memra_mmq_q5_K
743 };
744 let rc = unsafe {
745 launcher(
746 w_p as *const core::ffi::c_void,
747 x_p as *const f32,
748 y_p as *mut f32,
749 in_f as i32,
750 out_f as i32,
751 m as i32,
752 s_p as *mut core::ffi::c_void,
753 stream.cu_stream() as *mut core::ffi::c_void,
754 )
755 };
756 if rc != 0 {
757 return Err(format!("memra_mmq_q45k(qtype={qtype}) rc={rc}").into());
758 }
759 }
760 Ok(y)
761 }
762
763 /// Bare Q8_0 int8-MMA MMQ launch (no macro-scale) — the kernel_check accuracy-gate entry and
764 /// the `qmatvec_mmq` dispatch body. Conventional xy-tiling only (no stream-K / fixup scratch).
765 pub fn qmatvec_mmq_q8_0_raw(
766 &self,
767 bytes: &CudaSlice<u8>,
768 x: &CudaSlice<f32>,
769 m: usize,
770 in_f: usize,
771 out_f: usize,
772 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
773 assert!(
774 in_f % 32 == 0,
775 "MMQ Q8_0 requires in_f % 32 == 0, got {in_f}"
776 );
777 let act_bytes = unsafe { memra_mmq_q8_0_act_bytes(in_f as i32, m as i32) };
778 let mut scratch = self.alloc_uninit::<u8>(act_bytes)?;
779 let mut y = self.alloc_uninit::<f32>(m * out_f)?;
780 {
781 let stream = self.gpu.stream();
782 let (w_p, _gw) = bytes.device_ptr(&stream);
783 let (x_p, _gx) = x.device_ptr(&stream);
784 let (y_p, _gy) = y.device_ptr_mut(&stream);
785 let (s_p, _gs) = scratch.device_ptr_mut(&stream);
786 let rc = unsafe {
787 memra_mmq_q8_0(
788 w_p as *const core::ffi::c_void,
789 x_p as *const f32,
790 y_p as *mut f32,
791 in_f as i32,
792 out_f as i32,
793 m as i32,
794 s_p as *mut core::ffi::c_void,
795 stream.cu_stream() as *mut core::ffi::c_void,
796 )
797 };
798 if rc != 0 {
799 return Err(format!("memra_mmq_q8_0 rc={rc}").into());
800 }
801 }
802 Ok(y)
803 }
804
805 /// Accumulator-instrument bytes for a pre-quantized block_q8_1_mmq activation buffer
806 /// (cu/mmq_q8_0_f32acc.cu). The caller synthesizes that buffer itself — see `accprobe_gemm`.
807 pub fn accprobe_act_bytes(&self, in_f: usize, m: usize) -> usize {
808 unsafe { memra_accprobe_act_bytes(in_f as i32, m as i32) }
809 }
810
811 /// Run one arm of the Q1 accumulator instrument. `f32acc=false` is the Q8_0 MMQ floor's GEMM
812 /// verbatim (s32 accumulate); `f32acc=true` is the byte-identical kernel with the f8f6f4 f32
813 /// accumulate. `act_q` is a PRE-QUANTIZED block_q8_1_mmq buffer of at least
814 /// `accprobe_act_bytes(in_f, m)` bytes — keeping the quantizer out of the timed region is the
815 /// point, so this wrapper does not build it. Research instrument: the output is not a numeric
816 /// claim.
817 pub fn accprobe_gemm(
818 &self,
819 w_q8_0: &CudaSlice<u8>,
820 act_q: &CudaSlice<u8>,
821 m: usize,
822 in_f: usize,
823 out_f: usize,
824 f32acc: bool,
825 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
826 assert!(
827 in_f % 32 == 0,
828 "accprobe requires in_f % 32 == 0, got {in_f}"
829 );
830 assert!(
831 act_q.len() >= self.accprobe_act_bytes(in_f, m),
832 "accprobe act_q too small: {} < {}",
833 act_q.len(),
834 self.accprobe_act_bytes(in_f, m)
835 );
836 let mut y = self.alloc_uninit::<f32>(m * out_f)?;
837 {
838 let stream = self.gpu.stream();
839 let (w_p, _gw) = w_q8_0.device_ptr(&stream);
840 let (a_p, _ga) = act_q.device_ptr(&stream);
841 let (y_p, _gy) = y.device_ptr_mut(&stream);
842 let f = if f32acc {
843 memra_accprobe_gemm_f32
844 } else {
845 memra_accprobe_gemm_s32
846 };
847 let rc = unsafe {
848 f(
849 w_p as *const core::ffi::c_void,
850 a_p as *const core::ffi::c_void,
851 y_p as *mut f32,
852 in_f as i32,
853 out_f as i32,
854 m as i32,
855 stream.cu_stream() as *mut core::ffi::c_void,
856 )
857 };
858 if rc != 0 {
859 let arm = if f32acc { "f32" } else { "s32" };
860 return Err(format!("memra_accprobe_gemm_{arm} rc={rc}").into());
861 }
862 }
863 Ok(y)
864 }
865
866 /// Open a quantize-once sharing window for the NEXT activation (quantize-once seam): sibling
867 /// Q4_0 MMQ matmuls on the SAME input (q/k/v; gate/up) quantize its D4 scratch once. Safe by
868 /// construction: a hit requires the same window epoch AND the same (ptr, m, in_f) — the caller
869 /// opens a window while it holds the shared input alive, so its address can neither change nor
870 /// be recycled inside the window. Paths that never call this never hit the cache.
871 pub fn mmq_act_begin(&self) {
872 use std::sync::atomic::Ordering;
873 MMQ_ACT_EPOCH.fetch_add(1, Ordering::Relaxed);
874 *MMQ_ACT_SLOT.lock().unwrap() = None;
875 }
876
877 /// Bare Q4_0 int8-MMA MMQ launch (no macro-scale) — the kernel_check accuracy-gate entry and
878 /// the `qmatvec_mmq` dispatch body. `rp` selects the weight layout (MEMRA_Q4RP split-plane vs
879 /// raw ggml 18B blocks) — pure address remap, bit-identical output.
880 pub fn qmatvec_mmq_q4_0_raw(
881 &self,
882 bytes: &CudaSlice<u8>,
883 x: &CudaSlice<f32>,
884 m: usize,
885 in_f: usize,
886 out_f: usize,
887 rp: bool,
888 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
889 use std::sync::atomic::Ordering;
890 assert!(
891 in_f % 32 == 0,
892 "MMQ Q4_0 requires in_f % 32 == 0, got {in_f}"
893 );
894 let mut y = self.alloc_uninit::<f32>(m * out_f)?;
895 let stream = self.gpu.stream();
896 let (x_p, _gx) = x.device_ptr(&stream);
897 let epoch = MMQ_ACT_EPOCH.load(Ordering::Relaxed);
898 // quantize-once: reuse the window's scratch when the SAME activation comes back.
899 let mut slot = MMQ_ACT_SLOT.lock().unwrap();
900 let hit = matches!(&*slot,
901 Some((e, p, mm, inf, _)) if *e == epoch && *p == x_p as u64 && *mm == m && *inf == in_f);
902 if !hit {
903 let act_bytes = unsafe { memra_mmq_q4_0_act_bytes(in_f as i32, m as i32) };
904 let mut scratch = self.alloc_uninit::<u8>(act_bytes)?;
905 {
906 let (s_p, _gs) = scratch.device_ptr_mut(&stream);
907 let rc = unsafe {
908 memra_mmq_q4_0_quant_act(
909 x_p as *const f32,
910 s_p as *mut core::ffi::c_void,
911 in_f as i32,
912 m as i32,
913 stream.cu_stream() as *mut core::ffi::c_void,
914 )
915 };
916 if rc != 0 {
917 return Err(
918 format!("memra_mmq_q4_0_quant_act(in_f={in_f}, m={m}) rc={rc}").into(),
919 );
920 }
921 }
922 *slot = Some((epoch, x_p as u64, m, in_f, scratch));
923 }
924 let scratch = &slot.as_ref().unwrap().4;
925 {
926 let (w_p, _gw) = bytes.device_ptr(&stream);
927 let (y_p, _gy) = y.device_ptr_mut(&stream);
928 let (s_p, _gs) = scratch.device_ptr(&stream);
929 // Stream-k arm (DEFAULT since 2026-07-23; MEMRA_MMQ_SK=0 reverts to xy-tiling):
930 // small-batch tail-wave fix — the sk entry itself falls back to (bit-identical)
931 // tiling at >=90% wave efficiency. Band-class fold order below that. Gate: 12B
932 // pp512 +3.3% (1.005x vs llama), pp1736 +1.0%; 31B +0.5%; D512 sentinel MATCH.
933 //
934 // SPEC-SERVING FLIP (2026-07-27, the f16pv/wkv acceptance-law pattern): with
935 // MEMRA_DRAFT set, big dense models force tiling while MoE/small models defer
936 // to the fail-closed TILE form. The former shape-timing autotune was removed 2026-08-14:
937 // its per-process timing coin selected different fold orders on independent
938 // boots. On the measured 82-SM 5090, TILE is both faster and higher-acceptance
939 // for the 26B depth cell. Every other hardware class requires its own gate
940 // before selecting SK without an explicit form override.
941 // MEMRA_MMQ_SK controls entry and MEMRA_MMQ_SK_FORM pins the numerical form.
942 // HOPPER DEFAULT OFF (2026-07-31, #23): on sm_90a the SK arm computes WRONG
943 // values for the 26B a4b's non-rp Q4_0 shapes once the prefill width crosses
944 // 256 (prefill argmax garbage, maxdiff ~10; MEMRA_MMQ_SK=0 -> MATCH,
945 // one-variable kill x confirmed on-box). The SK split/fixup is SM-count
946 // dependent (132 vs 170) — until the kernel is
947 // fixed for that class, Hopper fails CLOSED to the bit-identical xy-tiling
948 // (cost on the healthy models: g12 -1.4%, g31 -0.6% prefill, N=3 on-box).
949 // sm_120a keeps the SK entry on (rig-divergence law). MEMRA_MMQ_SK=1 forces
950 // entry; MEMRA_MMQ_SK_FORM=sk forces the actual SK numerical form.
951 static SK_ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
952 let sk = match crate::MMQ_SK_FORCE.load(std::sync::atomic::Ordering::Relaxed) {
953 0 => false,
954 1 => true,
955 _ => *SK_ON.get_or_init(|| {
956 std::env::var("MEMRA_MMQ_SK")
957 .map(|v| v != "0")
958 .unwrap_or(!cfg!(memra_hopper_mma))
959 }),
960 };
961 let rc = if sk {
962 let mut fx = MMQ_FIXUP_SLOT.lock().unwrap();
963 if fx.is_none() {
964 let nb = unsafe { memra_mmq_q4_0_fixup_bytes() };
965 *fx = Some(self.alloc_uninit::<u8>(nb)?);
966 }
967 let (f_p, _gf) = fx.as_mut().unwrap().device_ptr_mut(&stream);
968 unsafe {
969 memra_mmq_q4_0_gemm_sk(
970 w_p as *const core::ffi::c_void,
971 s_p as *const core::ffi::c_void,
972 y_p as *mut f32,
973 f_p as *mut core::ffi::c_void,
974 in_f as i32,
975 out_f as i32,
976 m as i32,
977 stream.cu_stream() as *mut core::ffi::c_void,
978 rp as i32,
979 )
980 }
981 } else {
982 unsafe {
983 memra_mmq_q4_0_gemm(
984 w_p as *const core::ffi::c_void,
985 s_p as *const core::ffi::c_void,
986 y_p as *mut f32,
987 in_f as i32,
988 out_f as i32,
989 m as i32,
990 stream.cu_stream() as *mut core::ffi::c_void,
991 rp as i32,
992 )
993 }
994 };
995 if rc != 0 {
996 return Err(format!(
997 "memra_mmq_q4_0_gemm(rp={rp}, in_f={in_f}, out_f={out_f}, m={m}, wbytes={}) rc={rc}",
998 bytes.len()
999 )
1000 .into());
1001 }
1002 }
1003 Ok(y)
1004 }
1005
1006 /// Run the vendored NVFP4 MMQ prefill GEMM from raw weight bytes + f32 activation.
1007 /// y[m, out_f] = x[m, in_f] @ W^T. The per-tensor NVFP4 macro-scale is FOLDED into the MMQ
1008 /// write-back epilogue (was a separate scale_inplace launch + full y round-trip per matmul).
1009 /// Same elementwise multiply -> bit-identical to the two-launch form.
1010 /// `x` is the RAW f32 activation (the launcher quantizes it to block_fp4_mmq internally).
1011 pub fn qmatvec_mmq_nvfp4(
1012 &self,
1013 bytes: &CudaSlice<u8>,
1014 x: &CudaSlice<f32>,
1015 m: usize,
1016 in_f: usize,
1017 out_f: usize,
1018 scale: f32,
1019 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1020 self.qmatvec_mmq_nvfp4_scaled(bytes, x, m, in_f, out_f, scale)
1021 }
1022
1023 /// Bare MMQ launch (no macro-scale) — for the kernel_check accuracy gate.
1024 pub fn qmatvec_mmq_nvfp4_raw(
1025 &self,
1026 bytes: &CudaSlice<u8>,
1027 x: &CudaSlice<f32>,
1028 m: usize,
1029 in_f: usize,
1030 out_f: usize,
1031 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1032 self.qmatvec_mmq_nvfp4_scaled(bytes, x, m, in_f, out_f, 1.0)
1033 }
1034
1035 /// Bare MMQ launch on the PRE-PORT activation quantizer (per-sub-block UE4M3 scale only, no
1036 /// per-token row amax). The numeric oracle for the two-level quantizer: kernel-check runs both
1037 /// and reports the accuracy delta, so the port's value is measured rather than asserted.
1038 pub fn qmatvec_mmq_nvfp4_raw_v1(
1039 &self,
1040 bytes: &CudaSlice<u8>,
1041 x: &CudaSlice<f32>,
1042 m: usize,
1043 in_f: usize,
1044 out_f: usize,
1045 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1046 self.qmatvec_mmq_nvfp4_inner(bytes, x, m, in_f, out_f, 1.0, false, 0)
1047 }
1048
1049 /// Bare MMQ launch with an explicit residual-channel count — for the kernel-check k sweep.
1050 pub fn qmatvec_mmq_nvfp4_raw_res(
1051 &self,
1052 bytes: &CudaSlice<u8>,
1053 x: &CudaSlice<f32>,
1054 m: usize,
1055 in_f: usize,
1056 out_f: usize,
1057 residual_k: i32,
1058 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1059 self.qmatvec_mmq_nvfp4_inner(bytes, x, m, in_f, out_f, 1.0, true, residual_k)
1060 }
1061
1062 fn qmatvec_mmq_nvfp4_scaled(
1063 &self,
1064 bytes: &CudaSlice<u8>,
1065 x: &CudaSlice<f32>,
1066 m: usize,
1067 in_f: usize,
1068 out_f: usize,
1069 scale: f32,
1070 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1071 self.qmatvec_mmq_nvfp4_inner(bytes, x, m, in_f, out_f, scale, true, mmq_residual_k())
1072 }
1073
1074 fn qmatvec_mmq_nvfp4_inner(
1075 &self,
1076 bytes: &CudaSlice<u8>,
1077 x: &CudaSlice<f32>,
1078 m: usize,
1079 in_f: usize,
1080 out_f: usize,
1081 scale: f32,
1082 per_token_scale: bool,
1083 residual_k: i32,
1084 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1085 assert!(
1086 in_f % 64 == 0,
1087 "MMQ NVFP4 requires in_f % 64 == 0, got {in_f}"
1088 );
1089 let act_bytes = unsafe { memra_mmq_nvfp4_act_bytes(in_f as i32, m as i32) };
1090 let mut scratch = self.alloc_uninit::<u8>(act_bytes)?;
1091 let mut y = self.alloc_uninit::<f32>(m * out_f)?;
1092 {
1093 let stream = self.gpu.stream();
1094 let (w_p, _gw) = bytes.device_ptr(&stream);
1095 let (x_p, _gx) = x.device_ptr(&stream);
1096 let (y_p, _gy) = y.device_ptr_mut(&stream);
1097 let (s_p, _gs) = scratch.device_ptr_mut(&stream);
1098 let rc = unsafe {
1099 memra_mmq_nvfp4_ex2(
1100 w_p as *const core::ffi::c_void,
1101 x_p as *const f32,
1102 y_p as *mut f32,
1103 in_f as i32,
1104 out_f as i32,
1105 m as i32,
1106 s_p as *mut core::ffi::c_void,
1107 stream.cu_stream() as *mut core::ffi::c_void,
1108 scale,
1109 per_token_scale as i32,
1110 residual_k,
1111 )
1112 };
1113 if rc != 0 {
1114 return Err(format!("memra_mmq_nvfp4_ex2 rc={rc}").into());
1115 }
1116 }
1117 Ok(y)
1118 }
1119
1120 /// STAGE 2 W4A8 MMQ NVFP4: same tile as the W4A4 path, but weight FP4 is LUT-dequantized to
1121 /// int8 at tile-load and the activation stays q8_1 int8 — the accuracy-safe rung. Macro-scale
1122 /// folded into the write-back epilogue (bit-identical to a post-matmul scale_inplace).
1123 /// `rp` selects the weight layout (A6 split-plane vs GGUF blocks) — bit-identical output.
1124 pub fn qmatvec_mmq_nvfp4_w4a8(
1125 &self,
1126 bytes: &CudaSlice<u8>,
1127 x: &CudaSlice<f32>,
1128 m: usize,
1129 in_f: usize,
1130 out_f: usize,
1131 scale: f32,
1132 rp: bool,
1133 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1134 self.qmatvec_mmq_nvfp4_w4a8_scaled(bytes, x, m, in_f, out_f, scale, rp)
1135 }
1136
1137 /// Bare W4A8 MMQ launch (no macro-scale, GGUF layout) — for the kernel_check accuracy gate.
1138 pub fn qmatvec_mmq_nvfp4_w4a8_raw(
1139 &self,
1140 bytes: &CudaSlice<u8>,
1141 x: &CudaSlice<f32>,
1142 m: usize,
1143 in_f: usize,
1144 out_f: usize,
1145 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1146 self.qmatvec_mmq_nvfp4_w4a8_scaled(bytes, x, m, in_f, out_f, 1.0, false)
1147 }
1148
1149 /// Bare W4A8 MMQ launch on an A6 split-plane repacked weight — the rp-loader bit-identity gate
1150 /// compares this against `qmatvec_mmq_nvfp4_w4a8_raw` on the same weight.
1151 pub fn qmatvec_mmq_nvfp4_w4a8_raw_rp(
1152 &self,
1153 bytes: &CudaSlice<u8>,
1154 x: &CudaSlice<f32>,
1155 m: usize,
1156 in_f: usize,
1157 out_f: usize,
1158 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1159 self.qmatvec_mmq_nvfp4_w4a8_scaled(bytes, x, m, in_f, out_f, 1.0, true)
1160 }
1161
1162 fn qmatvec_mmq_nvfp4_w4a8_scaled(
1163 &self,
1164 bytes: &CudaSlice<u8>,
1165 x: &CudaSlice<f32>,
1166 m: usize,
1167 in_f: usize,
1168 out_f: usize,
1169 scale: f32,
1170 rp: bool,
1171 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1172 assert!(
1173 in_f % 64 == 0,
1174 "MMQ NVFP4 W4A8 requires in_f % 64 == 0, got {in_f}"
1175 );
1176 let act_bytes = unsafe { memra_mmq_nvfp4_w4a8_act_bytes(in_f as i32, m as i32) };
1177 let mut scratch = self.alloc_uninit::<u8>(act_bytes)?;
1178 let mut y = self.alloc_uninit::<f32>(m * out_f)?;
1179 {
1180 let stream = self.gpu.stream();
1181 let (w_p, _gw) = bytes.device_ptr(&stream);
1182 let (x_p, _gx) = x.device_ptr(&stream);
1183 let (y_p, _gy) = y.device_ptr_mut(&stream);
1184 let (s_p, _gs) = scratch.device_ptr_mut(&stream);
1185 // MEMRA_MMQ_F8F4=1: the R-B W4A8-FP8 tile (own numeric config; battery-gated seam).
1186 // Scratch layouts are footprint-identical, so only the entry point swaps.
1187 static F8F4: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1188 let f8f4 = *F8F4.get_or_init(|| std::env::var("MEMRA_MMQ_F8F4").as_deref() == Ok("1"));
1189 let rc = unsafe {
1190 if f8f4 {
1191 memra_mmq_nvfp4_f8f4(
1192 w_p as *const core::ffi::c_void,
1193 x_p as *const f32,
1194 y_p as *mut f32,
1195 in_f as i32,
1196 out_f as i32,
1197 m as i32,
1198 s_p as *mut core::ffi::c_void,
1199 stream.cu_stream() as *mut core::ffi::c_void,
1200 scale,
1201 rp as i32,
1202 )
1203 } else {
1204 memra_mmq_nvfp4_w4a8(
1205 w_p as *const core::ffi::c_void,
1206 x_p as *const f32,
1207 y_p as *mut f32,
1208 in_f as i32,
1209 out_f as i32,
1210 m as i32,
1211 s_p as *mut core::ffi::c_void,
1212 stream.cu_stream() as *mut core::ffi::c_void,
1213 scale,
1214 rp as i32,
1215 )
1216 }
1217 };
1218 if rc != 0 {
1219 return Err(format!("memra_mmq_nvfp4_w4a8(f8f4={f8f4}) rc={rc}").into());
1220 }
1221 }
1222 Ok(y)
1223 }
1224
1225 /// PER-BLOCK FP8 MMQ prefill GEMM (cu/mmq_fp8_blk.cu). `w_e4m3` is the raw checkpoint e4m3
1226 /// plane [out_f x in_f] and `blk_scales` the device f32 grid [ceil(out_f/128) x
1227 /// ceil(in_f/128)] — no re-quantization of either.
1228 pub fn qmatvec_mmq_fp8_blk(
1229 &self,
1230 w_e4m3: &CudaSlice<u8>,
1231 blk_scales: &CudaSlice<f32>,
1232 x: &CudaSlice<f32>,
1233 m: usize,
1234 in_f: usize,
1235 out_f: usize,
1236 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1237 self.qmatvec_mmq_fp8_blk_scaled(w_e4m3, blk_scales, x, m, in_f, out_f, 1.0)
1238 }
1239
1240 pub fn qmatvec_mmq_fp8_blk_scaled(
1241 &self,
1242 w_e4m3: &CudaSlice<u8>,
1243 blk_scales: &CudaSlice<f32>,
1244 x: &CudaSlice<f32>,
1245 m: usize,
1246 in_f: usize,
1247 out_f: usize,
1248 scale: f32,
1249 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1250 assert!(
1251 in_f % 16 == 0,
1252 "per-block FP8 MMQ requires in_f % 16 == 0, got {in_f}"
1253 );
1254 let want_scales = ((out_f + 127) / 128) * ((in_f + 127) / 128);
1255 assert!(
1256 blk_scales.len() >= want_scales,
1257 "blk_scales too small: {} < {want_scales}",
1258 blk_scales.len()
1259 );
1260 assert!(
1261 w_e4m3.len() >= out_f * in_f,
1262 "e4m3 plane too small: {} < {}",
1263 w_e4m3.len(),
1264 out_f * in_f
1265 );
1266 let act_bytes = unsafe { memra_mmq_fp8_blk_act_bytes(in_f as i32, m as i32) };
1267 let mut scratch = self.alloc_uninit::<u8>(act_bytes)?;
1268 let mut y = self.alloc_uninit::<f32>(m * out_f)?;
1269 {
1270 let stream = self.gpu.stream();
1271 let (w_p, _gw) = w_e4m3.device_ptr(&stream);
1272 let (sc_p, _gsc) = blk_scales.device_ptr(&stream);
1273 let (x_p, _gx) = x.device_ptr(&stream);
1274 let (y_p, _gy) = y.device_ptr_mut(&stream);
1275 let (s_p, _gs) = scratch.device_ptr_mut(&stream);
1276 let rc = unsafe {
1277 memra_mmq_fp8_blk(
1278 w_p as *const core::ffi::c_void,
1279 sc_p as *const f32,
1280 x_p as *const f32,
1281 y_p as *mut f32,
1282 in_f as i32,
1283 out_f as i32,
1284 m as i32,
1285 s_p as *mut core::ffi::c_void,
1286 stream.cu_stream() as *mut core::ffi::c_void,
1287 scale,
1288 )
1289 };
1290 if rc != 0 {
1291 return Err(format!("memra_mmq_fp8_blk rc={rc}").into());
1292 }
1293 }
1294 Ok(y)
1295 }
1296
1297 /// Count e4m3 NaN codes (magnitude 0x7F) in a device e4m3 plane. 0 is the precondition for
1298 /// routing that tensor through `qmatvec_mmq_fp8_blk` (hardware decodes them to NaN, the
1299 /// host/ARM B' reference to 0.0).
1300 pub fn fp8_blk_nan_count(
1301 &self,
1302 w_e4m3: &CudaSlice<u8>,
1303 ) -> Result<u32, Box<dyn std::error::Error>> {
1304 let mut cnt = self.htod_u32_v(&[0u32])?;
1305 let n = w_e4m3.len();
1306 {
1307 let stream = self.gpu.stream();
1308 let (w_p, _gw) = w_e4m3.device_ptr(&stream);
1309 let (c_p, _gc) = cnt.device_ptr_mut(&stream);
1310 let rc = unsafe {
1311 memra_fp8_blk_count_nan(
1312 w_p as *const core::ffi::c_void,
1313 n,
1314 c_p as *mut u32,
1315 stream.cu_stream() as *mut core::ffi::c_void,
1316 )
1317 };
1318 if rc != 0 {
1319 return Err(format!("memra_fp8_blk_count_nan rc={rc}").into());
1320 }
1321 }
1322 Ok(self.dtoh_u32(&cnt)?[0])
1323 }
1324
1325 /// Quantize token-major f32 activation [n_tokens, in_f] to the block_q8_1_mmq (D4) scratch the
1326 /// IQ expert-MMA kernel consumes. Returns the scratch buffer (one per proj input per layer).
1327 pub fn mmq_iq_quantize_act(
1328 &self,
1329 x: &CudaSlice<f32>,
1330 in_f: usize,
1331 n_tokens: usize,
1332 ) -> Result<CudaSlice<u8>, Box<dyn std::error::Error>> {
1333 let act_bytes = unsafe { memra_mmq_iq_experts_act_bytes(in_f as i32, n_tokens as i32) };
1334 let mut scratch = self.alloc_uninit::<u8>(act_bytes)?;
1335 {
1336 let stream = self.gpu.stream();
1337 let (x_p, _gx) = x.device_ptr(&stream);
1338 let (s_p, _gs) = scratch.device_ptr_mut(&stream);
1339 let rc = unsafe {
1340 memra_mmq_iq_quantize_act(
1341 x_p as *const f32,
1342 s_p as *mut core::ffi::c_void,
1343 in_f as i32,
1344 n_tokens as i32,
1345 stream.cu_stream() as *mut core::ffi::c_void,
1346 )
1347 };
1348 if rc != 0 {
1349 return Err(format!("memra_mmq_iq_quantize_act rc={rc}").into());
1350 }
1351 }
1352 Ok(scratch)
1353 }
1354
1355 /// Fused act-epilogue (research lever #3): silu/gelu(gate)*up + D4 quantize in one launch —
1356 /// replaces moe_pairs_{silu,gelu}_mul + mmq_iq_quantize_act without materializing the f32 act
1357 /// buffer (saves one full write + one full read pass over [n_pairs x n_ff]). Scratch bytes are
1358 /// BYTE-IDENTICAL to the two-pass path (kernel-check `iq fused act+quant` gates it).
1359 /// `act_kind`: 0 = silu*mul (qwen35moe), 1 = gelu_tanh*mul (gemma4).
1360 pub fn mmq_iq_fused_act_quant(
1361 &self,
1362 gate: &CudaSlice<f32>,
1363 up: &CudaSlice<f32>,
1364 in_f: usize,
1365 n_tokens: usize,
1366 act_kind: i32,
1367 ) -> Result<CudaSlice<u8>, Box<dyn std::error::Error>> {
1368 let act_bytes = unsafe { memra_mmq_iq_experts_act_bytes(in_f as i32, n_tokens as i32) };
1369 let mut scratch = self.alloc_uninit::<u8>(act_bytes)?;
1370 {
1371 let stream = self.gpu.stream();
1372 let (g_p, _gg) = gate.device_ptr(&stream);
1373 let (u_p, _gu) = up.device_ptr(&stream);
1374 let (s_p, _gs) = scratch.device_ptr_mut(&stream);
1375 let rc = unsafe {
1376 memra_mmq_iq_fused_act_quant(
1377 g_p as *const f32,
1378 u_p as *const f32,
1379 s_p as *mut core::ffi::c_void,
1380 in_f as i32,
1381 n_tokens as i32,
1382 act_kind,
1383 stream.cu_stream() as *mut core::ffi::c_void,
1384 )
1385 };
1386 if rc != 0 {
1387 return Err(format!("memra_mmq_iq_fused_act_quant rc={rc}").into());
1388 }
1389 }
1390 Ok(scratch)
1391 }
1392
1393 /// Expert-segmented IQ3_S/IQ4_XS int8-MMA MMQ (the m16n8k16.s8 analog of moe_pairs_matvec_q8_dec).
1394 /// Same CSR inputs (table/ex_ids/ex_off/ex_pairs/pair_tok) + a pre-quantized q8_1_mmq activation
1395 /// scratch (from `mmq_iq_quantize_act` over n_tokens). y = [n_pairs, out_f] pair-major.
1396 #[allow(clippy::too_many_arguments)]
1397 pub fn mmq_iq_experts(
1398 &self,
1399 table: &CudaSlice<u64>,
1400 proj: i32,
1401 n_expert: usize,
1402 ex_ids: &CudaSlice<i32>,
1403 ex_off: &CudaSlice<i32>,
1404 ex_pairs: &CudaSlice<i32>,
1405 pair_tok: &CudaSlice<i32>,
1406 act_scratch: &CudaSlice<u8>,
1407 in_f: usize,
1408 out_f: usize,
1409 n_active: usize,
1410 n_pairs: usize,
1411 n_tokens: usize,
1412 qtype: i32,
1413 row_bytes: usize,
1414 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1415 let mut y = self.alloc_uninit::<f32>(n_pairs * out_f)?;
1416 {
1417 let stream = self.gpu.stream();
1418 let (tab_p, _g0) = table.device_ptr(&stream);
1419 let (ei_p, _g1) = ex_ids.device_ptr(&stream);
1420 let (eo_p, _g2) = ex_off.device_ptr(&stream);
1421 let (ep_p, _g3) = ex_pairs.device_ptr(&stream);
1422 let (pt_p, _g4) = pair_tok.device_ptr(&stream);
1423 let (as_p, _g5) = act_scratch.device_ptr(&stream);
1424 let (y_p, _g6) = y.device_ptr_mut(&stream);
1425 let rc = unsafe {
1426 memra_mmq_iq_experts(
1427 tab_p as *const u64,
1428 proj,
1429 n_expert as i32,
1430 ei_p as *const i32,
1431 eo_p as *const i32,
1432 ep_p as *const i32,
1433 pt_p as *const i32,
1434 as_p as *const core::ffi::c_void,
1435 y_p as *mut f32,
1436 in_f as i32,
1437 out_f as i32,
1438 n_active as i32,
1439 n_tokens as i32,
1440 qtype,
1441 row_bytes as i64,
1442 stream.cu_stream() as *mut core::ffi::c_void,
1443 )
1444 };
1445 if rc != 0 {
1446 return Err(format!("memra_mmq_iq_experts rc={rc}").into());
1447 }
1448 }
1449 Ok(y)
1450 }
1451
1452 /// Gather+convert the activation to f16 pair-major [n_pairs, in_f] for the grouped
1453 /// GEMM, normalized per row by its amax (raw f16 overflows on gemma's activation
1454 /// spikes — round 46 NaN find). Returns (act_f16, row_scales) — the scales fold back
1455 /// into the GEMM output. `pair_tok` = None when the input is already pair-major.
1456 pub fn moe_f16g_act(
1457 &self,
1458 x: &CudaSlice<f32>,
1459 pair_tok: Option<&CudaSlice<i32>>,
1460 in_f: usize,
1461 n_pairs: usize,
1462 ) -> Result<(CudaSlice<u8>, CudaSlice<f32>), Box<dyn std::error::Error>> {
1463 let mut act = self.alloc_uninit::<u8>(n_pairs * in_f * 2)?;
1464 let mut scales = self.alloc_uninit::<f32>(n_pairs)?;
1465 {
1466 let stream = self.gpu.stream();
1467 let (x_p, _gx) = x.device_ptr(&stream);
1468 let pt_p = match pair_tok {
1469 Some(pt) => {
1470 let (p, _g) = pt.device_ptr(&stream);
1471 p as *const i32
1472 }
1473 None => std::ptr::null(),
1474 };
1475 let (a_p, _ga) = act.device_ptr_mut(&stream);
1476 let (s_p, _gs) = scales.device_ptr_mut(&stream);
1477 let rc = unsafe {
1478 memra_moe_f16g_gather_act(
1479 x_p as *const f32,
1480 pt_p,
1481 a_p as *mut core::ffi::c_void,
1482 s_p as *mut f32,
1483 in_f as i32,
1484 n_pairs as i32,
1485 stream.cu_stream() as *mut core::ffi::c_void,
1486 )
1487 };
1488 if rc != 0 {
1489 return Err(format!("memra_moe_f16g_gather_act rc={rc}").into());
1490 }
1491 }
1492 Ok((act, scales))
1493 }
1494
1495 /// One projection through the grouped f16 lane: dequant the active experts' rows to an
1496 /// f16 workspace, then ONE grouped GEMM over the CSR groups (variable m per expert).
1497 /// y = f32 [n_pairs, out_f] pair-major — same layout as mmq_iq_experts.
1498 /// MEMRA_MOE_F16G=1: cublasGemmGroupedBatchedEx (+ h2f pass + per-projection sync — the
1499 /// grouped API runs on internal streams unordered with ours, round-47 ledger).
1500 /// MEMRA_MOE_F16G=2: single-kernel grouped GEMM on the engine stream (round 49) — the
1501 /// row scale folds into the kernel epilogue; no f16 C, no h2f, NO sync (ordered by
1502 /// construction). f16-MIRROR numeric class either way (argmax/spec gated, not
1503 /// byte-identity). Errors on unsupported qtype (caller keeps the MMQ arm as fallback).
1504 #[allow(clippy::too_many_arguments)]
1505 pub fn moe_f16_grouped(
1506 &self,
1507 table: &CudaSlice<u64>,
1508 proj: i32,
1509 n_expert: usize,
1510 ex_ids: &CudaSlice<i32>,
1511 ex_off_host: &[i32],
1512 ex_off_dev: &CudaSlice<i32>,
1513 act_f16: &CudaSlice<u8>,
1514 act_scale: &CudaSlice<f32>,
1515 in_f: usize,
1516 out_f: usize,
1517 n_active: usize,
1518 n_pairs: usize,
1519 qtype: i32,
1520 row_bytes: usize,
1521 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1522 let sk = crate::moe_f16g_mode() >= 2 && in_f % 32 == 0;
1523 // DIRECT-FROM-QUANT lane (lane/kquant-tile-loaders + lane/iq-direct-loaders, default
1524 // ON — MEMRA_F16G_DIRECT=0 is the rollback seam): Q4_K/Q6_K/IQ4_XS/IQ3_S expert
1525 // projections skip the dequant-workspace pass entirely; the sk visitor forms dequant
1526 // B tiles in-register from the superblocks. Bit-identical to the workspace path by
1527 // construction (kernel-check "f16g-kq-direct") — this is a pure data-movement change,
1528 // not a numeric-class change. Admission mirrors the C-side guards; the grid-scan
1529 // rollback arm (MEMRA_F16G_SK=0) keeps the workspace.
1530 let (shape_sel, cross) = crate::moe_f16g_sk_params();
1531 if sk
1532 && shape_sel >= 0
1533 && crate::moe_f16g_direct_on(qtype)
1534 && (qtype == crate::QT_Q4_K
1535 || qtype == crate::QT_Q6_K
1536 || qtype == crate::QT_IQ4_XS
1537 || qtype == crate::QT_IQ3_S)
1538 && in_f % 256 == 0
1539 && n_active <= 512
1540 && n_active > 0
1541 {
1542 let max_m = ex_off_host
1543 .windows(2)
1544 .map(|w| w[1] - w[0])
1545 .max()
1546 .unwrap_or(0);
1547 let mut y = self.alloc_uninit::<f32>(n_pairs * out_f)?;
1548 {
1549 let stream = self.gpu.stream();
1550 let (tab_p, _g0) = table.device_ptr(&stream);
1551 let (ei_p, _g1) = ex_ids.device_ptr(&stream);
1552 let (a_p, _g2) = act_f16.device_ptr(&stream);
1553 let (s_p, _g3) = act_scale.device_ptr(&stream);
1554 let (off_p, _g4) = ex_off_dev.device_ptr(&stream);
1555 let (y_p, _g5) = y.device_ptr_mut(&stream);
1556 let rc = unsafe {
1557 memra_moe_kq_gemm_sk(
1558 tab_p as *const u64,
1559 proj,
1560 n_expert as i32,
1561 ei_p as *const i32,
1562 a_p as *const core::ffi::c_void,
1563 y_p as *mut f32,
1564 s_p as *const f32,
1565 off_p as *const i32,
1566 ex_off_host.as_ptr(),
1567 n_active as i32,
1568 max_m,
1569 in_f as i32,
1570 out_f as i32,
1571 qtype,
1572 cross,
1573 crate::moe_f16g_tail_on() as i32,
1574 row_bytes as i64,
1575 stream.cu_stream() as *mut core::ffi::c_void,
1576 )
1577 };
1578 if rc != 0 {
1579 return Err(format!("memra_moe_kq_gemm_sk rc={rc}").into());
1580 }
1581 }
1582 return Ok(y);
1583 }
1584 // one-time cublas grouped init (algo heuristics + module load cost ~10% of a cold
1585 // g26 prime when paid inside the first projection): a tiny dummy grouped GEMM at
1586 // first use, synced, so the real prime runs warm. The =2 path never touches cublas.
1587 if !sk {
1588 static WARM: std::sync::Once = std::sync::Once::new();
1589 let mut warm_err = None;
1590 WARM.call_once(|| {
1591 let r = (|| -> Result<(), Box<dyn std::error::Error>> {
1592 let w = self.alloc_uninit::<u8>(2 * 32 * 64 * 2)?;
1593 let a = self.alloc_uninit::<u8>(4 * 64 * 2)?;
1594 let mut yw = self.alloc_uninit::<u8>(4 * 32 * 2)?;
1595 let off = [0i32, 2, 4];
1596 let stream = self.gpu.stream();
1597 let (w_p, _a1) = w.device_ptr(&stream);
1598 let (a_p, _a2) = a.device_ptr(&stream);
1599 let (y_p, _a3) = yw.device_ptr_mut(&stream);
1600 let rc = unsafe {
1601 memra_moe_f16g_gemm(
1602 w_p as *const core::ffi::c_void,
1603 a_p as *const core::ffi::c_void,
1604 y_p as *mut core::ffi::c_void,
1605 off.as_ptr(),
1606 2,
1607 64,
1608 32,
1609 stream.cu_stream() as *mut core::ffi::c_void,
1610 )
1611 };
1612 if rc != 0 {
1613 return Err(format!("f16g warmup rc={rc}").into());
1614 }
1615 self.gpu.stream().synchronize()?;
1616 Ok(())
1617 })();
1618 if let Err(e) = r {
1619 warm_err = Some(e.to_string());
1620 }
1621 });
1622 if let Some(we) = warm_err {
1623 return Err(we.into());
1624 }
1625 }
1626 let w_bytes = n_active * out_f * in_f * 2;
1627 let mut w_f16 = self.alloc_uninit::<u8>(w_bytes)?;
1628 let mut y = self.alloc_uninit::<f32>(n_pairs * out_f)?;
1629 {
1630 let stream = self.gpu.stream();
1631 let (tab_p, _g0) = table.device_ptr(&stream);
1632 let (ei_p, _g1) = ex_ids.device_ptr(&stream);
1633 let (w_p, _g2) = w_f16.device_ptr_mut(&stream);
1634 let rc = unsafe {
1635 memra_moe_f16g_dequant(
1636 tab_p as *const u64,
1637 proj,
1638 n_expert as i32,
1639 ei_p as *const i32,
1640 w_p as *mut core::ffi::c_void,
1641 in_f as i32,
1642 out_f as i32,
1643 n_active as i32,
1644 qtype,
1645 row_bytes as i64,
1646 stream.cu_stream() as *mut core::ffi::c_void,
1647 )
1648 };
1649 if rc != 0 {
1650 return Err(format!("memra_moe_f16g_dequant rc={rc}").into());
1651 }
1652 let (a_p, _g3) = act_f16.device_ptr(&stream);
1653 let (s_p, _g6) = act_scale.device_ptr(&stream);
1654 let (y_p, _g5) = y.device_ptr_mut(&stream);
1655 if sk {
1656 let max_m = ex_off_host
1657 .windows(2)
1658 .map(|w| w[1] - w[0])
1659 .max()
1660 .unwrap_or(0);
1661 let (off_p, _g7) = ex_off_dev.device_ptr(&stream);
1662 let (shape_sel, cross) = crate::moe_f16g_sk_params();
1663 let rc = unsafe {
1664 memra_moe_f16g_gemm_sk(
1665 w_p as *const core::ffi::c_void,
1666 a_p as *const core::ffi::c_void,
1667 y_p as *mut f32,
1668 s_p as *const f32,
1669 off_p as *const i32,
1670 ex_off_host.as_ptr(),
1671 n_active as i32,
1672 max_m,
1673 in_f as i32,
1674 out_f as i32,
1675 shape_sel,
1676 cross,
1677 crate::moe_f16g_tail_on() as i32,
1678 stream.cu_stream() as *mut core::ffi::c_void,
1679 )
1680 };
1681 if rc != 0 {
1682 return Err(format!("memra_moe_f16g_gemm_sk rc={rc}").into());
1683 }
1684 } else {
1685 let mut y16 = self.alloc_uninit::<u8>(n_pairs * out_f * 2)?;
1686 let (y16_p, _g4) = y16.device_ptr_mut(&stream);
1687 let rc = unsafe {
1688 memra_moe_f16g_gemm(
1689 w_p as *const core::ffi::c_void,
1690 a_p as *const core::ffi::c_void,
1691 y16_p as *mut core::ffi::c_void,
1692 ex_off_host.as_ptr(),
1693 n_active as i32,
1694 in_f as i32,
1695 out_f as i32,
1696 stream.cu_stream() as *mut core::ffi::c_void,
1697 )
1698 };
1699 if rc != 0 {
1700 return Err(format!("memra_moe_f16g_gemm rc={rc}").into());
1701 }
1702 let rc = unsafe {
1703 memra_moe_f16g_h2f_scaled(
1704 y16_p as *const core::ffi::c_void,
1705 y_p as *mut f32,
1706 s_p as *const f32,
1707 out_f as i32,
1708 n_pairs as i32,
1709 stream.cu_stream() as *mut core::ffi::c_void,
1710 )
1711 };
1712 if rc != 0 {
1713 return Err(format!("memra_moe_f16g_h2f_scaled rc={rc}").into());
1714 }
1715 }
1716 }
1717 // MODE 1 ONLY: cublasGemmGroupedBatchedEx issues through internal streams NOT ordered
1718 // with ours (round 46: NaN race, clean under sync — 205=205 MATCH). Full sync per
1719 // projection. Mode 2 (single kernel, our stream) is ordered by construction — no sync,
1720 // that is the point of this arc.
1721 if !sk {
1722 self.gpu.stream().synchronize()?;
1723 }
1724 if std::env::var("MEMRA_F16G_DEBUG").is_ok() {
1725 // FULL NaN/Inf scan of w, act (through h2f) and y — localizes the corrupt stage.
1726 let wn = n_active * out_f * in_f;
1727 let an = n_pairs * in_f;
1728 let mut wf = self.alloc_uninit::<f32>(wn)?;
1729 let mut af = self.alloc_uninit::<f32>(an)?;
1730 {
1731 let stream = self.gpu.stream();
1732 let (w_p, _a) = w_f16.device_ptr(&stream);
1733 let (a_p, _b) = act_f16.device_ptr(&stream);
1734 let (wf_p, _c) = wf.device_ptr_mut(&stream);
1735 let (af_p, _d) = af.device_ptr_mut(&stream);
1736 unsafe {
1737 memra_moe_f16g_h2f(
1738 w_p as *const core::ffi::c_void,
1739 wf_p as *mut f32,
1740 wn,
1741 stream.cu_stream() as *mut core::ffi::c_void,
1742 );
1743 memra_moe_f16g_h2f(
1744 a_p as *const core::ffi::c_void,
1745 af_p as *mut f32,
1746 an,
1747 stream.cu_stream() as *mut core::ffi::c_void,
1748 );
1749 }
1750 }
1751 let (wh, ah, yh) = (self.dtoh(&wf)?, self.dtoh(&af)?, self.dtoh(&y)?);
1752 let scan = |v: &[f32]| -> (usize, f32) {
1753 let bad = v.iter().filter(|x| !x.is_finite()).count();
1754 let mx = v
1755 .iter()
1756 .filter(|x| x.is_finite())
1757 .fold(0.0f32, |m, x| m.max(x.abs()));
1758 (bad, mx)
1759 };
1760 let (wb, wm) = scan(&wh);
1761 let (ab, am) = scan(&ah);
1762 let (yb, ym) = scan(&yh);
1763 eprintln!(
1764 "[f16g-debug] proj={proj} w: bad={wb} max={wm:.3e} | act: bad={ab} \
1765 max={am:.3e} | y: bad={yb} max={ym:.3e} (na={n_active} np={n_pairs} \
1766 in={in_f} out={out_f})"
1767 );
1768 }
1769 Ok(y)
1770 }
1771
1772 /// Raw sk grouped-GEMM entry for kernel-check ("f16g-sk" section): explicit shape/cross
1773 /// instead of the env policy. shape_sel < 0 = the round-49 grid-scan rollback arm; else
1774 /// the round-51 problem-visitor split at `cross` (1 forces all-128, i32::MAX all-32).
1775 /// tail: 1 = the deep tail (32x64x64 3-stage, lane/sk-tail-form) on sub-cross groups,
1776 /// 0 = the round-51 2-stage 32x64x32 tail.
1777 /// w_f16 = [n_active][out_f][in_f] f16 bytes, act_f16 = [n_pairs][in_f] f16 bytes.
1778 #[allow(clippy::too_many_arguments)]
1779 pub fn moe_f16g_gemm_sk_raw(
1780 &self,
1781 w_f16: &CudaSlice<u8>,
1782 act_f16: &CudaSlice<u8>,
1783 row_scale: &CudaSlice<f32>,
1784 ex_off_host: &[i32],
1785 ex_off_dev: &CudaSlice<i32>,
1786 in_f: usize,
1787 out_f: usize,
1788 n_pairs: usize,
1789 shape_sel: i32,
1790 cross: i32,
1791 tail: i32,
1792 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1793 let n_active = ex_off_host.len() - 1;
1794 let max_m = ex_off_host
1795 .windows(2)
1796 .map(|w| w[1] - w[0])
1797 .max()
1798 .unwrap_or(0);
1799 let mut y = self.alloc_uninit::<f32>(n_pairs * out_f)?;
1800 {
1801 let stream = self.gpu.stream();
1802 let (w_p, _g0) = w_f16.device_ptr(&stream);
1803 let (a_p, _g1) = act_f16.device_ptr(&stream);
1804 let (s_p, _g2) = row_scale.device_ptr(&stream);
1805 let (off_p, _g3) = ex_off_dev.device_ptr(&stream);
1806 let (y_p, _g4) = y.device_ptr_mut(&stream);
1807 let rc = unsafe {
1808 memra_moe_f16g_gemm_sk(
1809 w_p as *const core::ffi::c_void,
1810 a_p as *const core::ffi::c_void,
1811 y_p as *mut f32,
1812 s_p as *const f32,
1813 off_p as *const i32,
1814 ex_off_host.as_ptr(),
1815 n_active as i32,
1816 max_m,
1817 in_f as i32,
1818 out_f as i32,
1819 shape_sel,
1820 cross,
1821 tail,
1822 stream.cu_stream() as *mut core::ffi::c_void,
1823 )
1824 };
1825 if rc != 0 {
1826 return Err(format!("memra_moe_f16g_gemm_sk rc={rc}").into());
1827 }
1828 }
1829 Ok(y)
1830 }
1831
1832 /// Raw direct-from-quant sk grouped-GEMM entry for kernel-check ("f16g-kq-direct"):
1833 /// explicit cross/tail instead of the env policy. `table` = device u64 pointer table
1834 /// (proj-major, [n_proj][n_expert] — same contract as moe_f16_grouped), `ex_ids` =
1835 /// active-expert ids (device). Visitor forms only (the C side rejects anything else).
1836 #[allow(clippy::too_many_arguments)]
1837 pub fn moe_kq_gemm_sk_raw(
1838 &self,
1839 table: &CudaSlice<u64>,
1840 proj: i32,
1841 n_expert: usize,
1842 ex_ids: &CudaSlice<i32>,
1843 act_f16: &CudaSlice<u8>,
1844 row_scale: &CudaSlice<f32>,
1845 ex_off_host: &[i32],
1846 ex_off_dev: &CudaSlice<i32>,
1847 in_f: usize,
1848 out_f: usize,
1849 n_pairs: usize,
1850 qtype: i32,
1851 row_bytes: usize,
1852 cross: i32,
1853 tail: i32,
1854 ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1855 let n_active = ex_off_host.len() - 1;
1856 let max_m = ex_off_host
1857 .windows(2)
1858 .map(|w| w[1] - w[0])
1859 .max()
1860 .unwrap_or(0);
1861 let mut y = self.alloc_uninit::<f32>(n_pairs * out_f)?;
1862 {
1863 let stream = self.gpu.stream();
1864 let (tab_p, _g0) = table.device_ptr(&stream);
1865 let (ei_p, _g1) = ex_ids.device_ptr(&stream);
1866 let (a_p, _g2) = act_f16.device_ptr(&stream);
1867 let (s_p, _g3) = row_scale.device_ptr(&stream);
1868 let (off_p, _g4) = ex_off_dev.device_ptr(&stream);
1869 let (y_p, _g5) = y.device_ptr_mut(&stream);
1870 let rc = unsafe {
1871 memra_moe_kq_gemm_sk(
1872 tab_p as *const u64,
1873 proj,
1874 n_expert as i32,
1875 ei_p as *const i32,
1876 a_p as *const core::ffi::c_void,
1877 y_p as *mut f32,
1878 s_p as *const f32,
1879 off_p as *const i32,
1880 ex_off_host.as_ptr(),
1881 n_active as i32,
1882 max_m,
1883 in_f as i32,
1884 out_f as i32,
1885 qtype,
1886 cross,
1887 tail,
1888 row_bytes as i64,
1889 stream.cu_stream() as *mut core::ffi::c_void,
1890 )
1891 };
1892 if rc != 0 {
1893 return Err(format!("memra_moe_kq_gemm_sk rc={rc}").into());
1894 }
1895 }
1896 Ok(y)
1897 }
1898
1899 /// Raw dequant-workspace entry for kernel-check: dequant the active experts' rows to a
1900 /// fresh f16 workspace via the same kernel `moe_f16_grouped` uses (the direct loaders'
1901 /// bitwise reference).
1902 pub fn moe_f16g_dequant_raw(
1903 &self,
1904 table: &CudaSlice<u64>,
1905 proj: i32,
1906 n_expert: usize,
1907 ex_ids: &CudaSlice<i32>,
1908 in_f: usize,
1909 out_f: usize,
1910 n_active: usize,
1911 qtype: i32,
1912 row_bytes: usize,
1913 ) -> Result<CudaSlice<u8>, Box<dyn std::error::Error>> {
1914 let mut w_f16 = self.alloc_uninit::<u8>(n_active * out_f * in_f * 2)?;
1915 {
1916 let stream = self.gpu.stream();
1917 let (tab_p, _g0) = table.device_ptr(&stream);
1918 let (ei_p, _g1) = ex_ids.device_ptr(&stream);
1919 let (w_p, _g2) = w_f16.device_ptr_mut(&stream);
1920 let rc = unsafe {
1921 memra_moe_f16g_dequant(
1922 tab_p as *const u64,
1923 proj,
1924 n_expert as i32,
1925 ei_p as *const i32,
1926 w_p as *mut core::ffi::c_void,
1927 in_f as i32,
1928 out_f as i32,
1929 n_active as i32,
1930 qtype,
1931 row_bytes as i64,
1932 stream.cu_stream() as *mut core::ffi::c_void,
1933 )
1934 };
1935 if rc != 0 {
1936 return Err(format!("memra_moe_f16g_dequant rc={rc}").into());
1937 }
1938 }
1939 Ok(w_f16)
1940 }
1941}