Skip to main content

qualia_core_db/gguf_bridge/
mod.rs

1//! Model-inference runtime (honest module name: `crate::inference_runtime`).
2//!
3//! This is the **runtime** that runs model inference — not an "AI engine". It does two ordinary
4//! systems jobs: (1) **reads the GGUF weight file format** (tensors memory-mapped via `memmap2`,
5//! zero heap copy) and (2) **dispatches the tensor program to a GPU backend** (DirectML 1.15 on
6//! Windows x64; wgpu/WGSL — Vulkan/Metal/WebGPU — elsewhere).
7//!
8//! The *mathematics* of inference is not here and is not proprietary: it lives in `crate::solvers`
9//! as named STEM — GEMM (`linear_algebra::gemm`), activations/softmax/normalization
10//! (`activation`), attention (`attention`), RoPE (`rope`), the SwiGLU FFN (`feed_forward`) — and
11//! each kernel in this crate is proven equal to that library definition (the `*_stem_parity_tests`
12//! and `substrate_parity_tests`). What remains here is plumbing: GPU command encoding, the KV
13//! cache, weight loading, the autoregressive loop, and the GGUF/dequant codec.
14
15// pub(crate) so the concern submodules (gemm / ffn / attention / output / embedding / mc8_wasm …)
16// inherit these via `use super::*` — keeps the impl split a pure structural move.
17pub(crate) use crate::gguf_sharder::GgufTensorInfo;
18pub(crate) use crate::NQuin;
19use log;
20#[cfg(not(target_arch = "wasm32"))]
21pub(crate) use memmap2::MmapOptions;
22pub(crate) use std::sync::Arc;
23
24pub use crate::ggml_quants::{fetch_token_embedding, ExecutionError};
25
26/// Dequantize a mmap embedding row into caller-supplied `out` (no heap allocation).
27pub fn dequantize_token_embedding_into(
28    raw: &[u8],
29    tensor: &GgufTensorInfo,
30    out: &mut [f32],
31) -> Result<usize, ExecutionError> {
32    let n_embd = tensor.dims[0] as usize;
33    if out.len() < n_embd {
34        return Err(ExecutionError::MmapBounds);
35    }
36    crate::ggml_quants::dequantize_row_into(raw, tensor.ggml_type, n_embd, out).map_err(|e| match e
37    {
38        crate::ggml_quants::GgmlDequantError::UnsupportedType => ExecutionError::UnsupportedType,
39        crate::ggml_quants::GgmlDequantError::BufferTooSmall
40        | crate::ggml_quants::GgmlDequantError::TruncatedInput => ExecutionError::MmapBounds,
41    })
42}
43
44/// Represents a Q4_K Quantized or standard float Tensor mapped from a monolithic GGUF file.
45#[derive(Debug, Clone)]
46pub struct QTensor {
47    pub shape: Vec<usize>,
48    pub byte_offset: u64,
49    pub is_quantized_q4_k: bool,
50}
51
52impl QTensor {
53    pub fn new(shape: Vec<usize>, byte_offset: u64, is_quantized_q4_k: bool) -> Self {
54        Self {
55            shape,
56            byte_offset,
57            is_quantized_q4_k,
58        }
59    }
60
61    /// Maps the exact bytes from the GGUF using the 60-bit pointer.
62    pub fn map_from_pointer(quin: &NQuin) -> Option<Self> {
63        use crate::QuinPointerExt;
64
65        let flag = quin.extract_modality_flag();
66        if flag != crate::MODALITY_FLAG_LLM_TENSOR {
67            return None; // Not an LLM tensor
68        }
69
70        let offset = quin.extract_byte_offset();
71
72        // Mock parsing the GGUF header at the offset to find shape and quantization
73        // For demonstration, we assume a Q4_K tensor representation.
74        Some(Self::new(vec![4096, 4096], offset, true))
75    }
76}
77
78// ── gguf_bridge library submodules (extracted from the former 9k-line monolith) ──
79// GPU uniform-buffer param structs (EmbeddingGpuParams / GemmGpuParams / AttentionGpuParams /
80// ElemGpuParams + ELEM_OP_* codes) and the quant-support gates now live in dedicated files.
81mod gpu_params;
82mod quant_support;
83pub(crate) use gpu_params::*;
84pub(crate) use quant_support::*;
85
86/// KV attention bitmask words uploaded to `fused_attention.wgsl` binding 5.
87pub const KV_ATTENTION_MASK_WORDS: usize = crate::compute_universe::KV_ATTENTION_MASK_WORDS;
88
89// ElemGpuParams + ELEM_OP_* codes moved to `gpu_params` (see submodule declarations above).
90
91/// FNV-1a hash for bind group cache keys.
92#[cfg(target_arch = "wasm32")]
93#[inline]
94pub(crate) fn mc8_bg_hash(parts: &[u64]) -> u64 {
95    let mut h: u64 = 0xcbf29ce484222325;
96    for &p in parts {
97        h ^= p;
98        h = h.wrapping_mul(0x100000001b3);
99    }
100    h
101}
102
103/// MC8 Part 3s: WebGPU dynamic uniform offsets must be multiples of 256 bytes.
104#[cfg(target_arch = "wasm32")]
105pub(crate) const MC8_UNIFORM_ALIGN: usize = 256;
106#[cfg(target_arch = "wasm32")]
107pub(crate) const MC8_MAX_GEMM_UNIFORM_SLOTS: usize = 8;
108#[cfg(target_arch = "wasm32")]
109pub(crate) const MC8_MAX_ELEM_UNIFORM_SLOTS: usize = 8;
110#[cfg(target_arch = "wasm32")]
111pub(crate) const MC8_MAX_ATTN_UNIFORM_SLOTS: usize = 8;
112#[cfg(target_arch = "wasm32")]
113pub(crate) const MC8_MAX_ELEM_UNIFORM_LAYER_SLOTS: usize = MC8_MAX_ELEM_UNIFORM_SLOTS;
114/// MC8 Part 3v / Phase 5.4: layers encoded into one submit batch. Sizes the per-layer uniform
115/// buffers (slots_per_layer × this) and is the decode forward's chunk size — 64 → the whole
116/// ≤64-layer forward is a single submit. Per-chunk flush + reset handles deeper models.
117#[cfg(target_arch = "wasm32")]
118pub(crate) const MC8_LAYERS_PER_ENCODER: u32 = 64;
119/// Uniform slots reserved per layer within a chunk (must cover K/V/Q + tail).
120#[cfg(target_arch = "wasm32")]
121pub(crate) const MC8_ATTN_SLOTS_PER_LAYER: usize = 4;
122#[cfg(target_arch = "wasm32")]
123pub(crate) const MC8_ELEM_SLOTS_PER_LAYER: usize = 6;
124#[cfg(target_arch = "wasm32")]
125pub(crate) const MC8_GEMM_SLOTS_PER_LAYER: usize = 8; // o, gate, up, down + Phase 5.5 Q/K/V projection
126#[cfg(target_arch = "wasm32")]
127pub(crate) const MC8_MAX_ATTN_UNIFORM_CHUNK_SLOTS: usize =
128    MC8_ATTN_SLOTS_PER_LAYER * MC8_LAYERS_PER_ENCODER as usize;
129#[cfg(target_arch = "wasm32")]
130pub(crate) const MC8_MAX_ELEM_UNIFORM_CHUNK_SLOTS: usize =
131    MC8_ELEM_SLOTS_PER_LAYER * MC8_LAYERS_PER_ENCODER as usize;
132#[cfg(target_arch = "wasm32")]
133pub(crate) const MC8_MAX_GEMM_UNIFORM_CHUNK_SLOTS: usize =
134    MC8_GEMM_SLOTS_PER_LAYER * MC8_LAYERS_PER_ENCODER as usize;
135
136/// Part 3v: absolute uniform slot cursors within one encoder chunk.
137#[cfg(target_arch = "wasm32")]
138pub(crate) struct Mc8ChunkUniformCursors {
139    attn: usize,
140    elem: usize,
141    gemm: usize,
142}
143
144/// MC8 Part 3t: disjoint weight staging — eliminates `write_buffer` races within one layer submit.
145#[cfg(target_arch = "wasm32")]
146#[derive(Clone, Copy, Debug, PartialEq, Eq)]
147pub(crate) enum Mc8WeightRole {
148    AttnK,
149    AttnV,
150    AttnQ,
151    OProj,
152    Gate,
153    Up,
154    Down,
155}
156
157#[cfg(target_arch = "wasm32")]
158impl Mc8WeightRole {
159    /// Stable index into the per-role resident stride table (`mc8_weight_role_stride`).
160    #[inline]
161    pub(crate) fn idx(self) -> usize {
162        match self {
163            Mc8WeightRole::AttnK => 0,
164            Mc8WeightRole::AttnV => 1,
165            Mc8WeightRole::AttnQ => 2,
166            Mc8WeightRole::OProj => 3,
167            Mc8WeightRole::Gate => 4,
168            Mc8WeightRole::Up => 5,
169            Mc8WeightRole::Down => 6,
170        }
171    }
172}
173
174/// One buffer per GEMM role so mid-layer weight uploads never clobber in-flight dispatches.
175#[cfg(target_arch = "wasm32")]
176pub(crate) struct Mc8WeightArenaBufs {
177    qkv_k: wgpu::Buffer,
178    qkv_v: wgpu::Buffer,
179    qkv_q: wgpu::Buffer,
180    o_proj: wgpu::Buffer,
181    gate: wgpu::Buffer,
182    up: wgpu::Buffer,
183    down: wgpu::Buffer,
184}
185
186/// Part 3u: dynamic offsets for one full prefill layer (staged before encoder dispatches).
187#[cfg(target_arch = "wasm32")]
188pub(crate) struct Mc8PrefillLayerUniforms {
189    k_off: u32,
190    v_off: u32,
191    q_off: u32,
192    attn_norm_elem_off: Option<u32>,
193    off_o: Option<u32>,
194    off_attn_res: u32,
195    off_ffn_norm: Option<u32>,
196    off_gate: u32,
197    off_up: u32,
198    off_silu: u32,
199    off_down: u32,
200    off_ffn_res: u32,
201    /// Phase 5.5: dynamic offsets for the Q/K/V projection GEMMs (parallel kernel).
202    off_q_gemm: u32,
203    off_k_gemm: u32,
204    off_v_gemm: u32,
205}
206
207/// Strided work-buffer geometry shared by layer dispatches.
208#[cfg(target_arch = "wasm32")]
209pub(crate) struct Mc8PrefillLayerGeom {
210    row_stride: usize,
211    row_stride_u32: u32,
212    batch_in_bytes: wgpu::BufferAddress,
213    work_span_bytes: wgpu::BufferAddress,
214    emb_bytes: wgpu::BufferAddress,
215    n_embd_bytes: wgpu::BufferAddress,
216    slot_o: wgpu::BufferAddress,
217    slot_gate: wgpu::BufferAddress,
218    slot_up: wgpu::BufferAddress,
219    slot_save: wgpu::BufferAddress,
220    slot_scratch_half: wgpu::BufferAddress,
221    slot_o_f: u32,
222    slot_save_f: u32,
223    slot_gate_f: u32,
224    slot_up_f: u32,
225    slot_scratch_half_f: u32,
226}
227
228/// Stack arena for batched uniform uploads (one `write_buffer` per layer section).
229#[cfg(target_arch = "wasm32")]
230pub(crate) struct Mc8UniformArena {
231    bytes: [u8; MC8_MAX_GEMM_UNIFORM_SLOTS * MC8_UNIFORM_ALIGN],
232    slots: usize,
233}
234
235/// Part 3u: larger elem arena for full-layer super-staging.
236#[cfg(target_arch = "wasm32")]
237pub(crate) struct Mc8ElemUniformArena {
238    bytes: [u8; MC8_MAX_ELEM_UNIFORM_LAYER_SLOTS * MC8_UNIFORM_ALIGN],
239    slots: usize,
240}
241
242#[cfg(target_arch = "wasm32")]
243pub(crate) struct Mc8AttnUniformArena {
244    bytes: [u8; MC8_MAX_ATTN_UNIFORM_SLOTS * MC8_UNIFORM_ALIGN],
245    slots: usize,
246}
247
248#[cfg(target_arch = "wasm32")]
249impl Mc8ElemUniformArena {
250    pub(crate) fn push<T: bytemuck::Pod>(&mut self, value: &T) -> u32 {
251        debug_assert!(std::mem::size_of::<T>() <= MC8_UNIFORM_ALIGN);
252        debug_assert!(self.slots < MC8_MAX_ELEM_UNIFORM_LAYER_SLOTS);
253        let byte_off = self.slots * MC8_UNIFORM_ALIGN;
254        self.slots += 1;
255        self.bytes[byte_off..byte_off + std::mem::size_of::<T>()]
256            .copy_from_slice(bytemuck::bytes_of(value));
257        byte_off as u32
258    }
259
260    pub(crate) fn upload(&self, queue: &wgpu::Queue, buf: &wgpu::Buffer) {
261        if self.slots == 0 {
262            return;
263        }
264        queue.write_buffer(buf, 0, &self.bytes[..self.slots * MC8_UNIFORM_ALIGN]);
265    }
266
267    pub(crate) fn upload_at(&self, queue: &wgpu::Queue, buf: &wgpu::Buffer, base_slot: usize) {
268        if self.slots == 0 {
269            return;
270        }
271        if base_slot == 0 {
272            self.upload(queue, buf);
273            return;
274        }
275        let byte_off = (base_slot * MC8_UNIFORM_ALIGN) as wgpu::BufferAddress;
276        queue.write_buffer(buf, byte_off, &self.bytes[..self.slots * MC8_UNIFORM_ALIGN]);
277    }
278}
279
280#[cfg(target_arch = "wasm32")]
281impl Mc8AttnUniformArena {
282    pub(crate) fn push<T: bytemuck::Pod>(&mut self, value: &T) -> u32 {
283        debug_assert!(std::mem::size_of::<T>() <= MC8_UNIFORM_ALIGN);
284        debug_assert!(self.slots < MC8_MAX_ATTN_UNIFORM_SLOTS);
285        let byte_off = self.slots * MC8_UNIFORM_ALIGN;
286        self.slots += 1;
287        self.bytes[byte_off..byte_off + std::mem::size_of::<T>()]
288            .copy_from_slice(bytemuck::bytes_of(value));
289        byte_off as u32
290    }
291
292    pub(crate) fn upload(&self, queue: &wgpu::Queue, buf: &wgpu::Buffer) {
293        if self.slots == 0 {
294            return;
295        }
296        queue.write_buffer(buf, 0, &self.bytes[..self.slots * MC8_UNIFORM_ALIGN]);
297    }
298
299    pub(crate) fn upload_at(&self, queue: &wgpu::Queue, buf: &wgpu::Buffer, base_slot: usize) {
300        if self.slots == 0 {
301            return;
302        }
303        if base_slot == 0 {
304            self.upload(queue, buf);
305            return;
306        }
307        let byte_off = (base_slot * MC8_UNIFORM_ALIGN) as wgpu::BufferAddress;
308        queue.write_buffer(buf, byte_off, &self.bytes[..self.slots * MC8_UNIFORM_ALIGN]);
309    }
310}
311
312#[cfg(target_arch = "wasm32")]
313impl Mc8UniformArena {
314    pub(crate) fn push<T: bytemuck::Pod>(&mut self, value: &T) -> u32 {
315        debug_assert!(std::mem::size_of::<T>() <= MC8_UNIFORM_ALIGN);
316        debug_assert!(self.slots < MC8_MAX_GEMM_UNIFORM_SLOTS);
317        let byte_off = self.slots * MC8_UNIFORM_ALIGN;
318        self.slots += 1;
319        let dst = &mut self.bytes[byte_off..byte_off + std::mem::size_of::<T>()];
320        dst.copy_from_slice(bytemuck::bytes_of(value));
321        byte_off as u32
322    }
323
324    pub(crate) fn upload(&self, queue: &wgpu::Queue, buf: &wgpu::Buffer) {
325        if self.slots == 0 {
326            return;
327        }
328        queue.write_buffer(buf, 0, &self.bytes[..self.slots * MC8_UNIFORM_ALIGN]);
329    }
330
331    pub(crate) fn upload_at(&self, queue: &wgpu::Queue, buf: &wgpu::Buffer, base_slot: usize) {
332        if self.slots == 0 {
333            return;
334        }
335        let byte_off = (base_slot * MC8_UNIFORM_ALIGN) as wgpu::BufferAddress;
336        queue.write_buffer(buf, byte_off, &self.bytes[..self.slots * MC8_UNIFORM_ALIGN]);
337    }
338}
339
340#[cfg(target_arch = "wasm32")]
341impl Mc8ChunkUniformCursors {
342    pub(crate) fn reset(&mut self) {
343        self.attn = 0;
344        self.elem = 0;
345        self.gemm = 0;
346    }
347
348    pub(crate) fn attn_base_byte(&self) -> u32 {
349        (self.attn * MC8_UNIFORM_ALIGN) as u32
350    }
351
352    pub(crate) fn elem_base_byte(&self) -> u32 {
353        (self.elem * MC8_UNIFORM_ALIGN) as u32
354    }
355
356    pub(crate) fn gemm_base_byte(&self) -> u32 {
357        (self.gemm * MC8_UNIFORM_ALIGN) as u32
358    }
359}
360
361/// MC8: accumulates compute passes; submit + map_async only at pipeline boundary.
362#[cfg(target_arch = "wasm32")]
363pub(crate) struct WasmGpuPipeline {
364    encoder: wgpu::CommandEncoder,
365}
366
367// ggml_gpu_quant_supported / ggml_gpu_attention_shader_supported / ggml_gpu_gemm_supported moved to
368// the `quant_support` submodule (declared above; re-exported via `pub(crate) use quant_support::*`).
369
370/// Await `map_async` without `poll(Wait)` — yields to the browser event loop (MC6).
371#[cfg(target_arch = "wasm32")]
372pub(crate) async fn await_wgpu_map(slice: wgpu::BufferSlice<'_>) -> bool {
373    let (tx, rx) = futures_channel::oneshot::channel();
374    slice.map_async(wgpu::MapMode::Read, move |r| {
375        let _ = tx.send(r);
376    });
377    matches!(rx.await, Ok(Ok(())))
378}
379
380#[cfg(target_arch = "wasm32")]
381impl WasmGpuPipeline {
382    pub(crate) fn begin(engine: &QTensorEngine) -> Self {
383        Self {
384            encoder: engine
385                .device()
386                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
387                    label: Some("MC8FusedEncoder"),
388                }),
389        }
390    }
391
392    pub(crate) fn finish(self) -> wgpu::CommandBuffer {
393        self.encoder.finish()
394    }
395}
396
397// WASM-only MC8 GPU engine methods (resident weight arena, fused-encoder prefill/decode, async
398// readback) carved into the `mc8_wasm` submodule. cfg-gated so native never compiles it.
399#[cfg(target_arch = "wasm32")]
400mod mc8_wasm;
401
402/// Hard context ceiling — sized to keep KV arena under the 512MB RAM floor (Gemma 42L).
403pub const MAX_CONTEXT_WINDOW: u32 = 1024;
404/// Maximum bytes for the static KV arena (load-time allocation only).
405pub const KV_CACHE_MAX_BYTES: usize = 448 * 1024 * 1024;
406
407/// Static ring-buffer KV layout: `[layer][slot][K | V]` in f32, OR (W5a int8 mode) packed int8 +
408/// per-(slot,kv_head) f32 scale in the same 4-byte-element buffer. `total_f32_elems` counts 4-byte
409/// slots either way (u32/f32 share the size), so the allocation math is unchanged.
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411pub struct KvCacheLayout {
412    pub max_context: u32,
413    pub n_layer: u32,
414    pub n_kv_head: u32,
415    pub head_dim: u32,
416    pub slot_kv_elems: u32,
417    pub layer_stride: u32,
418    pub total_f32_elems: usize,
419    /// W5a: true ⇒ the arena is int8-quantized (K/V stored as packed i8 lanes + one f32 scale per
420    /// (slot, kv_head), K then V). `layer_stride` is then the int8 slot layout, not the f32 one.
421    pub int8: bool,
422    /// W5b Phase 4b: `> 0` ⇒ the arena stores k-sparse **dictionary codes** (`dict_k` code-words per
423    /// K/V vector — each word packs `u16 atom-index | f16 coefficient`), reconstructed in the attention
424    /// shader. Mutually exclusive with `int8` (dict mode wins). `0` ⇒ f32/int8 as above.
425    pub dict_k: u32,
426    /// W5b Phase 4b: dictionary size (atoms) — the atoms live in each layer's arena AFTER the code
427    /// region (`[K atoms n_atoms×head_dim][V atoms …]`), so the per-layer binding covers them.
428    pub dict_n_atoms: u32,
429}
430
431impl KvCacheLayout {
432    pub fn from_hyperparams(h: &crate::gguf_sharder::GgufHyperparams) -> Option<Self> {
433        // W5b Phase 4b: sparse-dictionary KV takes precedence over int8/f32 — but only when its toggle is
434        // on AND a certified dictionary is installed whose head_dim matches this model. Otherwise it
435        // transparently falls back (dict_k = 0).
436        #[cfg(not(target_arch = "wasm32"))]
437        let (dict_k, dict_n_atoms) = if crate::llm_bench::kv_dict_enabled() {
438            crate::kv_dict_runtime::installed_meta()
439                .filter(|&(_, hd, _)| hd == h.head_dim() as usize)
440                .map(|(k, _, na)| (k as u32, na as u32))
441                .unwrap_or((0, 0))
442        } else {
443            (0, 0)
444        };
445        #[cfg(target_arch = "wasm32")]
446        let (dict_k, dict_n_atoms) = (0u32, 0u32);
447
448        // W5a int8 KV is a native decode-path optimization, gated behind its own toggle and only when
449        // head_dim packs cleanly into u32 lanes. WASM always uses the f32 layout.
450        #[cfg(not(target_arch = "wasm32"))]
451        let want_int8 = dict_k == 0
452            && crate::llm_bench::kv_int8_enabled()
453            && (h.head_dim() % 4 == 0)
454            && h.head_dim() > 0;
455        #[cfg(target_arch = "wasm32")]
456        let want_int8 = false;
457        Self::from_hyperparams_mode(h, want_int8, dict_k, dict_n_atoms)
458    }
459
460    fn from_hyperparams_mode(
461        h: &crate::gguf_sharder::GgufHyperparams,
462        int8: bool,
463        dict_k: u32,
464        dict_n_atoms: u32,
465    ) -> Option<Self> {
466        let n_layer = h.n_layer;
467        let n_kv_head = h.effective_n_kv_head();
468        let head_dim = h.head_dim();
469        if n_layer == 0 || n_kv_head == 0 || head_dim == 0 {
470            return None;
471        }
472        // dict mode wins over int8.
473        let int8 = int8 && dict_k == 0;
474        let slot_kv_elems = n_kv_head * head_dim;
475        // f32:  per slot = 2·n_kv_head·head_dim 4-byte elems (K then V).
476        // int8: per slot = 2·n_kv_head·(1 scale + head_dim/4 packed words), K then V — ~3.8× smaller.
477        // dict: per slot = 2·n_kv_head·dict_k code-words (K then V) — each word = u16 index | f16 coeff.
478        let layer_stride = if dict_k > 0 {
479            // codes ([max_context][2 streams][n_kv_head][dict_k]) + the layer's dictionary atoms
480            // ([2 streams][dict_n_atoms][head_dim]) resident in the same slice for the shader.
481            MAX_CONTEXT_WINDOW * 2 * n_kv_head * dict_k + 2 * dict_n_atoms * head_dim
482        } else if int8 {
483            MAX_CONTEXT_WINDOW * 2 * n_kv_head * (1 + head_dim / 4)
484        } else {
485            MAX_CONTEXT_WINDOW * slot_kv_elems * 2
486        };
487        let total = (n_layer as usize).checked_mul(layer_stride as usize)?;
488        let bytes = total.checked_mul(std::mem::size_of::<f32>())?;
489        if bytes > KV_CACHE_MAX_BYTES {
490            return None;
491        }
492        Some(Self {
493            max_context: MAX_CONTEXT_WINDOW,
494            n_layer,
495            n_kv_head,
496            head_dim,
497            slot_kv_elems,
498            layer_stride,
499            total_f32_elems: total,
500            int8,
501            dict_k,
502            dict_n_atoms,
503        })
504    }
505
506    /// Derive the dense f32 device layout used by native CUDA kernels without changing the host
507    /// cache representation. Host int8/dictionary compression and the CUDA execution arena are
508    /// independent storage decisions.
509    #[cfg(not(target_arch = "wasm32"))]
510    pub(crate) fn dense_device_layout(self) -> Option<Self> {
511        let slot_kv_elems = self.n_kv_head.checked_mul(self.head_dim)?;
512        let layer_stride = self
513            .max_context
514            .checked_mul(slot_kv_elems)?
515            .checked_mul(2)?;
516        let total_f32_elems = (self.n_layer as usize).checked_mul(layer_stride as usize)?;
517        total_f32_elems
518            .checked_mul(std::mem::size_of::<f32>())
519            .filter(|bytes| *bytes <= KV_CACHE_MAX_BYTES)?;
520        Some(Self {
521            slot_kv_elems,
522            layer_stride,
523            total_f32_elems,
524            int8: false,
525            dict_k: 0,
526            dict_n_atoms: 0,
527            ..self
528        })
529    }
530
531    #[inline]
532    pub fn ring_slot(&self, token_idx: u32) -> u32 {
533        token_idx % self.max_context
534    }
535
536    #[inline]
537    pub fn k_index(&self, layer: u32, slot: u32, kv_head: u32, dim: u32) -> usize {
538        let base = layer as usize * self.layer_stride as usize
539            + slot as usize * self.slot_kv_elems as usize * 2;
540        base + kv_head as usize * self.head_dim as usize + dim as usize
541    }
542
543    #[inline]
544    pub fn v_index(&self, layer: u32, slot: u32, kv_head: u32, dim: u32) -> usize {
545        let k_base = layer as usize * self.layer_stride as usize
546            + slot as usize * self.slot_kv_elems as usize * 2;
547        let v_off = self.n_kv_head as usize * self.head_dim as usize;
548        k_base + v_off + kv_head as usize * self.head_dim as usize + dim as usize
549    }
550
551    /// W5b Phase 4b (dict mode): word offset of the `i`-th code word (`0..dict_k`) for the K
552    /// (`k_not_v = true`) or V vector of `(layer, slot, kv_head)`. Each code word packs
553    /// `u16 atom-index (high 16) | f16 coefficient (low 16)`. Slot layout mirrors f32 (K region then V
554    /// region), but each vector is `dict_k` words instead of `head_dim` floats.
555    #[inline]
556    pub fn code_index(&self, layer: u32, slot: u32, kv_head: u32, k_not_v: bool, i: u32) -> usize {
557        let dk = self.dict_k as usize;
558        let per_slot = 2 * self.n_kv_head as usize * dk;
559        let base = layer as usize * self.layer_stride as usize + slot as usize * per_slot;
560        let stream_off = if k_not_v {
561            0
562        } else {
563            self.n_kv_head as usize * dk
564        };
565        base + stream_off + kv_head as usize * dk + i as usize
566    }
567}
568
569/// Max GEMM row/column for stack buffers and reusable GPU staging (Gemma 4 FFN = 4×2560).
570const MAX_STACK_GEMM_DIM: usize = 10240;
571const MAX_STACK_GEMM_OUT: usize = MAX_STACK_GEMM_DIM;
572const MAX_STACK_GEMM_IN: usize = MAX_STACK_GEMM_DIM;
573/// Stack scratch for pre-norm hidden (SmolLM2 n_embd=960; cap supports Gemma-class models).
574const MAX_HIDDEN_DIM: usize = 4096;
575/// RMSNorm epsilon when GGUF KV does not expose `rms_norm_eps` (Llama/SmolLM default).
576pub(crate) const RMS_NORM_EPS: f32 = 1e-5;
577/// Prompt tokens per prefill GPU batch (stack + staging footprint = `emb_dim ×` this).
578pub const PREFILL_CHUNK_SIZE: usize = 64;
579/// Per-token KV masks uploaded for batched Q-SDPA (`PREFILL_CHUNK_SIZE × mask words`).
580const MAX_ATTN_MASK_UPLOAD_WORDS: usize = PREFILL_CHUNK_SIZE * KV_ATTENTION_MASK_WORDS;
581/// Max stacked embedding floats in a prefill chunk (`MAX_STACK_GEMM_IN × 64`).
582pub const MAX_PREFILL_BATCH_FLOATS: usize = MAX_STACK_GEMM_IN * PREFILL_CHUNK_SIZE;
583/// `llm_agent` stack chunk buffer (Gemma 2560 × 64).
584pub const PREFILL_CHUNK_STACK_FLOATS: usize = 2560 * PREFILL_CHUNK_SIZE;
585/// wgpu default max buffer size on many drivers (256 MiB).
586const MAX_WGPU_WEIGHT_STAGING: usize = 64 * 1024 * 1024;
587/// Vocabulary projection rows per chunked logits sweep.
588/// 10240 is the native GEMM output-buffer ceiling and a 256-row multiple, so
589/// resident logits chunk offsets stay storage-binding aligned while the current
590/// 49k-vocab model drops from six output chunks/token to five.
591pub const VOCAB_CHUNK_ROWS: usize = MAX_STACK_GEMM_OUT;
592
593/// Streaming argmax result across chunked vocabulary projection.
594#[derive(Debug, Clone, Copy, PartialEq)]
595pub struct StreamingArgmaxResult {
596    pub best_token_id: u32,
597    pub max_logit: f32,
598}
599
600#[derive(Debug, Clone, Copy, PartialEq, Eq)]
601pub struct GgufLoadReport {
602    pub mapped_bytes: u64,
603    pub tensor_data_offset: u64,
604    pub n_layer: u32,
605    pub n_head: u32,
606    pub n_kv_head: u32,
607    pub max_tensor_bytes: usize,
608    pub kv_cache_bytes: u64,
609    pub directml_enabled: bool,
610}
611
612// CPU numeric kernels + pre-norm helpers (bytes_to_gib / scrub_f32_volatile / update_streaming_argmax
613// [+sieved] / relu / silu / add_residual / rms_norm / dequant_norm_row_into / prepare_pre_norm_input)
614// moved to the `cpu_ops` submodule (declared below; re-exported via `pub(crate) use cpu_ops::*`).
615mod cpu_ops;
616pub(crate) use cpu_ops::*;
617#[cfg(not(target_arch = "wasm32"))]
618mod pipeline_cache;
619/// Prepared CPU execution floor for browser WASM. This backend owns no wgpu
620/// objects and remains available when the browser exposes no WebGPU adapter.
621pub mod wasm_cpu;
622#[cfg(not(target_arch = "wasm32"))]
623pub(crate) use pipeline_cache::*;
624
625// Concern submodules — each holds an `impl QTensorEngine` block for one hot-path area. Methods are
626// pub(crate) so they call across modules freely; types/imports arrive via each file's `use super::*`.
627mod async_dispatch;
628mod attention;
629#[cfg(target_arch = "wasm32")]
630mod browser;
631#[cfg(all(not(target_arch = "wasm32"), feature = "cuda"))]
632mod cuda_decode_plan;
633/// Hard cap on the KV context window a decode plan may request. Declared here rather than in the
634/// `cuda`-gated plan module because the raw-decode harness and the mega-pass guard validate
635/// against it on every target, CUDA or not.
636#[cfg(not(target_arch = "wasm32"))]
637pub(crate) const MAX_CUDA_CONTEXT_WINDOW: u32 = 4096;
638mod embedding;
639mod ffn;
640mod forward;
641mod gemm;
642mod init;
643mod load;
644mod output;
645#[cfg(not(target_arch = "wasm32"))]
646mod prefill_arena;
647mod prefill_async;
648#[cfg(not(target_arch = "wasm32"))]
649mod resident_decode;
650mod verify_arena;
651/// Cooperative browser yields + init-status for WASM LLM boot (phones).
652#[cfg(target_arch = "wasm32")]
653pub(crate) mod wasm_yield;
654
655/// MC8 pt3e: max abs error over the first `n` elements.
656#[cfg(all(target_arch = "wasm32", feature = "wasm-llm-diagnostics"))]
657fn probe_max_abs_diff(a: &[f32], b: &[f32], n: usize) -> f32 {
658    let n = n.min(a.len()).min(b.len());
659    let mut m = 0.0f32;
660    for i in 0..n {
661        m = m.max((a[i] - b[i]).abs());
662    }
663    m
664}
665
666#[cfg(all(target_arch = "wasm32", feature = "wasm-llm-diagnostics"))]
667fn probe_log_diff(phase: &str, cpu: &[f32], gpu: &[f32], n: usize) {
668    let n = n.min(8).min(cpu.len()).min(gpu.len());
669    if n == 0 {
670        return;
671    }
672    let err = probe_max_abs_diff(cpu, gpu, n);
673    wlog(&format!(
674        "[MC8 L0 diff] {phase}: cpu[0]={:.6} gpu[0]={:.6} max_abs_err={:.6}",
675        cpu[0], gpu[0], err
676    ));
677}
678
679#[cfg(all(target_arch = "wasm32", feature = "wasm-llm-diagnostics"))]
680fn probe_log_mid_diff(phase: &str, cpu: &[f32], gpu: &[f32], n: usize) {
681    let n = n.min(8).min(cpu.len()).min(gpu.len());
682    if n == 0 {
683        return;
684    }
685    let err = probe_max_abs_diff(cpu, gpu, n);
686    wlog(&format!(
687        "[MC8 L0 mid] {phase}: cpu[0]={:.6} gpu[0]={:.6} max_abs_err={:.6}",
688        cpu[0], gpu[0], err
689    ));
690}
691
692#[cfg(all(target_arch = "wasm32", feature = "wasm-llm-diagnostics"))]
693fn probe_log_ffn_diff(phase: &str, cpu: &[f32], gpu: &[f32], n: usize) {
694    let n = n.min(8).min(cpu.len()).min(gpu.len());
695    if n == 0 {
696        return;
697    }
698    let err = probe_max_abs_diff(cpu, gpu, n);
699    wlog(&format!(
700        "[MC8 L0 ffn] {phase}: cpu[0]={:.6} gpu[0]={:.6} max_abs_err={:.6}",
701        cpu[0], gpu[0], err
702    ));
703}
704
705/// MC8 pt3g: CPU SwiGLU stages from post-attn hidden @ L0.
706#[cfg(all(target_arch = "wasm32", feature = "wasm-llm-diagnostics"))]
707async fn mc8_cpu_l0_ffn_stages(
708    engine: &QTensorEngine,
709    index: &crate::gguf_sharder::GgufTensorIndex,
710    mmap: &[u8],
711    layout: &KvCacheLayout,
712    hidden_cpu: &[f32],
713    n_embd: usize,
714    token_idx: u32,
715    post_attn: &mut [f32],
716    ffn_input: &mut [f32],
717    gate: &mut [f32],
718    up: &mut [f32],
719    swiglu: &mut [f32],
720    down: &mut [f32],
721) -> Option<usize> {
722    let tensors = index.get_layer_tensors(0);
723    let mut attn_out = [0f32; MAX_HIDDEN_DIM];
724    let q_dim = mc8_cpu_l0_attn_out(
725        engine,
726        index,
727        mmap,
728        layout,
729        hidden_cpu,
730        n_embd,
731        token_idx,
732        &mut attn_out,
733    )
734    .await?;
735    let out_info = tensors.attn_output.as_ref()?;
736    let o_raw =
737        crate::ggml_quants::fetch_tensor_bytes(mmap, index.tensor_data_start, out_info).ok()?;
738    let (o_in, _) = QTensorEngine::matmul_dims(out_info);
739    if o_in > q_dim {
740        return None;
741    }
742    let mut o_proj = [0f32; MAX_HIDDEN_DIM];
743    if !stack_gemm_quant(
744        o_raw,
745        out_info,
746        &attn_out[..q_dim],
747        &mut o_proj[..n_embd],
748        o_in,
749        n_embd,
750    ) {
751        return None;
752    }
753    for i in 0..n_embd {
754        post_attn[i] = hidden_cpu[i] + o_proj[i];
755    }
756    let mut norm_w = [0f32; MAX_HIDDEN_DIM];
757    let mut ffn_norm_scratch = [0f32; MAX_HIDDEN_DIM];
758    let normed = prepare_pre_norm_input(
759        &post_attn[..n_embd],
760        n_embd,
761        tensors.ffn_norm.as_ref(),
762        Some(mmap),
763        index.tensor_data_start,
764        &mut ffn_norm_scratch,
765        &mut norm_w,
766    );
767    ffn_input[..n_embd].copy_from_slice(normed);
768    let gate_info = tensors.ffn_gate.as_ref()?;
769    let up_info = tensors.ffn_up.as_ref()?;
770    let down_info = tensors.ffn_down.as_ref()?;
771    let gate_raw =
772        crate::ggml_quants::fetch_tensor_bytes(mmap, index.tensor_data_start, gate_info).ok()?;
773    let up_raw =
774        crate::ggml_quants::fetch_tensor_bytes(mmap, index.tensor_data_start, up_info).ok()?;
775    let down_raw =
776        crate::ggml_quants::fetch_tensor_bytes(mmap, index.tensor_data_start, down_info).ok()?;
777    let (gate_in, n_ffn) = QTensorEngine::matmul_dims(gate_info);
778    let (up_in, up_out) = QTensorEngine::matmul_dims(up_info);
779    let (dn_in, dn_out) = QTensorEngine::matmul_dims(down_info);
780    if gate_in > n_embd
781        || up_in != gate_in
782        || up_out != n_ffn
783        || dn_in != n_ffn
784        || n_ffn > gate.len()
785        || dn_out < n_embd
786    {
787        return None;
788    }
789    if !stack_gemm_quant(
790        gate_raw,
791        gate_info,
792        normed,
793        &mut gate[..n_ffn],
794        gate_in,
795        n_ffn,
796    ) {
797        return None;
798    }
799    if !stack_gemm_quant(up_raw, up_info, normed, &mut up[..n_ffn], up_in, n_ffn) {
800        return None;
801    }
802    for i in 0..n_ffn {
803        let g = gate[i];
804        let silu = g / (1.0 + (-g).exp());
805        swiglu[i] = silu * up[i];
806    }
807    if !stack_gemm_quant(
808        down_raw,
809        down_info,
810        &swiglu[..dn_in],
811        &mut down[..n_embd],
812        dn_in,
813        n_embd,
814    ) {
815        return None;
816    }
817    Some(n_ffn)
818}
819
820/// Read one KV head from the CPU mirror arena.
821#[cfg(all(target_arch = "wasm32", feature = "wasm-llm-diagnostics"))]
822fn read_kv_cpu_head(
823    layout: &KvCacheLayout,
824    kv: &[f32],
825    layer: u32,
826    token_pos: u32,
827    kv_h: u32,
828    head_dim: usize,
829    k_not_v: bool,
830    out: &mut [f32],
831) -> bool {
832    if head_dim == 0 || head_dim > out.len() {
833        return false;
834    }
835    let slot = layout.ring_slot(token_pos);
836    for d in 0..head_dim {
837        let idx = if k_not_v {
838            layout.k_index(layer, slot, kv_h, d as u32)
839        } else {
840            layout.v_index(layer, slot, kv_h, d as u32)
841        };
842        if idx >= kv.len() {
843            return false;
844        }
845        out[d] = kv[idx];
846    }
847    true
848}
849
850#[cfg(all(target_arch = "wasm32", feature = "wasm-llm-diagnostics"))]
851fn probe_log_prefill_diff(phase: &str, cpu: &[f32], gpu: &[f32], n: usize) {
852    let n = n.min(8).min(cpu.len()).min(gpu.len());
853    if n == 0 {
854        return;
855    }
856    let err = probe_max_abs_diff(cpu, gpu, n);
857    wlog(&format!(
858        "[MC8 prefill] {phase}: cpu[0]={:.6} gpu[0]={:.6} max_abs_err={:.6}",
859        cpu[0], gpu[0], err
860    ));
861}
862
863/// MC8 pt3f: CPU SDPA @ L0 → full `q_dim` Attn_Out (async KV readback).
864#[cfg(all(target_arch = "wasm32", feature = "wasm-llm-diagnostics"))]
865async fn mc8_cpu_l0_attn_out(
866    engine: &QTensorEngine,
867    index: &crate::gguf_sharder::GgufTensorIndex,
868    mmap: &[u8],
869    layout: &KvCacheLayout,
870    hidden_cpu: &[f32],
871    n_embd: usize,
872    token_idx: u32,
873    out: &mut [f32],
874) -> Option<usize> {
875    let h = &index.hyperparams;
876    let tensors = index.get_layer_tensors(0);
877    let head_dim = h.head_dim() as usize;
878    let n_head = h.n_head as usize;
879    let q_heads_per_kv = h.q_heads_per_kv() as usize;
880    if head_dim == 0 || n_head == 0 || q_heads_per_kv == 0 {
881        return None;
882    }
883    let q_dim = n_head * head_dim;
884    if q_dim > out.len() {
885        return None;
886    }
887    let mut norm_w = [0f32; MAX_HIDDEN_DIM];
888    let mut norm_cpu = [0f32; MAX_HIDDEN_DIM];
889    let mut proj = [0f32; MAX_STACK_GEMM_OUT];
890    let normed = prepare_pre_norm_input(
891        &hidden_cpu[..n_embd],
892        n_embd,
893        tensors.attn_norm.as_ref(),
894        Some(mmap),
895        index.tensor_data_start,
896        &mut norm_cpu,
897        &mut norm_w,
898    );
899    let q_info = tensors.attn_q.as_ref()?;
900    let q_raw =
901        crate::ggml_quants::fetch_tensor_bytes(mmap, index.tensor_data_start, q_info).ok()?;
902    let (q_in, q_out) = QTensorEngine::matmul_dims(q_info);
903    if q_out != q_dim || !stack_gemm_quant(q_raw, q_info, normed, &mut proj[..q_out], q_in, q_out) {
904        return None;
905    }
906    rope_inplace(
907        &mut proj[..q_out],
908        n_head,
909        head_dim,
910        token_idx,
911        h.effective_rope_freq_base(),
912        h.effective_rope_scale(),
913    );
914    let scale = 1.0f32 / (head_dim as f32).sqrt();
915    let mut k_slot = [0f32; 128];
916    let mut v_slot = [0f32; 128];
917    if head_dim > k_slot.len() {
918        return None;
919    }
920    for qh in 0..n_head {
921        let kv_h = qh / q_heads_per_kv;
922        let q_off = qh * head_dim;
923        let mut att_scores = [0f32; MAX_CONTEXT_WINDOW as usize];
924        let mut max_score = f32::NEG_INFINITY;
925        for past_pos in 0..=token_idx {
926            let past_slot = layout.ring_slot(past_pos);
927            if !engine
928                .pipeline_read_kv_head(
929                    layout,
930                    0,
931                    past_slot,
932                    kv_h as u32,
933                    head_dim,
934                    true,
935                    &mut k_slot,
936                )
937                .await
938            {
939                return None;
940            }
941            let mut dot = 0.0f32;
942            for d in 0..head_dim {
943                dot += proj[q_off + d] * k_slot[d];
944            }
945            let score = dot * scale;
946            att_scores[past_pos as usize] = score;
947            max_score = max_score.max(score);
948        }
949        let mut sum_exp = 0.0f32;
950        for past_pos in 0..=token_idx {
951            let exp_val = (att_scores[past_pos as usize] - max_score).exp();
952            att_scores[past_pos as usize] = exp_val;
953            sum_exp += exp_val;
954        }
955        if sum_exp > 0.0 {
956            for past_pos in 0..=token_idx {
957                let prob = att_scores[past_pos as usize] / sum_exp;
958                let past_slot = layout.ring_slot(past_pos);
959                if !engine
960                    .pipeline_read_kv_head(
961                        layout,
962                        0,
963                        past_slot,
964                        kv_h as u32,
965                        head_dim,
966                        false,
967                        &mut v_slot,
968                    )
969                    .await
970                {
971                    return None;
972                }
973                for d in 0..head_dim {
974                    out[q_off + d] += v_slot[d] * prob;
975                }
976            }
977        }
978    }
979    Some(q_dim)
980}
981
982// --- PHASE 1 WASM OOB DIAGNOSTIC INSTRUMENTATION (remove once the trap is fixed) ---
983#[cfg(target_arch = "wasm32")]
984#[inline]
985pub(crate) fn wlog(s: &str) {
986    web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(s));
987}
988#[cfg(not(target_arch = "wasm32"))]
989#[inline]
990pub(crate) fn wlog(_s: &str) {}
991
992/// Decode-profiler: count of GPU `submit → poll(Maintain::Wait)` round-trips. Incremented by
993/// `QTensorEngine::poll_wait` (every native blocking sync point routes through it); read/reset by
994/// the bench to derive per-token synchronization overhead.
995pub static GPU_WAIT_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
996
997/// Total native GPU blocking-wait round-trips since the last reset.
998#[inline]
999pub fn gpu_wait_count() -> u64 {
1000    GPU_WAIT_COUNT.load(std::sync::atomic::Ordering::Relaxed)
1001}
1002
1003/// Reset the GPU blocking-wait counter before a measured run.
1004#[inline]
1005pub fn reset_gpu_wait_count() {
1006    GPU_WAIT_COUNT.store(0, std::sync::atomic::Ordering::Relaxed);
1007}
1008
1009/// In-place NEOX-style RoPE over `n_heads` consecutive `head_dim` blocks of `vec`.
1010/// Rotates split-half pairs `(i, i + head_dim/2)` — required for Llama/SmolLM2 GGUF weights.
1011/// (`fused_attention.wgsl` mirrors this NEOX split-half layout since MC8 Part 2.)
1012fn rope_inplace(vec: &mut [f32], n_heads: usize, head_dim: usize, pos: u32, base: f32, scale: f32) {
1013    let half = head_dim / 2;
1014    if half == 0 {
1015        return;
1016    }
1017    let scale = if scale > 0.0 && scale.is_finite() {
1018        scale
1019    } else {
1020        1.0
1021    };
1022    let scaled_pos = pos as f32 / scale;
1023    for head in 0..n_heads {
1024        let off = head * head_dim;
1025        if off + head_dim > vec.len() {
1026            return;
1027        }
1028        for i in 0..half {
1029            // Interleaved ("normal"/llama) rope: pair adjacent dims (2i, 2i+1). GGUF llama-arch
1030            // (SmolLM2) is is_neox=false — weights are permuted for interleaved, NOT split-half.
1031            let theta = scaled_pos * base.powf(-2.0 * i as f32 / head_dim as f32);
1032            let (s, c) = theta.sin_cos();
1033            let x0 = vec[off + 2 * i];
1034            let x1 = vec[off + 2 * i + 1];
1035            vec[off + 2 * i] = x0 * c - x1 * s;
1036            vec[off + 2 * i + 1] = x0 * s + x1 * c;
1037        }
1038    }
1039}
1040
1041/// STEM-grounding proof: the LLM's `rope_inplace` is the 2-D rotation defined in
1042/// `solvers::rope` — RoPE is trigonometry (a rotation per dimension pair), not a proprietary
1043/// operation. The inline `f32` kernel is checked against the `f64` STEM definition.
1044#[cfg(test)]
1045mod rope_stem_parity_tests {
1046    #[test]
1047    fn rope_kernel_is_the_stem_rotation() {
1048        let n_heads = 2usize;
1049        let head_dim = 8usize;
1050        let (pos, base, scale) = (7u32, 10000.0f32, 1.0f32);
1051        let xs: Vec<f32> = (0..n_heads * head_dim)
1052            .map(|i| (i as f32 - 8.0) * 0.25)
1053            .collect();
1054
1055        let mut got = xs.clone();
1056        super::rope_inplace(&mut got, n_heads, head_dim, pos, base, scale);
1057
1058        let mut want: Vec<f64> = xs.iter().map(|&v| v as f64).collect();
1059        crate::solvers::rope::rope_interleaved(
1060            &mut want,
1061            n_heads,
1062            head_dim,
1063            pos as f64,
1064            base as f64,
1065            scale as f64,
1066        );
1067
1068        for i in 0..xs.len() {
1069            assert!(
1070                (got[i] as f64 - want[i]).abs() < 1e-4,
1071                "RoPE kernel diverges from solvers::rope at {i}: {} vs {}",
1072                got[i],
1073                want[i]
1074            );
1075        }
1076    }
1077}
1078
1079/// Zero-heap CPU GEMM: `out[i] = dot(weight_row(i), input)` with per-row dequant.
1080/// `pub(crate)` so toolkit/parity probes can exercise the same kernel as the hot path.
1081pub(crate) fn stack_gemm_quant(
1082    raw: &[u8],
1083    info: &GgufTensorInfo,
1084    input: &[f32],
1085    out: &mut [f32],
1086    n_in: usize,
1087    n_out: usize,
1088) -> bool {
1089    if n_in > input.len() || n_out > out.len() || n_in > MAX_STACK_GEMM_IN {
1090        wlog(&format!(
1091            "[stack_gemm] GUARD tripped n_in={n_in} n_out={n_out} input={} out={} MAX_IN={MAX_STACK_GEMM_IN}",
1092            input.len(),
1093            out.len()
1094        ));
1095        return false;
1096    }
1097    #[cfg(target_arch = "wasm32")]
1098    if info.ggml_type == crate::ggml_quants::GGML_TYPE_Q8_0 {
1099        return wasm_cpu::q8_0_gemv_into(raw, input, out, n_in, n_out);
1100    }
1101    let mut row = [0f32; MAX_STACK_GEMM_IN];
1102    for i in 0..n_out {
1103        if crate::ggml_quants::dequant_matrix_row_into(raw, info, i, &mut row[..n_in]).unwrap_or(0)
1104            < n_in
1105        {
1106            return false;
1107        }
1108        out[i] = row[..n_in]
1109            .iter()
1110            .zip(&input[..n_in])
1111            .map(|(w, x)| w * x)
1112            .sum();
1113    }
1114    true
1115}
1116
1117pub struct QTensorEngine {
1118    /// Browser WASM keeps a private device; native reuses `gpu_context::shared_gpu()`.
1119    #[cfg(target_arch = "wasm32")]
1120    device: wgpu::Device,
1121    #[cfg(target_arch = "wasm32")]
1122    queue: wgpu::Queue,
1123    pub pipeline: wgpu::ComputePipeline,
1124    /// WASM multi-row Q8_0 GEMV (llama.cpp-style: 64 thr, 4 rows/WG, u32 packed reads).
1125    #[cfg(target_arch = "wasm32")]
1126    pub mmv_q8_0_pipeline: wgpu::ComputePipeline,
1127    #[cfg(not(target_arch = "wasm32"))]
1128    native_pipeline_cache: Option<wgpu::PipelineCache>,
1129    #[cfg(not(target_arch = "wasm32"))]
1130    pipeline_bind_layout: wgpu::BindGroupLayout,
1131    /// 0.0.21: cooperative GEMV (one workgroup per output row, shared-memory reduction). Same shader
1132    /// MODULE as `pipeline`, entry point `coop_gemv` / `coop_gemv_sg`. Selected per-call when
1133    /// `llm_bench::coop_gemv_enabled()`. Native only (the wasm decode path is the MC8 arena).
1134    #[cfg(not(target_arch = "wasm32"))]
1135    pub(crate) coop_gemv_pipeline: wgpu::ComputePipeline,
1136    #[cfg(not(target_arch = "wasm32"))]
1137    coop_gemv_bind_layout: wgpu::BindGroupLayout,
1138    /// Multi-row coop GEMV (8 rows/WG) for Q4_K_SOA large n_out — see `coop_gemv_mr`.
1139    #[cfg(not(target_arch = "wasm32"))]
1140    pub(crate) coop_gemv_mr_pipeline: wgpu::ComputePipeline,
1141    /// GEMV + residual add in one dispatch (O-proj / down-proj in resident mega-pass).
1142    #[cfg(not(target_arch = "wasm32"))]
1143    pub(crate) coop_gemv_residual_pipeline: wgpu::ComputePipeline,
1144    #[cfg(not(target_arch = "wasm32"))]
1145    coop_gemv_residual_bind_layout: wgpu::BindGroupLayout,
1146    /// Multi-row residual GEMV for Q4_K_SOA.
1147    #[cfg(not(target_arch = "wasm32"))]
1148    pub(crate) coop_gemv_residual_mr_pipeline: wgpu::ComputePipeline,
1149    /// Warp GEMV (32 thr/row) for Q4_K_SOA.
1150    #[cfg(not(target_arch = "wasm32"))]
1151    pub(crate) coop_gemv_warp_pipeline: wgpu::ComputePipeline,
1152    #[cfg(not(target_arch = "wasm32"))]
1153    pub(crate) coop_gemv_residual_warp_pipeline: wgpu::ComputePipeline,
1154    /// Legacy f32×f32 mock block for offset-0 `QTensor` fallback (no mmap).
1155    #[cfg(not(target_arch = "wasm32"))]
1156    mock_pipeline: wgpu::ComputePipeline,
1157    /// GPU-side Q6_K embedding dequant + matmul (zero CPU dequant).
1158    pub embedding_pipeline: wgpu::ComputePipeline,
1159    #[cfg(not(target_arch = "wasm32"))]
1160    embedding_bind_layout: wgpu::BindGroupLayout,
1161    pub is_initialized: bool,
1162    /// DirectML device — Some on Windows when DirectML 1.15 is linked.
1163    #[cfg(target_os = "windows")]
1164    pub dml: Option<crate::directml_bridge::DmlDevice>,
1165    /// Memory-mapped GGUF file (set after `load_gguf`).
1166    #[cfg(not(target_arch = "wasm32"))]
1167    pub gguf_mmap: Option<Arc<memmap2::Mmap>>,
1168    #[cfg(target_arch = "wasm32")]
1169    pub gguf_mmap: Option<Arc<[u8]>>,
1170    /// WASM: cached tokenizer extracted from gguf_mmap before dropping it.
1171    #[cfg(target_arch = "wasm32")]
1172    pub cached_tokenizer: Option<crate::gguf_sharder::GgufTokenizer>,
1173    /// WASM: cached tensor index extracted from gguf_mmap before dropping it.
1174    #[cfg(target_arch = "wasm32")]
1175    pub cached_tensor_index: Option<crate::gguf_sharder::GgufTensorIndex>,
1176    /// WASM: raw bytes of the token_embd tensor (for embedding lookup after dropping gguf_mmap).
1177    #[cfg(target_arch = "wasm32")]
1178    pub cached_token_embd: Option<Arc<[u8]>>,
1179    /// Resident P64 container bytes.
1180    #[cfg(target_arch = "wasm32")]
1181    pub p64_resident: Option<Arc<[u8]>>,
1182    /// Cached P64 index after `adopt_resident_p64_*` — decode must not re-CRC the container.
1183    #[cfg(not(target_arch = "wasm32"))]
1184    pub p64_index: Option<crate::p64_weight::P64TensorIndex>,
1185    /// Cached synthetic GGUF index built from `p64_index` (or from GGUF parse).
1186    #[cfg(not(target_arch = "wasm32"))]
1187    pub tensor_index_cache: Option<crate::gguf_sharder::GgufTensorIndex>,
1188
1189    /// Byte offset into the mmap where tensor data begins.
1190    pub tensor_data_offset: u64,
1191    pub hyperparams: crate::gguf_sharder::GgufHyperparams,
1192    pub max_tensor_bytes: usize,
1193    /// Reused layer staging buffers (one layer in VRAM at a time).
1194    gemm_input_buf: Option<wgpu::Buffer>,
1195    gemm_weight_buf: Option<wgpu::Buffer>,
1196    /// MC8 Part 3t: disjoint per-role weight arena (prefill single-submit).
1197    #[cfg(target_arch = "wasm32")]
1198    mc8_weight_arena: Option<Mc8WeightArenaBufs>,
1199    /// MC8 Part 3x: when set, the 7 role buffers hold ALL layers' weights (uploaded once);
1200    /// hot-path encoders bind a per-layer sub-range instead of re-`write_buffer`ing per forward.
1201    #[cfg(target_arch = "wasm32")]
1202    mc8_weights_resident: bool,
1203    /// Per-role 256-byte-aligned per-layer stride (bytes), indexed by `Mc8WeightRole::idx()`.
1204    #[cfg(target_arch = "wasm32")]
1205    mc8_weight_role_stride: [u64; 7],
1206    /// Legacy decode-path ping-pong (decode tail not on weight arena yet).
1207    #[cfg(target_arch = "wasm32")]
1208    gemm_weight_buf_b: Option<wgpu::Buffer>,
1209    gemm_output_buf: Option<wgpu::Buffer>,
1210    gemm_params_buf: Option<wgpu::Buffer>,
1211    gemm_output_staging: Option<wgpu::Buffer>,
1212    // A1a (STELLAR §A): persistent GPU top-k output-projection pipeline + small candidate buffers.
1213    // Lets the output logits stay on-GPU (top-k over them, read back only K pairs) instead of the
1214    // 196 KB/token full-logit readback. Created once in `ensure_gemm_buffers`.
1215    output_topk_pipeline: Option<wgpu::ComputePipeline>,
1216    output_topk_bind_layout: Option<wgpu::BindGroupLayout>,
1217    topk_cand_val_buf: Option<wgpu::Buffer>,
1218    topk_cand_idx_buf: Option<wgpu::Buffer>,
1219    topk_cand_staging: Option<wgpu::Buffer>,
1220    topk_params_buf: Option<wgpu::Buffer>,
1221    /// MC8 FFN / attention scratch (gate, up, o_proj).
1222    gemm_aux_buf: Option<wgpu::Buffer>,
1223    /// MC8 SwiGLU up-projection scratch (cannot alias gemm_output/work — in-place GEMM invalid).
1224    gemm_ffn_buf: Option<wgpu::Buffer>,
1225    /// Batched prefill RMS output (same span as `gemm_input_buf`; avoids in-place on batch_buf).
1226    #[cfg(target_arch = "wasm32")]
1227    prefill_scratch_buf: Option<wgpu::Buffer>,
1228    /// Strided prefill ping-pong rows (`PREFILL_CHUNK_SIZE × row_stride` floats each).
1229    #[cfg(target_arch = "wasm32")]
1230    prefill_work_buf_a: Option<wgpu::Buffer>,
1231    #[cfg(target_arch = "wasm32")]
1232    prefill_work_buf_b: Option<wgpu::Buffer>,
1233    /// Phase 5.5: Q/K/V projection scratch (parallel-GEMM output → lightweight attention shader).
1234    #[cfg(target_arch = "wasm32")]
1235    mc8_q_proj_buf: Option<wgpu::Buffer>,
1236    #[cfg(target_arch = "wasm32")]
1237    mc8_k_proj_buf: Option<wgpu::Buffer>,
1238    #[cfg(target_arch = "wasm32")]
1239    mc8_v_proj_buf: Option<wgpu::Buffer>,
1240    gemm_max_out_dim: u32,
1241    gemm_max_input_floats: usize,
1242    /// Static KV ring-buffer (allocated once at `load_gguf`).
1243    kv_layout: Option<KvCacheLayout>,
1244    kv_cache_gpu: Option<wgpu::Buffer>,
1245    /// CPU mirror for quantized-attention fallback (no growth during decode).
1246    kv_cache_cpu: Option<Box<[f32]>>,
1247    attention_pipeline: wgpu::ComputePipeline,
1248    #[cfg(not(target_arch = "wasm32"))]
1249    attention_bind_layout: wgpu::BindGroupLayout,
1250    attention_params_buf: Option<wgpu::Buffer>,
1251    attention_mask_buf: Option<wgpu::Buffer>,
1252    /// MC8 elementwise GPU ops (RMSNorm / SiLU×mul / residual).
1253    elem_rms_norm_pipeline: wgpu::ComputePipeline,
1254    elem_silu_mul_pipeline: wgpu::ComputePipeline,
1255    #[cfg(not(target_arch = "wasm32"))]
1256    elem_silu_mul_bind_layout: wgpu::BindGroupLayout,
1257    elem_add_residual_pipeline: wgpu::ComputePipeline,
1258    elem_params_buf: Option<wgpu::Buffer>,
1259    norm_weight_buf: Option<wgpu::Buffer>,
1260    /// MC8 Part 3s: dynamic-offset bind group layouts (uniform race elimination).
1261    #[cfg(target_arch = "wasm32")]
1262    mc8_gemm_bind_layout: wgpu::BindGroupLayout,
1263    #[cfg(target_arch = "wasm32")]
1264    mc8_elem_bind_layout: wgpu::BindGroupLayout,
1265    #[cfg(target_arch = "wasm32")]
1266    mc8_attn_bind_layout: wgpu::BindGroupLayout,
1267    /// Phase 5 dispatch fusion: SwiGLU expansion (gate · SiLU · up) collapsed into one pass.
1268    #[cfg(target_arch = "wasm32")]
1269    mc8_ffn_fused_bind_layout: wgpu::BindGroupLayout,
1270    #[cfg(target_arch = "wasm32")]
1271    mc8_ffn_fused_pipeline: wgpu::ComputePipeline,
1272    /// Native T-A1: same fused FFN expansion, static uniform (resident mega-pass).
1273    #[cfg(not(target_arch = "wasm32"))]
1274    ffn_fused_bind_layout: wgpu::BindGroupLayout,
1275    /// Naive 64-thread/row fused expansion (wasm-style; fallback).
1276    #[cfg(not(target_arch = "wasm32"))]
1277    ffn_fused_pipeline: wgpu::ComputePipeline,
1278    /// T-A1b: coop 256-thread/row fused expansion (preferred when coop GEMV is on).
1279    #[cfg(not(target_arch = "wasm32"))]
1280    ffn_fused_coop_pipeline: wgpu::ComputePipeline,
1281    /// Multi-row fused FFN (4 rows/WG) for Q4_K_SOA.
1282    #[cfg(not(target_arch = "wasm32"))]
1283    ffn_fused_mr_pipeline: wgpu::ComputePipeline,
1284    /// Warp fused FFN (32 thr/row) for Q4_K_SOA.
1285    #[cfg(not(target_arch = "wasm32"))]
1286    ffn_fused_warp_pipeline: wgpu::ComputePipeline,
1287    /// Dual K+V GEMV (shared act) for resident mega-pass.
1288    #[cfg(not(target_arch = "wasm32"))]
1289    dual_gemv_pipeline: wgpu::ComputePipeline,
1290    /// Dual multi-row (4 rows/WG) — default for SoA K+V.
1291    #[cfg(not(target_arch = "wasm32"))]
1292    dual_gemv_mr_pipeline: wgpu::ComputePipeline,
1293    #[cfg(not(target_arch = "wasm32"))]
1294    dual_gemv_bind_layout: wgpu::BindGroupLayout,
1295    /// Triple Q+K+V GEMV (shared act, GQA-safe) — one dispatch replaces dual+Q.
1296    #[cfg(not(target_arch = "wasm32"))]
1297    triple_gemv_pipeline: wgpu::ComputePipeline,
1298    #[cfg(not(target_arch = "wasm32"))]
1299    triple_gemv_bind_layout: wgpu::BindGroupLayout,
1300    /// Phase 5.3: the output/logits projection (tied `token_embd`, ~50 MB) uploaded to VRAM
1301    /// once at init so the per-token argmax binds resident sub-ranges instead of re-uploading
1302    /// the whole matrix every token (the decode throughput killer). A1a step-2 ports this to the
1303    /// native top-k decode path, so these two fields are available on both targets.
1304    mc8_logits_resident_buf: Option<wgpu::Buffer>,
1305    mc8_logits_row_bytes: u32,
1306    /// A1b (STELLAR §A): resident 2-bit ternary-FFN GEMM dispatcher, built once at P64 boot from
1307    /// the container's base-3 FFN blobs (rebaked to 2-bit, uploaded once). `None` until a ternary
1308    /// P64 is adopted; the FFN dispatch branch (`dispatch_ternary_ffn`) uses it when present +
1309    /// the toggle is on, else the CPU oracle. Native-only; the wasm ternary path is a later step.
1310    #[cfg(not(target_arch = "wasm32"))]
1311    ternary_ffn: Option<crate::ternary_gpu::TernaryFfnResident>,
1312    /// Phase 2 (resident weights): resident VRAM weight buffers, keyed by each weight byte-region's
1313    /// absolute mmap address (unique per distinct weight — incl. each output-projection vocab chunk,
1314    /// which all share one tensor `byte_offset` — and stable across tokens). Populated lazily on the
1315    /// first GEMM that touches a region and reused every token, so a weight is uploaded to VRAM ONCE
1316    /// instead of re-`write_buffer`ed (up to ~50 MB for a 3B FFN tensor) on every GEMM, every token —
1317    /// the decode-bandwidth lever for large models. Mmap bytes are immutable, so the cache is always
1318    /// coherent. Native-only (wasm uses the MC8 arena); active when `resident_weights_enabled()`.
1319    #[cfg(not(target_arch = "wasm32"))]
1320    gemm_resident_weights: std::sync::Mutex<std::collections::HashMap<u64, wgpu::Buffer>>,
1321    /// Phase 3 (FFN fusion): a small uniform buffer holding the gate/up/down GEMM `GemmGpuParams`
1322    /// at 256-aligned sub-ranges (3 slots), so all three GEMMs of one fused FFN submit can bind
1323    /// distinct params simultaneously. Lazily created native-only on the first fused FFN.
1324    #[cfg(not(target_arch = "wasm32"))]
1325    ffn_fused_params: Option<wgpu::Buffer>,
1326    /// Native attention preproject fusion: two 256-byte-aligned GEMM uniform slots
1327    /// (K,V) and two attention uniform slots (K-write,V-write), allowing K/V
1328    /// projection + KV-cache writes to share one submit without uniform races.
1329    #[cfg(not(target_arch = "wasm32"))]
1330    attention_kv_gemm_params: Option<wgpu::Buffer>,
1331    #[cfg(not(target_arch = "wasm32"))]
1332    attention_kv_params: Option<wgpu::Buffer>,
1333    /// Phase 5.4: all layers' attn_norm + ffn_norm weights resident (slot 2L = attn, 2L+1 = ffn),
1334    /// so RMSNorm binds a per-layer sub-range instead of re-`write_buffer`ing a shared single-layer
1335    /// `norm_weight_buf` every layer (the second per-layer write_buffer race blocking single-submit).
1336    #[cfg(target_arch = "wasm32")]
1337    mc8_norm_resident_buf: Option<wgpu::Buffer>,
1338    #[cfg(target_arch = "wasm32")]
1339    mc8_norm_stride: u32,
1340    /// Native GPU-resident single-fence decode plan (see `resident_decode.rs`).
1341    #[cfg(not(target_arch = "wasm32"))]
1342    resident_decode: resident_decode::ResidentDecodeState,
1343    /// Cold-built host descriptor for the native CUDA all-layer plan.
1344    #[cfg(all(not(target_arch = "wasm32"), feature = "cuda"))]
1345    cuda_decode_plan: cuda_decode_plan::CudaDecodePlanState,
1346    /// W3: native GPU-resident single-fence-per-chunk prefill plan (see `prefill_arena.rs`).
1347    #[cfg(not(target_arch = "wasm32"))]
1348    prefill_arena: prefill_arena::PrefillArenaState,
1349    /// W6a: batched speculative-verify forward plan (per-position argmax; see `verify_arena.rs`).
1350    #[cfg(not(target_arch = "wasm32"))]
1351    verify_arena: verify_arena::VerifyArenaState,
1352    /// Bind group cache: eliminates per-token `create_bind_group` calls by caching
1353    /// bind groups keyed on (buffer addresses, offsets, weight role, layer). Bind groups
1354    /// are identical across tokens for the same layer/op since only dynamic uniform
1355    /// offsets change — those are passed at `set_bind_group` time, not baked into the BG.
1356    #[cfg(target_arch = "wasm32")]
1357    mc8_bg_cache: std::sync::Mutex<std::collections::HashMap<u64, wgpu::BindGroup>>,
1358}
1359
1360#[cfg(target_arch = "wasm32")]
1361thread_local! {
1362    pub static WASM_ENGINE_INSTANCE: std::cell::RefCell<Option<QTensorEngine>> = std::cell::RefCell::new(None);
1363}
1364
1365/// Boot the resident WASM WebGPU engine from GGUF or P64 bytes.
1366///
1367/// Uses the **original sync adopt path** (full weight + logits + norm upload) so
1368/// decode stays coherent. Yields only between major phases so the UI can paint
1369/// status; weight upload itself is intentionally one blocking stretch with a
1370/// clear status line first (phones will freeze briefly — that is correct).
1371#[cfg(target_arch = "wasm32")]
1372pub async fn initialize_webgpu_engine(model_data: std::sync::Arc<[u8]>) -> Result<(), String> {
1373    use wasm_yield::{clear_init_status, phase, set_init_status};
1374
1375    // Use `try_new()` (not `new_async()`) so a missing/incompatible WebGPU adapter
1376    // surfaces as a rejected promise the JS layer can display, rather than an
1377    // `.expect()` panic that aborts the wasm module and leaves the init promise
1378    // pending forever (the "stuck on Initialising…" hang).
1379    phase("Requesting WebGPU adapter + device…").await;
1380    let mut engine = QTensorEngine::try_new().await?;
1381    phase("WebGPU device + pipelines ready — loading model (weights may take 1–3 min)…").await;
1382
1383    // Dual-format boot gate. P64 owns the canonical lowercase four-byte
1384    // `p64\0` magic. Sync adopt is the proven coherent path (do not defer uploads).
1385    if crate::p64_weight::has_p64_magic(&model_data) {
1386        set_init_status(format!(
1387            "Loading P64 + uploading GPU weights ({:.0} MB)…",
1388            model_data.len() as f64 / (1024.0 * 1024.0)
1389        ));
1390        // One yield so the status line paints before the long sync upload freezes the tab.
1391        wasm_yield::yield_to_browser().await;
1392        engine.adopt_resident_p64(model_data)?;
1393    } else {
1394        set_init_status(format!(
1395            "Loading GGUF + uploading GPU weights ({:.0} MB)…",
1396            model_data.len() as f64 / (1024.0 * 1024.0)
1397        ));
1398        wasm_yield::yield_to_browser().await;
1399        engine.adopt_resident_mmap(model_data)?;
1400    }
1401    phase("Engine resident — ready").await;
1402    WASM_ENGINE_INSTANCE.with(|g| *g.borrow_mut() = Some(engine));
1403    clear_init_status();
1404    Ok(())
1405}
1406
1407#[cfg(not(target_arch = "wasm32"))]
1408impl QTensorEngine {
1409    /// W3 kernel-parity probe (test/diagnostic): run the GPU GEMM (`dispatch_gemm_raw_into`) and the
1410    /// CPU reference (`stack_gemm_quant`) on the SAME quantized weights + input, writing each into a
1411    /// caller-provided buffer. Ensures the GEMM buffers exist first, so a fresh engine (no model
1412    /// loaded) can be probed directly. Returns `true` only if both ran; the caller compares the two
1413    /// outputs with [`crate::llm_kernel_parity`]. Enable [`crate::llm_gpu_profiler`] around the call
1414    /// to witness that the GPU path actually executed rather than silently falling back to the CPU.
1415    pub fn gemm_parity_probe(
1416        &mut self,
1417        info: &GgufTensorInfo,
1418        raw: &[u8],
1419        input: &[f32],
1420        gpu_out: &mut [f32],
1421        cpu_out: &mut [f32],
1422        n_in: usize,
1423        n_out: usize,
1424    ) -> bool {
1425        self.ensure_gemm_buffers(raw.len().max(1), n_out as u32);
1426        let gpu_ok = self.dispatch_gemm_raw_into(info, raw, input, gpu_out, n_in, n_out);
1427        let cpu_ok = stack_gemm_quant(raw, info, input, cpu_out, n_in, n_out);
1428        gpu_ok && cpu_ok
1429    }
1430}