1use crate::llm_agent::{AgentBackend, LocalLlmAgent};
7
8use super::metrics::tok_per_s;
9use super::*;
10
11pub fn compare_topk_decode(
15 model_path: &str,
16 prompt: &str,
17 decode_tokens: u32,
18) -> Result<(String, String), String> {
19 if !std::path::Path::new(model_path).exists() {
20 return Err(format!("model not found: {model_path}"));
21 }
22 let agent = LocalLlmAgent::with_local_backend(
23 "did:qualia:bench",
24 AgentBackend::Local {
25 model_path: model_path.to_string(),
26 context_window: 4096,
27 quantization: "auto".into(),
28 vision_projector_path: None,
29 modality: "text".into(),
30 architecture: None,
31 },
32 );
33 set_decode_budget_override(decode_tokens);
34 let model_id = crate::q_hash(model_path);
35 let _ = crate::resident_model::mount_resident_gguf(model_id, model_path, false);
36
37 set_gpu_topk(false);
38 let (off_text, _, _, _) = agent.infer_local_model_streaming::<fn(String)>(prompt, "", None);
39 set_gpu_topk(true);
40 let (on_text, _, _, _) = agent.infer_local_model_streaming::<fn(String)>(prompt, "", None);
41
42 set_gpu_topk(false);
43 set_decode_budget_override(0);
44 crate::resident_model::clear_resident_model();
45 Ok((off_text, on_text))
46}
47
48pub fn compare_topk_decode_blocking(
50 model_path: &str,
51 prompt: &str,
52 decode_tokens: u32,
53) -> Result<(String, String), String> {
54 let rt = tokio::runtime::Builder::new_multi_thread()
55 .worker_threads(2)
56 .enable_all()
57 .build()
58 .map_err(|e| e.to_string())?;
59 rt.block_on(async { compare_topk_decode(model_path, prompt, decode_tokens) })
60}
61
62#[cfg(not(target_arch = "wasm32"))]
67pub fn decode_with_metrics(
68 model_path: &str,
69 prompt: &str,
70 decode_tokens: u32,
71) -> Result<(String, f64), String> {
72 if !std::path::Path::new(model_path).exists() {
73 return Err(format!("model not found: {model_path}"));
74 }
75 let is_q42 = {
76 use std::io::Read;
77 let mut buf = [0u8; 4];
78 std::fs::File::open(model_path)
79 .and_then(|mut f| f.read_exact(&mut buf))
80 .map(|_| &buf == b"p64\0")
81 .unwrap_or(false)
82 };
83 let agent = LocalLlmAgent::with_local_backend(
84 "did:qualia:bench",
85 AgentBackend::Local {
86 model_path: model_path.to_string(),
87 context_window: 4096,
88 quantization: "auto".into(),
89 vision_projector_path: None,
90 modality: "text".into(),
91 architecture: None,
92 },
93 );
94 set_decode_budget_override(decode_tokens);
95 let model_id = crate::q_hash(model_path);
96 if is_q42 {
97 crate::resident_model::mount_resident_q42(model_id, model_path, false)?;
98 } else {
99 let _ = crate::resident_model::mount_resident_gguf(model_id, model_path, false);
100 }
101 reset_phase_metrics();
102 let (text, _, _, _) = agent.infer_local_model_streaming::<fn(String)>(prompt, "", None);
103 let snap = phase_snapshot();
104 let decode_tok_s = tok_per_s(snap.decode_tokens, snap.decode_ns);
105 set_decode_budget_override(0);
106 crate::resident_model::clear_resident_model();
107 Ok((text, decode_tok_s))
108}
109
110#[cfg(not(target_arch = "wasm32"))]
117pub fn gemm_parity_probe_blocking(
118 n_in: usize,
119 n_out: usize,
120 seed: u64,
121) -> Result<(f32, f64, u64, u64), String> {
122 use crate::gguf_sharder::GgufTensorInfo;
123 if n_in == 0 || n_out == 0 || n_in % 32 != 0 {
124 return Err("n_in must be a non-zero multiple of 32 (Q8_0 block size); n_out > 0".into());
125 }
126 let rt = tokio::runtime::Builder::new_multi_thread()
127 .worker_threads(2)
128 .enable_all()
129 .build()
130 .map_err(|e| e.to_string())?;
131 let mut engine = rt
132 .block_on(crate::gguf_bridge::QTensorEngine::try_new())
133 .map_err(|e| format!("engine init: {e}"))?;
134 let _guard = rt.enter(); let mut s = seed | 1;
138 let mut rng = move || -> f32 {
139 s = s
140 .wrapping_mul(6364136223846793005)
141 .wrapping_add(1442695040888963407);
142 ((s >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0
143 };
144
145 let row_bytes = crate::llm_kernel_parity::q8_0_bytes(n_in);
146 let mut raw = vec![0u8; row_bytes * n_out];
147 let mut row_f32 = vec![0f32; n_in];
148 for r in 0..n_out {
149 for x in row_f32.iter_mut() {
150 *x = rng();
151 }
152 if !crate::llm_kernel_parity::quantize_q8_0_from_f32(
153 &row_f32,
154 &mut raw[r * row_bytes..(r + 1) * row_bytes],
155 ) {
156 return Err("q8_0 quantize failed".into());
157 }
158 }
159 let input: Vec<f32> = (0..n_in).map(|_| rng()).collect();
160
161 let info = GgufTensorInfo {
162 dims: [n_in as u64, n_out as u64, 1, 1],
163 n_dims: 2,
164 ggml_type: crate::ggml_quants::GGML_TYPE_Q8_0,
165 byte_offset: 0,
166 };
167
168 crate::llm_gpu_profiler::set_enabled(true);
169 crate::llm_gpu_profiler::reset();
170 let mut gpu_out = vec![0f32; n_out];
171 let mut cpu_out = vec![0f32; n_out];
172 let ok = engine.gemm_parity_probe(&info, &raw, &input, &mut gpu_out, &mut cpu_out, n_in, n_out);
173 let calls = crate::llm_gpu_profiler::snapshot()
174 .iter()
175 .find(|t| matches!(t.phase, crate::llm_gpu_profiler::Phase::Gemm))
176 .map(|t| t.calls)
177 .unwrap_or(0);
178 crate::llm_gpu_profiler::set_enabled(false);
179 if !ok {
180 return Err("gemm_parity_probe: GPU or CPU path returned false".into());
181 }
182 Ok((
183 crate::llm_kernel_parity::max_abs_err(&gpu_out, &cpu_out),
184 crate::llm_kernel_parity::mean_abs_err(&gpu_out, &cpu_out),
185 crate::llm_kernel_parity::max_ulp_diff(&gpu_out, &cpu_out),
186 calls,
187 ))
188}
189
190#[cfg(not(target_arch = "wasm32"))]
195pub fn gemm_parity_probe_f16_blocking(
196 n_in: usize,
197 n_out: usize,
198 seed: u64,
199) -> Result<(f32, f64, u64, u64), String> {
200 use crate::gguf_sharder::GgufTensorInfo;
201 if n_in == 0 || n_out == 0 {
202 return Err("n_in and n_out must be > 0".into());
203 }
204 let rt = tokio::runtime::Builder::new_multi_thread()
205 .worker_threads(2)
206 .enable_all()
207 .build()
208 .map_err(|e| e.to_string())?;
209 let mut engine = rt
210 .block_on(crate::gguf_bridge::QTensorEngine::try_new())
211 .map_err(|e| format!("engine init: {e}"))?;
212 let _guard = rt.enter();
213
214 let mut s = seed | 1;
215 let mut rng = move || -> f32 {
216 s = s
217 .wrapping_mul(6364136223846793005)
218 .wrapping_add(1442695040888963407);
219 ((s >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0
220 };
221
222 let row_bytes = crate::llm_kernel_parity::f16_bytes(n_in);
223 let mut raw = vec![0u8; row_bytes * n_out];
224 let mut row_f32 = vec![0f32; n_in];
225 for r in 0..n_out {
226 for x in row_f32.iter_mut() {
227 *x = rng();
228 }
229 if !crate::llm_kernel_parity::quantize_f16_from_f32(
230 &row_f32,
231 &mut raw[r * row_bytes..(r + 1) * row_bytes],
232 ) {
233 return Err("f16 quantize failed".into());
234 }
235 }
236 let input: Vec<f32> = (0..n_in).map(|_| rng()).collect();
237
238 let info = GgufTensorInfo {
239 dims: [n_in as u64, n_out as u64, 1, 1],
240 n_dims: 2,
241 ggml_type: crate::ggml_quants::GGML_TYPE_F16,
242 byte_offset: 0,
243 };
244
245 crate::llm_gpu_profiler::set_enabled(true);
246 crate::llm_gpu_profiler::reset();
247 let mut gpu_out = vec![0f32; n_out];
248 let mut cpu_out = vec![0f32; n_out];
249 let ok = engine.gemm_parity_probe(&info, &raw, &input, &mut gpu_out, &mut cpu_out, n_in, n_out);
250 let calls = crate::llm_gpu_profiler::snapshot()
251 .iter()
252 .find(|t| matches!(t.phase, crate::llm_gpu_profiler::Phase::Gemm))
253 .map(|t| t.calls)
254 .unwrap_or(0);
255 crate::llm_gpu_profiler::set_enabled(false);
256 if !ok {
257 return Err("gemm_parity_probe (f16): GPU or CPU path returned false".into());
258 }
259 Ok((
260 crate::llm_kernel_parity::max_abs_err(&gpu_out, &cpu_out),
261 crate::llm_kernel_parity::mean_abs_err(&gpu_out, &cpu_out),
262 crate::llm_kernel_parity::max_ulp_diff(&gpu_out, &cpu_out),
263 calls,
264 ))
265}
266
267#[cfg(not(target_arch = "wasm32"))]
275pub fn perplexity_eval_blocking(model_path: &str, max_tok: usize) -> Result<(f64, usize), String> {
276 use crate::gguf_bridge::QTensorEngine;
277 use crate::gguf_sharder::{GgufTensorIndex, GgufTokenizer};
278
279 let corpus = crate::llm_eval::load_corpus().map_err(|e| format!("corpus load: {e}"))?;
280 if corpus.is_empty() {
281 return Err("eval corpus is empty".into());
282 }
283 let model_path = model_path.to_string();
284
285 std::thread::spawn(move || -> Result<(f64, usize), String> {
286 let rt = tokio::runtime::Builder::new_current_thread()
287 .enable_all()
288 .build()
289 .map_err(|e| e.to_string())?;
290 let _g = rt.enter();
291
292 let mut engine = QTensorEngine::new();
293 let mut magic = [0u8; 4];
294 let is_q42 = {
295 use std::io::Read;
296 std::fs::File::open(&model_path)
297 .and_then(|mut f| f.read_exact(&mut magic))
298 .map(|_| &magic == b"p64\0")
299 .unwrap_or(false)
300 };
301 if is_q42 {
302 let f = std::fs::File::open(&model_path).map_err(|e| e.to_string())?;
303 let mmap = unsafe { memmap2::Mmap::map(&f) }.map_err(|e| e.to_string())?;
304 engine
305 .adopt_resident_p64_mmap(std::sync::Arc::new(mmap))
306 .map_err(|e| format!("q42 adopt: {e}"))?;
307 } else {
308 engine.load_gguf(&model_path);
309 }
310
311 let mmap = engine
312 .gguf_mmap
313 .clone()
314 .ok_or_else(|| "model did not memory-map (load failed)".to_string())?;
315 let is_q42_mmap = mmap.len() >= 4 && mmap[0..4] == *b"p64\0";
316 let tok = if is_q42_mmap {
317 crate::p64_weight::P64TensorIndex::from_p64(&mmap)
318 .ok()
319 .and_then(|qi| GgufTokenizer::from_p64_section(qi.tokenizer_bytes(&mmap)))
320 .unwrap_or_default()
321 } else {
322 GgufTokenizer::from_gguf(&mmap)
323 };
324 let tensor_idx = if is_q42_mmap {
325 crate::p64_weight::P64TensorIndex::from_p64(&mmap)
326 .map(|qi| qi.to_gguf_index())
327 .map_err(|e| format!("q42 index: {e}"))?
328 } else {
329 GgufTensorIndex::from_gguf(&mmap)
330 };
331
332 let emb_dim = tensor_idx.emb_dim();
333 if emb_dim == 0 {
334 return Err("embedding dimension is 0 (tensor index parse failed)".into());
335 }
336 let vocab = tok.vocab_len().max(1) as usize;
337
338 let mut emb_buf = vec![0f32; emb_dim.max(8192)];
339 let mut scratch_a = vec![0f32; 16384];
340 let mut scratch_b = vec![0f32; 16384];
341 let mut logits = vec![0f32; vocab];
342 let mmap_bytes: &[u8] = &mmap;
343
344 let mut total_nll = 0.0f64;
345 let mut total_tok = 0usize;
346 for passage in &corpus {
347 let toks = tok.encode(passage);
348 if toks.len() < 2 {
349 continue;
350 }
351 let limit = if max_tok > 0 {
352 (max_tok + 1).min(toks.len())
353 } else {
354 toks.len()
355 };
356 engine.reset_kv_cache();
357 for i in 0..limit - 1 {
358 let n_emb = tensor_idx.dequantize_token_embedding_into(
359 mmap_bytes,
360 toks[i],
361 &mut emb_buf[..emb_dim],
362 );
363 if n_emb == 0 {
364 return Err(format!("embedding lookup failed for token {}", toks[i]));
365 }
366 crate::llm_awq::begin_forward();
369 let _ = engine.dispatch_transformer_forward(
370 &tensor_idx,
371 &mut emb_buf[..emb_dim],
372 emb_dim,
373 &mut scratch_a,
374 &mut scratch_b,
375 i as u32,
376 0, );
378 let _ =
379 engine.apply_output_norm_inplace(&tensor_idx, &mut emb_buf[..emb_dim], emb_dim);
380 let n = engine.dispatch_output_logits_into(
381 &tensor_idx,
382 &emb_buf[..emb_dim],
383 emb_dim,
384 &mut logits,
385 );
386 if n == 0 {
387 return Err("output projection produced no logits".into());
388 }
389 let nll = crate::llm_eval::token_nll(&logits[..n], toks[i + 1] as usize);
390 if nll.is_finite() {
391 total_nll += nll;
392 total_tok += 1;
393 }
394 }
395 }
396 if total_tok == 0 {
397 return Err("no tokens scored".into());
398 }
399 Ok((crate::llm_eval::perplexity(total_nll, total_tok), total_tok))
400 })
401 .join()
402 .map_err(|_| "perplexity eval thread panicked".to_string())?
403}
404
405#[cfg(not(target_arch = "wasm32"))]
413pub fn capture_kv_gpu_readback(
414 model_path: &str,
415 max_tok: usize,
416 max_per_layer: usize,
417) -> Result<crate::kv_capture::KvCapture, String> {
418 use crate::gguf_bridge::QTensorEngine;
419 use crate::gguf_sharder::{GgufTensorIndex, GgufTokenizer};
420
421 let corpus = crate::llm_eval::load_corpus().map_err(|e| format!("corpus load: {e}"))?;
422 if corpus.is_empty() {
423 return Err("eval corpus is empty".into());
424 }
425 let model_path = model_path.to_string();
426
427 std::thread::spawn(move || -> Result<crate::kv_capture::KvCapture, String> {
428 let rt = tokio::runtime::Builder::new_current_thread()
429 .enable_all()
430 .build()
431 .map_err(|e| e.to_string())?;
432 let _g = rt.enter();
433
434 let prev_int8 = kv_int8_enabled();
436 set_kv_int8(false);
437
438 let mut engine = QTensorEngine::new();
439 let mut magic = [0u8; 4];
440 let is_q42 = {
441 use std::io::Read;
442 std::fs::File::open(&model_path)
443 .and_then(|mut f| f.read_exact(&mut magic))
444 .map(|_| &magic == b"p64\0")
445 .unwrap_or(false)
446 };
447 if is_q42 {
448 let f = std::fs::File::open(&model_path).map_err(|e| e.to_string())?;
449 let mmap = unsafe { memmap2::Mmap::map(&f) }.map_err(|e| e.to_string())?;
450 engine
451 .adopt_resident_p64_mmap(std::sync::Arc::new(mmap))
452 .map_err(|e| format!("q42 adopt: {e}"))?;
453 } else {
454 engine.load_gguf(&model_path);
455 }
456
457 let mmap = engine
458 .gguf_mmap
459 .clone()
460 .ok_or_else(|| "model did not memory-map (load failed)".to_string())?;
461 let is_q42_mmap = mmap.len() >= 4 && mmap[0..4] == *b"p64\0";
462 let tok = if is_q42_mmap {
463 crate::p64_weight::P64TensorIndex::from_p64(&mmap)
464 .ok()
465 .and_then(|qi| GgufTokenizer::from_p64_section(qi.tokenizer_bytes(&mmap)))
466 .unwrap_or_default()
467 } else {
468 GgufTokenizer::from_gguf(&mmap)
469 };
470 let tensor_idx = if is_q42_mmap {
471 crate::p64_weight::P64TensorIndex::from_p64(&mmap)
472 .map(|qi| qi.to_gguf_index())
473 .map_err(|e| format!("q42 index: {e}"))?
474 } else {
475 GgufTensorIndex::from_gguf(&mmap)
476 };
477
478 let emb_dim = tensor_idx.emb_dim();
479 if emb_dim == 0 {
480 return Err("embedding dimension is 0".into());
481 }
482 let mut emb_buf = vec![0f32; emb_dim.max(8192)];
483 let mut scratch_a = vec![0f32; 16384];
484 let mut scratch_b = vec![0f32; 16384];
485 let mmap_bytes: &[u8] = &mmap;
486
487 let mut acc_k: Vec<Vec<Vec<f32>>> = Vec::new();
488 let mut acc_v: Vec<Vec<Vec<f32>>> = Vec::new();
489 let mut head_dim = 0usize;
490
491 'corpus: for passage in &corpus {
492 let toks = tok.encode(passage);
493 if toks.len() < 2 {
494 continue;
495 }
496 let limit = if max_tok > 0 {
497 (max_tok + 1).min(toks.len())
498 } else {
499 toks.len()
500 };
501 engine.reset_kv_cache();
502 for i in 0..limit - 1 {
503 let n_emb = tensor_idx.dequantize_token_embedding_into(
504 mmap_bytes,
505 toks[i],
506 &mut emb_buf[..emb_dim],
507 );
508 if n_emb == 0 {
509 return Err(format!("embedding lookup failed for token {}", toks[i]));
510 }
511 let _ = engine.dispatch_transformer_forward(
512 &tensor_idx,
513 &mut emb_buf[..emb_dim],
514 emb_dim,
515 &mut scratch_a,
516 &mut scratch_b,
517 i as u32,
518 0,
519 );
520 }
521 if let Some(cap) = engine.capture_kv_f32((limit - 1) as u32, max_per_layer) {
523 head_dim = cap.head_dim;
524 if acc_k.is_empty() {
525 acc_k = vec![Vec::new(); cap.k.len()];
526 acc_v = vec![Vec::new(); cap.v.len()];
527 }
528 let mut all_full = true;
529 for l in 0..cap.k.len().min(acc_k.len()) {
530 for vk in &cap.k[l] {
531 if acc_k[l].len() < max_per_layer {
532 acc_k[l].push(vk.clone());
533 }
534 }
535 for vv in &cap.v[l] {
536 if acc_v[l].len() < max_per_layer {
537 acc_v[l].push(vv.clone());
538 }
539 }
540 if acc_k[l].len() < max_per_layer {
541 all_full = false;
542 }
543 }
544 if all_full {
545 break 'corpus; }
547 }
548 }
549
550 set_kv_int8(prev_int8);
551 if head_dim == 0 {
552 return Err("no KV captured via GPU readback (int8 layout, or empty forward)".into());
553 }
554 Ok(crate::kv_capture::KvCapture {
555 head_dim,
556 k: acc_k,
557 v: acc_v,
558 })
559 })
560 .join()
561 .map_err(|_| "kv capture thread panicked".to_string())?
562}
563
564#[cfg(not(target_arch = "wasm32"))]
571pub fn awq_sweep_blocking(
572 gguf_path: &str,
573 alphas: &[f32],
574 max_tok: usize,
575 quant: crate::p64_weight::FfnQuant,
576) -> Result<(f64, Vec<(f32, f64, f64)>), String> {
577 use crate::p64_weight::compile_gguf_to_q42_ffn_quant_awq;
578
579 let bytes = std::fs::read(gguf_path).map_err(|e| format!("read gguf: {e}"))?;
580 let idx = crate::gguf_sharder::GgufTensorIndex::from_gguf(&bytes);
581 let n_layer = idx.hyperparams.n_layer;
582 let n_embd = idx.hyperparams.n_embd;
583 if n_layer == 0 || n_embd == 0 {
584 return Err("gguf parse failed (n_layer/n_embd = 0)".into());
585 }
586
587 set_ternary_ffn(false);
589 crate::llm_awq::enable(n_layer, n_embd)?;
590 let (ref_ppl, _) = perplexity_eval_blocking(gguf_path, max_tok)?;
591 let stats = crate::llm_awq::snapshot();
592 crate::llm_awq::disable();
593 if stats.is_empty() {
594 return Err("AWQ: no activation stats captured".into());
595 }
596
597 set_ternary_ffn(matches!(quant, crate::p64_weight::FfnQuant::Ternary));
600 let tmp = std::env::temp_dir();
601 let mut results = Vec::with_capacity(alphas.len());
602 for &alpha in alphas {
603 let scales = if alpha == 0.0 {
604 None
605 } else {
606 Some(stats.as_slice())
607 };
608 let q42 = compile_gguf_to_q42_ffn_quant_awq(&bytes, 14, scales, alpha, quant)
609 .map_err(|e| format!("AWQ compile (alpha={alpha}): {e}"))?;
610 let path = tmp.join(format!("awq_sweep_a{:.2}.q42", alpha));
611 std::fs::write(&path, &q42).map_err(|e| format!("write q42: {e}"))?;
612 let ps = path.to_string_lossy().to_string();
613 let (ppl, _) = perplexity_eval_blocking(&ps, max_tok)?;
614 let (text, _) = decode_with_metrics_blocking(&ps, "Once upon a time, there was a", 24)?;
615 let uniq = crate::llm_eval::unique_word_ratio(&text);
616 let _ = std::fs::remove_file(&path);
617 results.push((alpha, ppl, uniq));
618 }
619 set_ternary_ffn(false);
620 Ok((ref_ppl, results))
621}
622
623#[cfg(not(target_arch = "wasm32"))]
625pub fn decode_with_metrics_blocking(
626 model_path: &str,
627 prompt: &str,
628 decode_tokens: u32,
629) -> Result<(String, f64), String> {
630 let rt = tokio::runtime::Builder::new_multi_thread()
631 .worker_threads(2)
632 .enable_all()
633 .build()
634 .map_err(|e| e.to_string())?;
635 rt.block_on(async { decode_with_metrics(model_path, prompt, decode_tokens) })
636}
637
638#[cfg(not(target_arch = "wasm32"))]
641pub fn decode_sampled_blocking(
642 model_path: &str,
643 prompt: &str,
644 decode_tokens: u32,
645 cfg: crate::sampler::SamplerConfig,
646) -> Result<(String, f64), String> {
647 set_sampler_config(Some(cfg));
648 let out = decode_with_metrics_blocking(model_path, prompt, decode_tokens);
649 set_sampler_config(None);
650 out
651}
652
653#[cfg(not(target_arch = "wasm32"))]
663pub fn spec_verify_probe_blocking(
664 model_path: &str,
665 prompt: &str,
666 b: usize,
667) -> Result<(Vec<u32>, Vec<u32>, Vec<u32>), String> {
668 use crate::gguf_bridge::QTensorEngine;
669 use crate::gguf_sharder::{GgufTensorIndex, GgufTokenizer};
670 if !std::path::Path::new(model_path).exists() {
671 return Err(format!("model not found: {model_path}"));
672 }
673 let model_path = model_path.to_string();
674 let prompt = prompt.to_string();
675
676 std::thread::spawn(move || -> Result<(Vec<u32>, Vec<u32>, Vec<u32>), String> {
677 let rt = tokio::runtime::Builder::new_current_thread()
678 .enable_all()
679 .build()
680 .map_err(|e| e.to_string())?;
681 let _g = rt.enter();
682
683 let mut engine = QTensorEngine::new();
684 engine.load_gguf(&model_path);
685 let mmap = engine
686 .gguf_mmap
687 .clone()
688 .ok_or_else(|| "model did not memory-map".to_string())?;
689 let tok = GgufTokenizer::from_gguf(&mmap);
690 let idx = GgufTensorIndex::from_gguf(&mmap);
691 if !engine.mc8_upload_resident_logits(&idx) {
694 return Err("resident logits upload failed (verify tail needs it)".into());
695 }
696 let emb_dim = idx.emb_dim();
697 if emb_dim == 0 {
698 return Err("embedding dimension is 0".into());
699 }
700 let vocab = tok.vocab_len().max(1) as usize;
701 let toks = tok.encode(&prompt);
702 if toks.len() < b + 2 {
703 return Err(format!(
704 "prompt too short: {} tokens, need >= {}",
705 toks.len(),
706 b + 2
707 ));
708 }
709
710 let mut emb = vec![0f32; emb_dim.max(8192)];
711 let mut sa = vec![0f32; 16384];
712 let mut sb = vec![0f32; 16384];
713 let mut logits = vec![0f32; vocab];
714 let mmap_b: &[u8] = &mmap;
715
716 let argmax = |v: &[f32]| -> u32 {
717 let mut best_i = 0u32;
718 let mut best_v = f32::NEG_INFINITY;
719 for (i, &x) in v.iter().enumerate() {
720 if x > best_v {
721 best_v = x;
722 best_i = i as u32;
723 }
724 }
725 best_i
726 };
727
728 let p = toks.len() - 1;
730 engine.reset_kv_cache();
731 for i in 0..p {
732 let n = idx.dequantize_token_embedding_into(mmap_b, toks[i], &mut emb[..emb_dim]);
733 if n == 0 {
734 return Err(format!("embedding lookup failed for token {}", toks[i]));
735 }
736 let _ = engine.dispatch_transformer_forward(
737 &idx,
738 &mut emb[..emb_dim],
739 emb_dim,
740 &mut sa,
741 &mut sb,
742 i as u32,
743 0,
744 );
745 }
746 let mut inputs: Vec<u32> = Vec::with_capacity(b);
753 let mut reference: Vec<u32> = Vec::with_capacity(b);
754 let mut input = toks[p];
755 let mut pos = p as u32;
756 for _ in 0..b {
757 inputs.push(input);
758 let n = idx.dequantize_token_embedding_into(mmap_b, input, &mut emb[..emb_dim]);
759 if n == 0 {
760 return Err("reference embedding lookup failed".into());
761 }
762 let _ = engine.dispatch_transformer_forward(
763 &idx,
764 &mut emb[..emb_dim],
765 emb_dim,
766 &mut sa,
767 &mut sb,
768 pos,
769 0,
770 );
771 let _ = engine.apply_output_norm_inplace(&idx, &mut emb[..emb_dim], emb_dim);
772 let nl =
773 engine.dispatch_output_logits_into(&idx, &emb[..emb_dim], emb_dim, &mut logits);
774 if nl == 0 {
775 return Err("reference output projection produced no logits".into());
776 }
777 let out = argmax(&logits[..nl]);
778 reference.push(out);
779 input = out;
780 pos += 1;
781 }
782
783 let mut verify_out: Vec<u32> = Vec::new();
785 let mut verify_logit: Vec<f32> = Vec::new();
786 engine
787 .verify_draft_batch(&idx, &inputs, p as u32, &mut verify_out, &mut verify_logit)
788 .ok_or_else(|| "verify_draft_batch ineligible/failed".to_string())?;
789
790 engine.reset_kv_cache();
795 for i in 0..p {
796 let n = idx.dequantize_token_embedding_into(mmap_b, toks[i], &mut emb[..emb_dim]);
797 if n == 0 {
798 return Err("resident-ref prefill embedding failed".into());
799 }
800 let _ = engine.dispatch_transformer_forward(
801 &idx,
802 &mut emb[..emb_dim],
803 emb_dim,
804 &mut sa,
805 &mut sb,
806 i as u32,
807 0,
808 );
809 }
810 let mut resident_out: Vec<u32> = Vec::with_capacity(b);
811 for (j, &t) in inputs.iter().enumerate() {
812 let n = idx.dequantize_token_embedding_into(mmap_b, t, &mut emb[..emb_dim]);
813 if n == 0 {
814 return Err("resident-ref embedding failed".into());
815 }
816 match engine.dispatch_token_forward_resident(&idx, &emb[..emb_dim], (p + j) as u32) {
817 Some(r) => resident_out.push(r.best_token_id),
818 None => resident_out.push(u32::MAX),
819 }
820 }
821
822 Ok((reference, verify_out, resident_out))
823 })
824 .join()
825 .map_err(|_| "spec-verify probe thread panicked".to_string())?
826}