1pub(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
26pub 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#[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 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; }
69
70 let offset = quin.extract_byte_offset();
71
72 Some(Self::new(vec![4096, 4096], offset, true))
75 }
76}
77
78mod gpu_params;
82mod quant_support;
83pub(crate) use gpu_params::*;
84pub(crate) use quant_support::*;
85
86pub const KV_ATTENTION_MASK_WORDS: usize = crate::compute_universe::KV_ATTENTION_MASK_WORDS;
88
89#[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#[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#[cfg(target_arch = "wasm32")]
118pub(crate) const MC8_LAYERS_PER_ENCODER: u32 = 64;
119#[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; #[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#[cfg(target_arch = "wasm32")]
138pub(crate) struct Mc8ChunkUniformCursors {
139 attn: usize,
140 elem: usize,
141 gemm: usize,
142}
143
144#[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 #[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#[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#[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 off_q_gemm: u32,
203 off_k_gemm: u32,
204 off_v_gemm: u32,
205}
206
207#[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#[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#[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#[cfg(target_arch = "wasm32")]
363pub(crate) struct WasmGpuPipeline {
364 encoder: wgpu::CommandEncoder,
365}
366
367#[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#[cfg(target_arch = "wasm32")]
400mod mc8_wasm;
401
402pub const MAX_CONTEXT_WINDOW: u32 = 1024;
404pub const KV_CACHE_MAX_BYTES: usize = 448 * 1024 * 1024;
406
407#[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 pub int8: bool,
422 pub dict_k: u32,
426 pub dict_n_atoms: u32,
429}
430
431impl KvCacheLayout {
432 pub fn from_hyperparams(h: &crate::gguf_sharder::GgufHyperparams) -> Option<Self> {
433 #[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 #[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 let int8 = int8 && dict_k == 0;
474 let slot_kv_elems = n_kv_head * head_dim;
475 let layer_stride = if dict_k > 0 {
479 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 #[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 #[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
569const 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;
573const MAX_HIDDEN_DIM: usize = 4096;
575pub(crate) const RMS_NORM_EPS: f32 = 1e-5;
577pub const PREFILL_CHUNK_SIZE: usize = 64;
579const MAX_ATTN_MASK_UPLOAD_WORDS: usize = PREFILL_CHUNK_SIZE * KV_ATTENTION_MASK_WORDS;
581pub const MAX_PREFILL_BATCH_FLOATS: usize = MAX_STACK_GEMM_IN * PREFILL_CHUNK_SIZE;
583pub const PREFILL_CHUNK_STACK_FLOATS: usize = 2560 * PREFILL_CHUNK_SIZE;
585const MAX_WGPU_WEIGHT_STAGING: usize = 64 * 1024 * 1024;
587pub const VOCAB_CHUNK_ROWS: usize = MAX_STACK_GEMM_OUT;
592
593#[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
612mod cpu_ops;
616pub(crate) use cpu_ops::*;
617#[cfg(not(target_arch = "wasm32"))]
618mod pipeline_cache;
619pub mod wasm_cpu;
622#[cfg(not(target_arch = "wasm32"))]
623pub(crate) use pipeline_cache::*;
624
625mod 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#[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#[cfg(target_arch = "wasm32")]
653pub(crate) mod wasm_yield;
654
655#[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#[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#[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#[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#[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
992pub static GPU_WAIT_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
996
997#[inline]
999pub fn gpu_wait_count() -> u64 {
1000 GPU_WAIT_COUNT.load(std::sync::atomic::Ordering::Relaxed)
1001}
1002
1003#[inline]
1005pub fn reset_gpu_wait_count() {
1006 GPU_WAIT_COUNT.store(0, std::sync::atomic::Ordering::Relaxed);
1007}
1008
1009fn 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 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#[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
1079pub(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 #[cfg(target_arch = "wasm32")]
1120 device: wgpu::Device,
1121 #[cfg(target_arch = "wasm32")]
1122 queue: wgpu::Queue,
1123 pub pipeline: wgpu::ComputePipeline,
1124 #[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 #[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 #[cfg(not(target_arch = "wasm32"))]
1140 pub(crate) coop_gemv_mr_pipeline: wgpu::ComputePipeline,
1141 #[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 #[cfg(not(target_arch = "wasm32"))]
1148 pub(crate) coop_gemv_residual_mr_pipeline: wgpu::ComputePipeline,
1149 #[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 #[cfg(not(target_arch = "wasm32"))]
1156 mock_pipeline: wgpu::ComputePipeline,
1157 pub embedding_pipeline: wgpu::ComputePipeline,
1159 #[cfg(not(target_arch = "wasm32"))]
1160 embedding_bind_layout: wgpu::BindGroupLayout,
1161 pub is_initialized: bool,
1162 #[cfg(target_os = "windows")]
1164 pub dml: Option<crate::directml_bridge::DmlDevice>,
1165 #[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 #[cfg(target_arch = "wasm32")]
1172 pub cached_tokenizer: Option<crate::gguf_sharder::GgufTokenizer>,
1173 #[cfg(target_arch = "wasm32")]
1175 pub cached_tensor_index: Option<crate::gguf_sharder::GgufTensorIndex>,
1176 #[cfg(target_arch = "wasm32")]
1178 pub cached_token_embd: Option<Arc<[u8]>>,
1179 #[cfg(target_arch = "wasm32")]
1181 pub p64_resident: Option<Arc<[u8]>>,
1182 #[cfg(not(target_arch = "wasm32"))]
1184 pub p64_index: Option<crate::p64_weight::P64TensorIndex>,
1185 #[cfg(not(target_arch = "wasm32"))]
1187 pub tensor_index_cache: Option<crate::gguf_sharder::GgufTensorIndex>,
1188
1189 pub tensor_data_offset: u64,
1191 pub hyperparams: crate::gguf_sharder::GgufHyperparams,
1192 pub max_tensor_bytes: usize,
1193 gemm_input_buf: Option<wgpu::Buffer>,
1195 gemm_weight_buf: Option<wgpu::Buffer>,
1196 #[cfg(target_arch = "wasm32")]
1198 mc8_weight_arena: Option<Mc8WeightArenaBufs>,
1199 #[cfg(target_arch = "wasm32")]
1202 mc8_weights_resident: bool,
1203 #[cfg(target_arch = "wasm32")]
1205 mc8_weight_role_stride: [u64; 7],
1206 #[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 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 gemm_aux_buf: Option<wgpu::Buffer>,
1223 gemm_ffn_buf: Option<wgpu::Buffer>,
1225 #[cfg(target_arch = "wasm32")]
1227 prefill_scratch_buf: Option<wgpu::Buffer>,
1228 #[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 #[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 kv_layout: Option<KvCacheLayout>,
1244 kv_cache_gpu: Option<wgpu::Buffer>,
1245 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 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 #[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 #[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 #[cfg(not(target_arch = "wasm32"))]
1274 ffn_fused_bind_layout: wgpu::BindGroupLayout,
1275 #[cfg(not(target_arch = "wasm32"))]
1277 ffn_fused_pipeline: wgpu::ComputePipeline,
1278 #[cfg(not(target_arch = "wasm32"))]
1280 ffn_fused_coop_pipeline: wgpu::ComputePipeline,
1281 #[cfg(not(target_arch = "wasm32"))]
1283 ffn_fused_mr_pipeline: wgpu::ComputePipeline,
1284 #[cfg(not(target_arch = "wasm32"))]
1286 ffn_fused_warp_pipeline: wgpu::ComputePipeline,
1287 #[cfg(not(target_arch = "wasm32"))]
1289 dual_gemv_pipeline: wgpu::ComputePipeline,
1290 #[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 #[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 mc8_logits_resident_buf: Option<wgpu::Buffer>,
1305 mc8_logits_row_bytes: u32,
1306 #[cfg(not(target_arch = "wasm32"))]
1311 ternary_ffn: Option<crate::ternary_gpu::TernaryFfnResident>,
1312 #[cfg(not(target_arch = "wasm32"))]
1320 gemm_resident_weights: std::sync::Mutex<std::collections::HashMap<u64, wgpu::Buffer>>,
1321 #[cfg(not(target_arch = "wasm32"))]
1325 ffn_fused_params: Option<wgpu::Buffer>,
1326 #[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 #[cfg(target_arch = "wasm32")]
1337 mc8_norm_resident_buf: Option<wgpu::Buffer>,
1338 #[cfg(target_arch = "wasm32")]
1339 mc8_norm_stride: u32,
1340 #[cfg(not(target_arch = "wasm32"))]
1342 resident_decode: resident_decode::ResidentDecodeState,
1343 #[cfg(all(not(target_arch = "wasm32"), feature = "cuda"))]
1345 cuda_decode_plan: cuda_decode_plan::CudaDecodePlanState,
1346 #[cfg(not(target_arch = "wasm32"))]
1348 prefill_arena: prefill_arena::PrefillArenaState,
1349 #[cfg(not(target_arch = "wasm32"))]
1351 verify_arena: verify_arena::VerifyArenaState,
1352 #[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#[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 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 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 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 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}