qualia_core_db/inference/inference_agent/decode.rs
1// Phase 8 bifurcated-compute decode path for `LocalLlmAgent`.
2//
3// This is the LIVE LLM decode agent: it loads the GGUF/P64 model, tokenises,
4// runs the autoregressive loop through `QTensorEngine`, and streams logit
5// summaries from the LLM engine thread to the Webizen Sentinel over wait-free
6// SPSC ring buffers. Moved verbatim from the `inference_agent.rs` monolith —
7// no logic, control-flow, or signature changes.
8
9#[cfg(not(target_arch = "wasm32"))]
10use super::config::effective_inference_timeout_ms;
11use super::config::{DECODE_TOKEN_BUDGET, TEST_TRANSFORMER_LAYER_CAP, TEST_VOCAB_CHUNK_CAP};
12#[cfg(not(target_arch = "wasm32"))]
13use super::decode_helpers::get_prefix_cache;
14use super::decode_helpers::{
15 apply_model_helper_stops, build_sieve, drain_tensor_context_inject, embedding_fallback_logits,
16 try_accept_topology_draft, TopologyDraftStep,
17};
18use super::local_agent::LocalLlmAgent;
19#[cfg(not(target_arch = "wasm32"))]
20use super::sticky_infer;
21use super::types::AgentBackend;
22use crate::{q_hash, NQuin};
23
24impl LocalLlmAgent {
25 /// Phase 8: Bifurcated Compute — SPSC Wait-Free Intercept.
26 ///
27 /// On native targets: loads the GGUF model, tokenises the prompt, and runs an
28 /// autoregressive decode loop via `QTensorEngine::dispatch_fused_transformer_block`.
29 /// Logit summaries flow from the LLM engine thread to the Webizen Sentinel (this
30 /// thread) over a wait-free SPSC ring. The Sentinel may inject `DenyRollback`
31 /// for real governance signals; the old IEEE-754 `0x99` mantissa check was
32 /// removed (it fired randomly ~1/256 tokens and corrupted the stream).
33 ///
34 /// On WASM / non-local backends: falls through to the original mock path.
35 /// Run local inference, optionally streaming decoded text deltas to `on_token`.
36 pub fn infer_local_model_streaming<F: FnMut(String) + Send + 'static>(
37 &self,
38 prompt: &str,
39 graph_context: &str,
40 on_token: Option<F>,
41 ) -> (String, Vec<u64>, u32, Option<NQuin>) {
42 self.infer_local_model_inner(prompt, graph_context, on_token)
43 }
44
45 pub(super) fn infer_local_model(
46 &self,
47 prompt: &str,
48 graph_context: &str,
49 ) -> (String, Vec<u64>, u32, Option<NQuin>) {
50 self.infer_local_model_inner::<fn(String)>(prompt, graph_context, None)
51 }
52
53 #[cfg_attr(target_arch = "wasm32", allow(unused_variables, unused_mut))]
54 fn infer_local_model_inner<F: FnMut(String) + Send + 'static>(
55 &self,
56 prompt: &str,
57 graph_context: &str,
58 mut on_token: Option<F>,
59 ) -> (String, Vec<u64>, u32, Option<NQuin>) {
60 let prov_hash = graph_context
61 .bytes()
62 .take(8)
63 .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64));
64 let use_sieve = self
65 .use_sieve_output
66 .load(std::sync::atomic::Ordering::Relaxed);
67 let sieve_spec = if use_sieve {
68 Some(*self.sieve_spec.lock().unwrap_or_else(|e| e.into_inner()))
69 } else {
70 None
71 };
72 let sieve_lex_path = if use_sieve {
73 self.sieve_lex_path
74 .lock()
75 .unwrap_or_else(|e| e.into_inner())
76 .clone()
77 } else {
78 None
79 };
80
81 // ── Native GPU path ─────────────────────────────────────────────────
82 #[cfg(not(target_arch = "wasm32"))]
83 {
84 use crate::gguf_bridge::{QTensor, QTensorEngine};
85 use crate::gguf_sharder::GgufTokenizer;
86 use rtrb::RingBuffer;
87
88 let model_path = match &self.backend {
89 AgentBackend::Local { model_path, .. } => model_path.clone(),
90 _ => {
91 return (
92 String::from("[no local model configured]"),
93 vec![prov_hash],
94 0,
95 None,
96 );
97 }
98 };
99 let prompt_owned = prompt.to_string();
100
101 // Multi-mode: portable | cuda | quant-graph (`QUALIA_INFERENCE_MODE`).
102 let _mode = crate::inference_modes::bootstrap_inference_mode();
103
104 // ── LoRA context detection (before thread spawn) ─────────────────
105 // Detect the prompt domain and pre-load the matching LoRA adapter.
106 // The pre-computed delta vectors are cloned into the inference thread
107 // as fixed-size heap data — one allocation per infer call, not per token.
108 #[allow(unused_variables)]
109 let lora_active_adapter: Option<crate::lora::LoRAAdapter> = {
110 let mut guard = self.lora_manager.lock().unwrap_or_else(|e| e.into_inner());
111 if let Some(ref mut mgr) = *guard {
112 let (ctx, conf, _switched) =
113 mgr.auto_switch(&prompt_owned, mgr.detector.confidence_threshold);
114 log::debug!("LoRA|context-detect|domain={ctx}|conf={conf:.3}");
115 mgr.active().cloned()
116 } else {
117 None
118 }
119 };
120
121 // Fixed-size types keep the hot-path allocation-free in the ring buffer.
122 #[derive(Clone, Copy)]
123 struct LogitSummary {
124 _top_id: u32,
125 anomaly: u8,
126 }
127 #[derive(Clone)]
128 enum LlmMsg {
129 Logit(LogitSummary),
130 Eos,
131 }
132 #[derive(Clone)]
133 enum SentMsg {
134 DenyRollback,
135 }
136
137 // LogitStream: LLM engine → Webizen Sentinel
138 let (mut lp, mut lc) = RingBuffer::<LlmMsg>::new(1024);
139 // ControlStream: Webizen Sentinel → LLM engine
140 let (mut cp, mut cc) = RingBuffer::<SentMsg>::new(16);
141
142 let stream_pair = if on_token.is_some() {
143 Some(std::sync::mpsc::sync_channel::<String>(512))
144 } else {
145 None
146 };
147 let stream_tx_thread = stream_pair.as_ref().map(|(tx, _)| tx.clone());
148
149 // Move the (optional) LoRA adapter into the inference thread.
150 let lora_for_thread = lora_active_adapter;
151
152 // Sticky pool thread owns the engine (thread_local); caller runs Sentinel.
153 let (done_tx, done_rx) =
154 std::sync::mpsc::sync_channel::<(String, u32, Option<NQuin>, bool)>(1);
155
156 // ── LLM engine job (sticky 1-thread pool) ────────────────────────
157 sticky_infer::pool().spawn(move || {
158 let result = sticky_infer::with_engine(
159 model_path.as_str(),
160 |engine| {
161 // Load only on cache miss / path change.
162 if let Some(mmap) =
163 crate::resident_model::resident_mmap_for_path(model_path.as_str())
164 {
165 let is_p64 = crate::p64_weight::has_p64_magic(&mmap[..]);
166 let adopted = if is_p64 {
167 engine.adopt_resident_p64_mmap(mmap).is_ok()
168 } else {
169 engine.adopt_resident_mmap(mmap).is_ok()
170 };
171 if !adopted {
172 engine.load_model(&model_path);
173 }
174 } else {
175 engine.load_model(&model_path);
176 }
177 },
178 |engine: &mut QTensorEngine| {
179 // Initialize Tokio runtime for the sticky thread (once per job; cheap if already warm).
180 let rt = tokio::runtime::Builder::new_current_thread()
181 .enable_all()
182 .build()
183 .unwrap_or_else(|e| {
184 panic!("Failed to create Tokio runtime for LLM thread: {}", e)
185 });
186 let _rt_guard = rt.enter();
187
188 // A0 phase timing (D17/D22): once-per-phase, off the per-token hot path.
189 let t_phase = std::time::Instant::now();
190
191 // Thermal-eviction WAL is a native-only file mmap (`memmap2`); on
192 // wasm there is no mmap'd-file WAL, so the telemetry is simply absent.
193 #[cfg(not(target_arch = "wasm32"))]
194 let mut thermal_wal_opt = {
195 let wal_path = std::env::var("QUALIA_DATA_DIR")
196 .map(|p| std::path::PathBuf::from(p).join("thermal_eviction.wal"))
197 .unwrap_or_else(|_| std::env::temp_dir().join("thermal_eviction.wal"));
198 crate::inference::thermal_wal::ThermalWal::open(&wal_path, 1024).ok()
199 };
200
201 let lora_adapter = lora_for_thread;
202 let sieve_spec = sieve_spec;
203 let sieve_lex_path = sieve_lex_path;
204
205 // Tokenizer + tensor index come from the matching on-disk
206 // format. P64 carries a Q42T tokenizer section and a manifest;
207 // GGUF carries both in its own metadata.
208 let is_p64_mmap = engine
209 .gguf_mmap
210 .as_ref()
211 .map(|m| crate::p64_weight::has_p64_magic(&m[..]))
212 .unwrap_or(false);
213 // Prefer engine-cached index from adopt (zero re-CRC); else parse once.
214 let p64_index = engine.p64_index.clone().or_else(|| {
215 if is_p64_mmap {
216 engine
217 .gguf_mmap
218 .as_ref()
219 .and_then(|m| crate::p64_weight::P64TensorIndex::from_p64(m).ok())
220 } else {
221 None
222 }
223 });
224 let mut tok = if let (Some(qi), Some(m)) =
225 (p64_index.as_ref(), engine.gguf_mmap.as_ref())
226 {
227 GgufTokenizer::from_p64_section(qi.tokenizer_bytes(m)).unwrap_or_default()
228 } else {
229 engine
230 .gguf_mmap
231 .as_ref()
232 .map(|m| GgufTokenizer::from_gguf(m))
233 .unwrap_or_default()
234 };
235 // Sibling canonical `.q42` metadata (convert-time stop set / chat metadata).
236 apply_model_helper_stops(&model_path, &mut tok);
237
238 let tensor_idx = engine.tensor_index_cache.clone().or_else(|| {
239 if let Some(qi) = p64_index {
240 Some(qi.to_gguf_index())
241 } else {
242 engine
243 .gguf_mmap
244 .as_ref()
245 .map(|m| crate::gguf_sharder::GgufTensorIndex::from_gguf(m))
246 }
247 });
248
249 let mut ctx = tok.encode_chat_prompt(&prompt_owned);
250 // Keep `eos` for draft/topology APIs that still take a single id; decode
251 // termination uses the full stop set (eos + chat end-of-turn specials).
252 let eos = tok.eos_token_id;
253 let vlen = tok.vocab_len().max(1);
254
255 // Use the real embedding dimension if the tensor was found; fall back to 4096.
256 let emb_dim = tensor_idx
257 .as_ref()
258 .map(|idx| idx.emb_dim())
259 .filter(|&d| d > 0)
260 .unwrap_or(4096);
261
262 // Stack buffers — zero-heap path (512MB floor safe).
263 use crate::gguf_bridge::{PREFILL_CHUNK_SIZE, PREFILL_CHUNK_STACK_FLOATS};
264
265 const MAX_EMB_DIM: usize = 8192;
266 const MAX_FFN_DIM: usize = 10240;
267 let mut emb_buf = [0f32; MAX_EMB_DIM];
268 let mut scratch_a = [0f32; MAX_FFN_DIM];
269 let mut scratch_b = [0f32; MAX_FFN_DIM];
270 let mut prefill_chunk = [0f32; PREFILL_CHUNK_STACK_FLOATS];
271 let emb_dim = emb_dim.min(MAX_EMB_DIM);
272 let mut prefix_cached = false;
273 if prov_hash != 0 {
274 if let Ok(cache) = get_prefix_cache().lock() {
275 if let Some(cached_kv) = cache.get(&prov_hash) {
276 engine.set_kv_cache_cpu(cached_kv);
277 prefix_cached = true;
278 }
279 }
280 }
281
282 if !prefix_cached {
283 engine.reset_kv_cache();
284 }
285
286 // Phase boundary: load (mmap/adopt + tokenizer + tensor index + setup) done.
287 crate::llm_bench::record_load_ns(t_phase.elapsed().as_nanos() as u64);
288 let t_prefill = std::time::Instant::now();
289
290 // Chunked prefill: populate KV for prompt tokens [0, prompt_len-1).
291 let prompt_len = ctx.len();
292 crate::tensor::kv_provenance::rebuild_prompt_provenance(
293 prompt_len as u32,
294 crate::tensor::resident_substrate::global_resident_substrate().node_count(),
295 0,
296 );
297 let draft_mapper = crate::topology_draft::TopologyDraftMapper::new(&tok);
298 if !prefix_cached {
299 if prompt_len > 1 {
300 if let Some(idx) = tensor_idx.as_ref() {
301 let prefill_tokens = prompt_len - 1;
302 let chunk_cap = (PREFILL_CHUNK_STACK_FLOATS / emb_dim)
303 .min(PREFILL_CHUNK_SIZE)
304 .max(1);
305 let mut pos = 0usize;
306 while pos < prefill_tokens {
307 let n = (prefill_tokens - pos).min(chunk_cap);
308 let batch_elems = n * emb_dim;
309 {
310 let mmap = match engine.gguf_mmap.as_deref() {
311 Some(m) => m,
312 None => break,
313 };
314 for t in 0..n {
315 let _ = idx.dequantize_token_embedding_into(
316 mmap,
317 ctx[pos + t],
318 &mut prefill_chunk[t * emb_dim..(t + 1) * emb_dim],
319 );
320 }
321 }
322 if !engine.dispatch_prefill_chunk(
323 idx,
324 &mut prefill_chunk[..batch_elems],
325 emb_dim,
326 n as u32,
327 pos as u32,
328 &mut scratch_a,
329 &mut scratch_b,
330 TEST_TRANSFORMER_LAYER_CAP,
331 ) {
332 crate::gguf_bridge::wlog(&format!(
333 "[llm] PREFILL chunk FAILED pos={pos} n={n}"
334 ));
335 }
336 pos += n;
337 }
338 }
339 }
340
341 if prov_hash != 0 {
342 if let Some(cpu_kv) = engine.get_kv_cache_cpu() {
343 if let Ok(mut cache) = get_prefix_cache().lock() {
344 cache.insert(prov_hash, cpu_kv.into());
345 }
346 }
347 }
348 }
349
350 // Phase boundary: prefill done. Decode phase begins below.
351 crate::llm_bench::record_prefill(
352 t_prefill.elapsed().as_nanos() as u64,
353 prompt_len.saturating_sub(1) as u64,
354 );
355
356 let mut out_ids: Vec<u32> = Vec::new();
357 let mut streamed_len = 0usize;
358 let mut sieve = if use_sieve {
359 build_sieve(&tok, sieve_spec.as_ref(), sieve_lex_path.as_deref())
360 } else {
361 None
362 };
363 let mut semantic_quin: Option<NQuin> = None;
364 let mut sieve_failed = false;
365 let gen_budget = if sieve.is_some() {
366 3usize
367 } else {
368 // Benchmark override (A0): a fixed decode count for stable tok/s; 0 = default.
369 let ov = crate::llm_bench::decode_budget_override();
370 if ov > 0 {
371 ov as usize
372 } else {
373 DECODE_TOKEN_BUDGET as usize
374 }
375 };
376
377 #[cfg(not(target_arch = "wasm32"))]
378 crate::compute_universe::start_tensor_search_producer();
379 crate::compute_universe::publish_query_tensor(
380 crate::tensor::Tensor10D::default(),
381 0,
382 );
383 // Qualia-unique hybrid: graph route mask + 10D query + deontic obligation.
384 // Must run *after* the default query publish so it is not wiped.
385 crate::qualia_hybrid::prepare_hybrid_decode(&prompt_owned);
386
387 let t_decode = std::time::Instant::now();
388 // A1a: GPU top-1 decode path toggle (default-on; QUALIA_LLM_GPU_TOPK / set_gpu_topk).
389 let gpu_topk_enabled = crate::llm_bench::gpu_topk_enabled();
390 // W2: exact CPU sampler. `None` ⇒ greedy argmax (pre-W2 byte-identical path). When
391 // active, decode uses the legacy forward (leaves the normed hidden in `emb_buf`),
392 // reads back the FULL logit vector, and runs the penalty/temp/top-k/top-p chain.
393 #[cfg(not(target_arch = "wasm32"))]
394 let mut sampler =
395 crate::llm_bench::sampler_config().map(crate::sampler::SamplerState::new);
396 #[cfg(target_arch = "wasm32")]
397 let mut sampler: Option<crate::sampler::SamplerState> = None;
398 let mut sampler_logits: Vec<f32> = Vec::new();
399 // Decode-profiler (gated): one-shot empty submit→wait baseline on the SAME device, so
400 // the bench can separate per-token fence latency from real kernel compute time.
401 #[cfg(not(target_arch = "wasm32"))]
402 if std::env::var("QUALIA_LLM_PROFILE_DECODE").is_ok() {
403 let n = 64u32;
404 crate::llm_bench::record_empty_rt(
405 engine.bench_empty_submit_roundtrip(n),
406 n as u64,
407 );
408 }
409
410 // Phase 6: Initialize Semantic Chunking State
411 let mut current_page_id = 100u64; // Starting mock page ID
412 let chunk_policy = crate::q42::q42_kvp::Q42ChunkPolicy {
413 max_tokens: 128,
414 semantic_shift_threshold: 0.0,
415 discourse_boundary_weight: 0.0,
416 attention_phase_weight: 0.0,
417 max_entropy_drop: -2.0, // A threshold that will trigger when top1 and top2 are close
418 thermal_pressure_bias: 0.0,
419 reserved: [0; 40],
420 };
421
422 // QUALIA_GRAPH_FORCE=1: emit grounded repair tokens without model decode.
423 let mut graph_force_emitted = false;
424 #[cfg(not(target_arch = "wasm32"))]
425 if let Some(forced) = crate::qualia_hybrid::force_fact_tokens(&prompt_owned, &|s| {
426 tok.encode(s)
427 }) {
428 for &tid in &forced {
429 let next = tid % vlen.max(1);
430 out_ids.push(next);
431 ctx.push(next);
432 if let Some(ref tx) = stream_tx_thread {
433 let full = tok.decode(&out_ids);
434 if full.len() > streamed_len {
435 let delta = full[streamed_len..].to_string();
436 streamed_len = full.len();
437 let _ = tx.send(delta);
438 }
439 }
440 let fixed = crate::llm_bench::decode_budget_fixed_tokens();
441 if out_ids.len() >= gen_budget
442 || (!fixed && tok.is_stop_token(next))
443 {
444 break;
445 }
446 }
447 graph_force_emitted = true;
448 }
449
450 if !graph_force_emitted {
451 for step in 0..gen_budget {
452 crate::gpu_context::record_llm_decode_step();
453
454 // Codex P0 — cooperative deadline: break BEFORE the wall-clock timeout instead of
455 // the old post-hoc check in infer() that let a no-EOS run continue for minutes.
456 // t_decode starts at decode entry (post-prefill); INFERENCE_TIMEOUT_MS bounds the
457 // generation phase so the call never appears frozen.
458 if t_decode.elapsed().as_millis() as u64 >= effective_inference_timeout_ms() {
459 break;
460 }
461
462 // W7 — periodic GPU thermal check during sustained decode: recommends a TDP cap,
463 // and (when the auto-cap user option is on + a real NVML governor is present)
464 // applies it under sustained Critical, restoring on cool-down. Cheap NVML read
465 // every 32 tokens; no-op without the `nvml` feature or an NVIDIA card.
466 #[cfg(not(target_arch = "wasm32"))]
467 if step > 0 && step % 32 == 0 {
468 crate::inference::thermal_telemetry::thermal_tick();
469 }
470
471 // W6a — prompt-lookup / graph fact speculative decode (default OFF, exact-output).
472 // Prefer quant-graph fact draft when mode=quant-graph; else n-gram prompt-lookup.
473 // FastVerify: skip mid-decode fact draft (post-turn heal only) for Ollama-like speed.
474 // Verify drafts in ONE batched forward. Bit-identical to greedy when accepted.
475 #[cfg(not(target_arch = "wasm32"))]
476 if (crate::llm_bench::spec_decode_enabled()
477 || (crate::inference_modes::quant_graph_grounding_enabled()
478 && crate::inference_modes::sentinel_mid_decode_enabled()))
479 && sieve.is_none()
480 && sampler.is_none()
481 && TEST_TRANSFORMER_LAYER_CAP == 0
482 {
483 if let Some(idx) = tensor_idx.as_ref() {
484 let cur = *ctx.last().unwrap_or(&tok.bos_token_id);
485 let draft = crate::qualia_hybrid::propose_best_draft(
486 &prompt_owned,
487 &ctx,
488 &|s| tok.encode(s),
489 );
490 if draft.len > 0 {
491 // inputs = [cur, d0..d_{m-1}] at positions [pos, pos+m], pos = ctx.len()-1.
492 let mut inputs = Vec::with_capacity(draft.len + 1);
493 inputs.push(cur);
494 inputs.extend_from_slice(draft.as_slice());
495 let pos = ctx.len().saturating_sub(1) as u32;
496 let mut amax: Vec<u32> = Vec::new();
497 let mut alog: Vec<f32> = Vec::new();
498 if engine
499 .verify_draft_batch(idx, &inputs, pos, &mut amax, &mut alog)
500 .is_some()
501 && amax.len() == inputs.len()
502 {
503 // Accept the longest prefix where argmax[i] == draft[i]; then emit
504 // d0..d_{k-1} (accepted) + argmax[k] (correction/bonus) = k+1 tokens.
505 let m = draft.len;
506 let mut k = 0usize;
507 while k < m && amax[k] == draft.tokens[k] {
508 k += 1;
509 }
510 crate::llm_bench::record_spec_step(m as u64, k as u64);
511 let mut stop = false;
512 for i in 0..=k {
513 let (tokn, _logv) = if i < k {
514 (draft.tokens[i], alog[i])
515 } else {
516 (amax[k], alog[k])
517 };
518 // anomaly 0x01 = normal. Do not use random mantissa bytes.
519 let _ = lp.push(LlmMsg::Logit(LogitSummary {
520 _top_id: tokn,
521 anomaly: 0x01u8,
522 }));
523 let next = tokn % vlen;
524 out_ids.push(next);
525 ctx.push(next);
526 if let Some(ref tx) = stream_tx_thread {
527 let full = tok.decode(&out_ids);
528 if full.len() > streamed_len {
529 let delta = full[streamed_len..].to_string();
530 streamed_len = full.len();
531 let _ = tx.send(delta);
532 }
533 }
534 let fixed = crate::llm_bench::decode_budget_fixed_tokens();
535 if out_ids.len() >= gen_budget
536 || (!fixed && tok.is_stop_token(next))
537 {
538 stop = true;
539 break;
540 }
541 }
542 if stop {
543 break;
544 }
545 continue; // skip the normal single-token path this step
546 }
547 }
548 }
549 }
550
551 let draft_step = try_accept_topology_draft(
552 engine,
553 tensor_idx.as_ref(),
554 &draft_mapper,
555 &mut ctx,
556 emb_dim,
557 &mut emb_buf,
558 &mut scratch_a,
559 &mut scratch_b,
560 &mut out_ids,
561 &mut sieve,
562 prov_hash,
563 eos,
564 &tok,
565 &mut streamed_len,
566 stream_tx_thread.as_ref(),
567 None,
568 );
569 match draft_step {
570 TopologyDraftStep::AcceptedFull => continue,
571 TopologyDraftStep::Stop {
572 sieve_failed: sf,
573 semantic_quin: sq,
574 } => {
575 sieve_failed = sf;
576 semantic_quin = sq;
577 break;
578 }
579 _ => {}
580 }
581
582 drain_tensor_context_inject();
583 let _attention_mask = crate::compute_universe::attention_route_mask();
584 // FastVerify: skip ControlStream — no mid-decode DenyRollback tax.
585 let mut rollback = if crate::inference_modes::sentinel_mid_decode_enabled() {
586 cc.pop().is_ok()
587 } else {
588 false
589 };
590 if matches!(draft_step, TopologyDraftStep::Denied) {
591 rollback = true;
592 }
593
594 let cur = *ctx.last().unwrap_or(&tok.bos_token_id);
595 crate::compute_universe::publish_decode_hint(cur, step as u32);
596
597 // 1) Embedding lookup → hidden state (stack dequant).
598 let hidden_ok = tensor_idx
599 .as_ref()
600 .and_then(|idx| {
601 engine.gguf_mmap.as_deref().map(|m| {
602 idx.dequantize_token_embedding_into(m, cur, &mut emb_buf[..emb_dim])
603 })
604 })
605 .unwrap_or(0);
606
607 // 1b) LoRA delta — additive correction to the embedding vector.
608 // Applied after dequantize so the base model is unmodified.
609 // Silently skipped if dimensions don't match (wrong adapter for model).
610 if hidden_ok > 0 {
611 if let Some(ref adapter) = lora_adapter {
612 if adapter.meta.n_in == hidden_ok && adapter.meta.n_out == hidden_ok {
613 let snap: Vec<f32> = emb_buf[..hidden_ok].to_vec();
614 let _ = adapter.apply_cpu(&snap, &mut emb_buf[..hidden_ok]);
615 }
616 }
617 }
618
619 let (top_i, top_v) = if hidden_ok > 0 {
620 if let Some(idx) = tensor_idx.as_ref() {
621 let token_idx = ctx.len().saturating_sub(1) as u32;
622 let sieve_mask = sieve.as_ref().map(|s| s.current_mask());
623 // Resident-token fast path: the WHOLE forward (32 layers + output norm
624 // + logits top-1) in ONE submit with ONE fence. `Some` means the token
625 // was produced and the KV cache was written; `None` falls through to
626 // the legacy per-layer path unchanged (non-sieve, full-depth only —
627 // the unit-test 2-layer cap keeps its per-layer semantics).
628 // Resident single-fence forward:
629 // • greedy → GPU top-1 inside the encoder
630 // • sampler → same layer stack, read back post-norm hidden, then
631 // full logits + CPU sample (chat no longer pays ~107 fences/token)
632 #[cfg(not(target_arch = "wasm32"))]
633 let (resident_hit, resident_hidden_ok) = if sieve_mask.is_none()
634 && TEST_TRANSFORMER_LAYER_CAP == 0
635 {
636 let t_res = std::time::Instant::now();
637 if sampler.is_some() {
638 // Sampling does not need GPU top-1; resident still wins.
639 // Copy embedding input aside so out_hidden can reuse emb_buf.
640 scratch_a[..emb_dim].copy_from_slice(&emb_buf[..emb_dim]);
641 let ok = engine.dispatch_token_forward_resident_hidden(
642 idx,
643 &scratch_a[..emb_dim],
644 token_idx,
645 &mut emb_buf[..emb_dim],
646 );
647 if ok {
648 crate::llm_bench::add_decode_forward_ns(
649 t_res.elapsed().as_nanos() as u64,
650 );
651 crate::llm_bench::record_resident_hit();
652 (None, true)
653 } else {
654 crate::llm_bench::record_resident_fallback();
655 (None, false)
656 }
657 } else if gpu_topk_enabled {
658 let hit = engine.dispatch_token_forward_resident(
659 idx,
660 &emb_buf[..emb_dim],
661 token_idx,
662 );
663 if hit.is_some() {
664 crate::llm_bench::add_decode_forward_ns(
665 t_res.elapsed().as_nanos() as u64,
666 );
667 crate::llm_bench::record_resident_hit();
668 } else {
669 crate::llm_bench::record_resident_fallback();
670 }
671 (hit, false)
672 } else {
673 (None, false)
674 }
675 } else {
676 (None, false)
677 };
678 #[cfg(target_arch = "wasm32")]
679 let (resident_hit, resident_hidden_ok): (
680 Option<crate::gguf_bridge::StreamingArgmaxResult>,
681 bool,
682 ) = (None, false);
683
684 // CUDA mega-pass: attempt single-fence all-layer forward.
685 // Returns Some(token_id) when fully done (including logits).
686 // Returns Some(u32::MAX) when forward is done but logits projection
687 // is still needed (hidden state has been read back into emb_buf).
688 // Returns None to fall back to per-layer path.
689 #[cfg(all(not(target_arch = "wasm32"), feature = "cuda"))]
690 let mega_token: Option<u32> = if resident_hit.is_none() && !resident_hidden_ok {
691 engine.try_cuda_mega_pass_decode(
692 idx,
693 &mut emb_buf[..emb_dim],
694 emb_dim,
695 token_idx,
696 )
697 } else {
698 None
699 };
700 #[cfg(any(target_arch = "wasm32", not(feature = "cuda")))]
701 let mega_token: Option<u32> = None;
702
703 // mega_forward_done = mega-pass completed the 32-layer forward
704 // (either fully or just the forward — sentinel u32::MAX means
705 // forward done but logits still needed).
706 let mega_forward_done = mega_token.is_some();
707 let mega_full_token = mega_token.filter(|&t| t != u32::MAX);
708 #[cfg(all(not(target_arch = "wasm32"), feature = "cuda"))]
709 {
710 if mega_forward_done {
711 crate::llm_bench::record_cuda_mega_hit();
712 } else if matches!(
713 std::env::var("QUALIA_LLM_CUDA_DECODE").ok().as_deref(),
714 Some("1") | Some("true") | Some("on")
715 ) {
716 crate::llm_bench::record_cuda_mega_fallback();
717 }
718 }
719
720 if resident_hit.is_none() && !resident_hidden_ok {
721 if mega_forward_done {
722 crate::llm_bench::add_decode_forward_ns(0);
723 // Skip forward — mega-pass did it. If mega_full_token is
724 // Some, logits are also done. If None (sentinel), logits
725 // projection still runs below.
726 } else {
727 // Decode-profiler: time the 32-layer forward (legacy path).
728 let t_fwd = std::time::Instant::now();
729 let _layers = engine.dispatch_transformer_forward(
730 idx,
731 &mut emb_buf[..emb_dim],
732 emb_dim,
733 &mut scratch_a,
734 &mut scratch_b,
735 token_idx,
736 TEST_TRANSFORMER_LAYER_CAP,
737 );
738 let _ = engine.apply_output_norm_inplace(
739 idx,
740 &mut emb_buf[..emb_dim],
741 emb_dim,
742 );
743 crate::llm_bench::add_decode_forward_ns(
744 t_fwd.elapsed().as_nanos() as u64
745 );
746 }
747 }
748 // If mega-pass produced a full token, use it directly and skip projection.
749 // mega_full_token is None when the sentinel was returned (forward done,
750 // logits still needed) — in that case the output projection runs normally.
751 let mega_pass_done = mega_full_token.is_some();
752 let mega_pass_tok = mega_full_token.unwrap_or(0) as usize;
753 // Decode-profiler: time the output projection (argmax / top-k).
754 let t_out = std::time::Instant::now();
755 // W2: exact sampling — read back the FULL logit vector for this token and
756 // run the CPU chain. Only when a non-greedy sampler is installed; on any
757 // readback failure, `sampled` stays None and the greedy paths below run
758 // (never a silent hang). The legacy forward above left the normed hidden
759 // in `emb_buf`, so the projection input is correct.
760 #[cfg(not(target_arch = "wasm32"))]
761 let sampled: Option<(usize, f32)> = if let Some(s) = sampler.as_mut() {
762 let vocab = idx
763 .logits_projection_info()
764 .map(|i| QTensorEngine::matmul_dims(i).1)
765 .unwrap_or(0);
766 if vocab > 0 {
767 if sampler_logits.len() < vocab {
768 sampler_logits.resize(vocab, 0.0);
769 }
770 // Existing chunked projection; `written == vocab` iff it produced
771 // REAL logits (else it degraded to copying hidden → not sampleable,
772 // so we fall through to the greedy paths rather than sample garbage).
773 let written = engine.dispatch_output_logits_into(
774 idx,
775 &emb_buf[..emb_dim],
776 emb_dim,
777 &mut sampler_logits[..vocab],
778 );
779 if written == vocab {
780 // Neuro-symbolic: soft-boost graph answer tokens before sample.
781 let _ = crate::qualia_hybrid::apply_graph_logit_bias(
782 &prompt_owned,
783 &mut sampler_logits[..vocab],
784 &|s| {
785 let ids = tok.encode(s);
786 ids.first().copied()
787 },
788 );
789 let tid = s.sample(&mut sampler_logits[..vocab], &ctx);
790 Some((tid as usize, sampler_logits[tid as usize]))
791 } else {
792 None
793 }
794 } else {
795 None
796 }
797 } else {
798 None
799 };
800 #[cfg(target_arch = "wasm32")]
801 let sampled: Option<(usize, f32)> = None;
802
803 // A1a: GPU top-1 path (additive, default-on; non-sieve only in v1).
804 // Returns the argmax token via the on-GPU block reduction; falls
805 // through to the existing argmax path if disabled or on any failure — the
806 // working path is never bypassed. Skipped when sampling produced a token.
807 let topk_hit = if sampled.is_some() {
808 None
809 } else if resident_hit.is_some() {
810 resident_hit
811 } else if gpu_topk_enabled && sieve_mask.is_none() {
812 engine.dispatch_output_top1_chunked(
813 idx,
814 &emb_buf[..emb_dim],
815 emb_dim,
816 )
817 } else {
818 None
819 };
820 let out_sel = if mega_pass_done {
821 crate::llm_bench::add_decode_output_ns(0);
822 (mega_pass_tok, 0.0f32)
823 } else if let Some(sel) = sampled {
824 crate::llm_bench::record_sampled_token();
825 sel
826 } else if let Some(item) = topk_hit {
827 crate::llm_bench::record_topk_hit();
828 (item.best_token_id as usize, item.max_logit)
829 } else if let Some(argmax) = engine.dispatch_output_argmax_chunked(
830 idx,
831 &emb_buf[..emb_dim],
832 emb_dim,
833 &mut scratch_a[..],
834 TEST_VOCAB_CHUNK_CAP,
835 sieve_mask,
836 ) {
837 crate::llm_bench::record_argmax_fallback();
838 if argmax.max_logit > f32::NEG_INFINITY {
839 (argmax.best_token_id as usize, argmax.max_logit)
840 } else {
841 sieve_failed = true;
842 (0usize, f32::NEG_INFINITY)
843 }
844 } else {
845 let mut top1_v = f32::NEG_INFINITY;
846 let mut top1_i = 0usize;
847 let mut top2_v = f32::NEG_INFINITY;
848 for (i, &v) in emb_buf[..emb_dim].iter().enumerate() {
849 if v > top1_v {
850 top2_v = top1_v;
851 top1_v = v;
852 top1_i = i;
853 } else if v > top2_v {
854 top2_v = v;
855 }
856 }
857
858 // Phase 6: Semantic Chunking Entropy Calculation
859 let fast_entropy = -(top1_v - top2_v);
860 if fast_entropy > chunk_policy.max_entropy_drop {
861 current_page_id += 1;
862
863 #[cfg(not(target_arch = "wasm32"))]
864 if let Some(ref mut wal) = thermal_wal_opt {
865 let record =
866 crate::inference::thermal_wal::ThermalEvictionRecord {
867 timestamp_ms: std::time::SystemTime::now()
868 .duration_since(std::time::UNIX_EPOCH)
869 .unwrap_or_default()
870 .as_millis()
871 as u64,
872 page_id: (current_page_id - 1) as u32,
873 fast_entropy,
874 top1_v,
875 top2_v,
876 reserved: [0; 8],
877 };
878 wal.append(record);
879 }
880
881 if std::env::var("QUALIA_LLM_DEBUG_DECODE").is_ok() {
882 eprintln!(
883 "[NEW CHUNK] page_id={} entropy={:.3}",
884 current_page_id, fast_entropy
885 );
886 eprintln!(
887 "[THERMAL EVICT] Hard eviction logged for page_id={}",
888 current_page_id - 1
889 );
890 }
891 }
892
893 // Update KV provenance for this new token
894 let token_idx = ctx.len() as u32;
895 crate::tensor::kv_provenance::record_kv_provenance(
896 token_idx,
897 token_idx,
898 current_page_id,
899 );
900
901 (top1_i, top1_v)
902 };
903 crate::llm_bench::add_decode_output_ns(
904 t_out.elapsed().as_nanos() as u64
905 );
906 out_sel
907 } else {
908 (0usize, 0.0)
909 }
910 } else {
911 let wt = QTensor::new(vec![emb_dim, emb_dim], 0, true);
912 let logits = embedding_fallback_logits(
913 &engine,
914 tensor_idx.as_ref(),
915 lora_adapter.as_ref(),
916 cur,
917 emb_dim,
918 &mut emb_buf[..],
919 &wt,
920 );
921 logits.iter().enumerate().fold(
922 (0usize, f32::NEG_INFINITY),
923 |(bi, bv), (i, &v)| if v > bv { (i, v) } else { (bi, bv) },
924 )
925 };
926
927 // #48 diagnostic: reveal eos vs argmax for the first step (gated, native).
928 if step == 0 && std::env::var("QUALIA_LLM_DEBUG_DECODE").is_ok() {
929 eprintln!(
930 "[decode-dbg] step0 eos={} vlen={} prompt_last={} top_i={} top_v={} decoded={:?}",
931 eos,
932 vlen,
933 cur,
934 top_i,
935 top_v,
936 tok.decode(&[top_i as u32])
937 );
938 if let Some(idx) = tensor_idx.as_ref() {
939 if let Some(top5) = engine.dispatch_output_topk_chunked(
940 idx,
941 &emb_buf[..emb_dim],
942 emb_dim,
943 5,
944 ) {
945 for it in &top5 {
946 eprintln!(
947 "[top5] id={} logit={:.3} dec={:?}",
948 it.token_id,
949 it.logit,
950 tok.decode(&[it.token_id])
951 );
952 }
953 }
954 }
955 }
956
957 // FastVerify: skip per-token Logit ring push (only Eos ends the turn).
958 if crate::inference_modes::sentinel_mid_decode_enabled() {
959 // anomaly 0x01 = normal. Removed IEEE mantissa 0x99 check (random ~1/256 fire).
960 let _ = lp.push(LlmMsg::Logit(LogitSummary {
961 _top_id: top_i as u32,
962 anomaly: 0x01u8,
963 }));
964 }
965
966 if sieve_failed {
967 break;
968 }
969
970 // DenyRollback must never inject sequential garbage (cur+1).
971 if rollback {
972 log::warn!(
973 "LLM_DECODE|sentinel-deny-rollback|keeping argmax token {} (no cur+1)",
974 top_i
975 );
976 }
977 let next = (top_i as u32) % vlen;
978
979 if let Some(ref mut s) = sieve {
980 match s.apply_token(next) {
981 Ok(()) => {
982 out_ids.push(next);
983 ctx.push(next);
984 if s.is_complete() {
985 semantic_quin = Some(s.assemble_quin(prov_hash));
986 break;
987 }
988 }
989 Err(_) => {
990 sieve_failed = true;
991 break;
992 }
993 }
994 } else {
995 out_ids.push(next);
996 ctx.push(next);
997 if let Some(ref tx) = stream_tx_thread {
998 let full = tok.decode(&out_ids);
999 if full.len() > streamed_len {
1000 let delta = full[streamed_len..].to_string();
1001 streamed_len = full.len();
1002 let _ = tx.send(delta);
1003 }
1004 }
1005 // Stop on eos AND chat end-of-turn — unless fixed-token bench override.
1006 let fixed = crate::llm_bench::decode_budget_fixed_tokens();
1007 if out_ids.len() >= gen_budget
1008 || (!fixed && tok.is_stop_token(next))
1009 {
1010 break;
1011 }
1012 }
1013 }
1014 } // end else: normal decode (not QUALIA_GRAPH_FORCE)
1015
1016 // Phase boundary: decode loop complete.
1017 crate::llm_bench::record_decode(
1018 t_decode.elapsed().as_nanos() as u64,
1019 out_ids.len() as u64,
1020 );
1021
1022 let _ = lp.push(LlmMsg::Eos);
1023 let text = if semantic_quin.is_some() {
1024 String::new()
1025 } else if sieve_failed {
1026 String::from("[sieve-misaligned]")
1027 } else {
1028 let raw = tok.decode(&out_ids);
1029 // Post-turn path (FastVerify / QuantGraph): generate full draft at
1030 // full speed, then graph/CML self-heal + optional HTML surface.
1031 if crate::inference_modes::post_turn_verify_enabled() {
1032 let v = crate::post_turn_verify::verify_and_heal_turn(&prompt_owned, &raw);
1033 if crate::post_turn_verify::return_html_as_text() {
1034 v.display_html
1035 } else {
1036 v.final_text
1037 }
1038 } else {
1039 crate::quant_graph_grounding::maybe_ground_generation(&prompt_owned, &raw)
1040 .text
1041 }
1042 };
1043 (text, out_ids.len() as u32, semantic_quin, sieve_failed)
1044 }, // sticky_infer::with_engine f
1045 ); // sticky_infer::with_engine
1046 let _ = done_tx.send(result);
1047 }); // sticky pool spawn
1048
1049 // ── Webizen Sentinel (calling thread) ────────────────────────────
1050 // FastVerify: still drain stream + wait for Eos, but ignore anomaly mid-decode
1051 // (no DenyRollback) so generation is uninterrupted like Ollama.
1052 let mid_sentinel = crate::inference_modes::sentinel_mid_decode_enabled();
1053 let mut drain_tokens = || {
1054 if let (Some((_, ref rx)), Some(cb)) = (&stream_pair, on_token.as_mut()) {
1055 while let Ok(delta) = rx.try_recv() {
1056 cb(delta);
1057 }
1058 }
1059 };
1060
1061 loop {
1062 drain_tokens();
1063 match lc.pop() {
1064 Ok(LlmMsg::Eos) => break,
1065 Ok(LlmMsg::Logit(s)) => {
1066 if mid_sentinel && s.anomaly == 0x99 {
1067 let _ = cp.push(SentMsg::DenyRollback);
1068 }
1069 }
1070 Err(_) => std::hint::spin_loop(),
1071 }
1072 }
1073
1074 drain_tokens();
1075
1076 let (text, tokens, semantic_quin, sieve_failed) = done_rx
1077 .recv()
1078 .unwrap_or_else(|_| (String::new(), 0, None, false));
1079 let mut prov = vec![prov_hash];
1080 if prov_hash == 0 {
1081 prov.push(q_hash("qualia:grounded"));
1082 }
1083 if let Some(q) = semantic_quin {
1084 prov.push(q.subject);
1085 prov.push(q.predicate);
1086 prov.push(q.object);
1087 }
1088 if sieve_failed && semantic_quin.is_none() {
1089 return (text, prov, tokens, None);
1090 }
1091 return (text, prov, tokens, semantic_quin);
1092 }
1093
1094 // ── Native GPU path ─────────────────────────────────────────────────
1095 #[cfg(target_arch = "wasm32")]
1096 {
1097 use crate::gguf_bridge::QTensor;
1098 use crate::gguf_sharder::GgufTokenizer;
1099
1100 let model_path = match &self.backend {
1101 AgentBackend::Local { model_path, .. } => model_path.clone(),
1102 _ => {
1103 return (
1104 String::from("[no local model configured]"),
1105 vec![prov_hash],
1106 0,
1107 None,
1108 );
1109 }
1110 };
1111
1112 // ── WASM Extension Bus Offloading ────────────────────────────────
1113 if crate::extension_bus::wasm_bus::is_connected() {
1114 if let Some(cb) = on_token {
1115 let _ = crate::extension_bus::wasm_bus::send_intent(prompt, graph_context, cb);
1116 } else {
1117 let _ =
1118 crate::extension_bus::wasm_bus::send_intent(prompt, graph_context, |_| {});
1119 }
1120 return (String::new(), vec![prov_hash], 0, None);
1121 }
1122 let prompt_owned = prompt.to_string();
1123
1124 // Multi-mode: portable | cuda | quant-graph (`QUALIA_INFERENCE_MODE`).
1125 let _mode = crate::inference_modes::bootstrap_inference_mode();
1126
1127 // ── LoRA context detection (before thread spawn) ─────────────────
1128 // Detect the prompt domain and pre-load the matching LoRA adapter.
1129 // The pre-computed delta vectors are cloned into the inference thread
1130 // as fixed-size heap data — one allocation per infer call, not per token.
1131 #[allow(unused_variables)]
1132 let lora_active_adapter: Option<crate::lora::LoRAAdapter> = {
1133 let mut guard = self.lora_manager.lock().unwrap_or_else(|e| e.into_inner());
1134 if let Some(ref mut mgr) = *guard {
1135 let (ctx, conf, _switched) =
1136 mgr.auto_switch(&prompt_owned, mgr.detector.confidence_threshold);
1137 log::debug!("LoRA|context-detect|domain={ctx}|conf={conf:.3}");
1138 mgr.active().cloned()
1139 } else {
1140 None
1141 }
1142 };
1143
1144 // Move the (optional) LoRA adapter into the inference thread.
1145 let lora_for_thread = lora_active_adapter;
1146
1147 // ── LLM engine synchronous execution ─────────────────────────────
1148 let (text, tokens, semantic_quin, sieve_failed) = {
1149 let mut rollback = false;
1150
1151 let lora_adapter = lora_for_thread;
1152 let sieve_spec = sieve_spec;
1153 let sieve_lex_path = sieve_lex_path;
1154 // Build the GPU engine and memory-map the GGUF inside the thread to
1155 // avoid Send constraints on the DirectML / wgpu device handles.
1156 let mut engine = {
1157 let engine_guard =
1158 crate::gguf_bridge::WASM_ENGINE_INSTANCE.with(|g| g.borrow_mut().take());
1159 engine_guard.expect(
1160 "WASM WebGPU engine not initialized. Call initialize_webgpu_engine first.",
1161 )
1162 };
1163
1164 let is_p64_mmap = engine
1165 .gguf_mmap
1166 .as_ref()
1167 .map(|m| crate::p64_weight::has_p64_magic(&m[..]))
1168 .unwrap_or(false);
1169 let p64_index = if is_p64_mmap {
1170 engine
1171 .gguf_mmap
1172 .as_ref()
1173 .and_then(|m| crate::p64_weight::P64TensorIndex::from_p64(m).ok())
1174 } else {
1175 None
1176 };
1177 let mut tok =
1178 if let (Some(qi), Some(m)) = (p64_index.as_ref(), engine.gguf_mmap.as_ref()) {
1179 GgufTokenizer::from_p64_section(qi.tokenizer_bytes(m)).unwrap_or_default()
1180 } else {
1181 engine
1182 .gguf_mmap
1183 .as_ref()
1184 .map(|m| GgufTokenizer::from_gguf(m))
1185 .unwrap_or_default()
1186 };
1187 apply_model_helper_stops(&model_path, &mut tok);
1188
1189 let tensor_idx = if let Some(qi) = p64_index {
1190 Some(qi.to_gguf_index())
1191 } else {
1192 engine
1193 .gguf_mmap
1194 .as_ref()
1195 .map(|m| crate::gguf_sharder::GgufTensorIndex::from_gguf(m))
1196 };
1197
1198 let mut ctx = tok.encode_chat_prompt(&prompt_owned);
1199 let eos = tok.eos_token_id;
1200 let vlen = tok.vocab_len().max(1);
1201
1202 // Use the real embedding dimension if the tensor was found; fall back to 4096.
1203 let emb_dim = tensor_idx
1204 .as_ref()
1205 .map(|idx| idx.emb_dim())
1206 .filter(|&d| d > 0)
1207 .unwrap_or(4096);
1208
1209 // Stack buffers — zero-heap path (512MB floor safe).
1210 use crate::gguf_bridge::{PREFILL_CHUNK_SIZE, PREFILL_CHUNK_STACK_FLOATS};
1211
1212 const MAX_EMB_DIM: usize = 8192;
1213 const MAX_FFN_DIM: usize = 10240;
1214 let mut emb_buf = [0f32; MAX_EMB_DIM];
1215 let mut scratch_a = [0f32; MAX_FFN_DIM];
1216 let mut scratch_b = [0f32; MAX_FFN_DIM];
1217 let mut prefill_chunk = [0f32; PREFILL_CHUNK_STACK_FLOATS];
1218 let emb_dim = emb_dim.min(MAX_EMB_DIM);
1219 engine.reset_kv_cache();
1220
1221 // Chunked prefill: populate KV for prompt tokens [0, prompt_len-1).
1222 let prompt_len = ctx.len();
1223 crate::tensor::kv_provenance::rebuild_prompt_provenance(
1224 prompt_len as u32,
1225 crate::tensor::resident_substrate::global_resident_substrate().node_count(),
1226 0,
1227 );
1228 let draft_mapper = crate::topology_draft::TopologyDraftMapper::new(&tok);
1229 if prompt_len > 1 {
1230 if let Some(idx) = tensor_idx.as_ref() {
1231 let prefill_tokens = prompt_len - 1;
1232 let chunk_cap = (PREFILL_CHUNK_STACK_FLOATS / emb_dim)
1233 .min(PREFILL_CHUNK_SIZE)
1234 .max(1);
1235 let mut pos = 0usize;
1236 while pos < prefill_tokens {
1237 let n = (prefill_tokens - pos).min(chunk_cap);
1238 let batch_elems = n * emb_dim;
1239 {
1240 let mmap = match engine.gguf_mmap.as_deref() {
1241 Some(m) => m,
1242 None => break,
1243 };
1244 for t in 0..n {
1245 let _ = idx.dequantize_token_embedding_into(
1246 mmap,
1247 ctx[pos + t],
1248 &mut prefill_chunk[t * emb_dim..(t + 1) * emb_dim],
1249 );
1250 }
1251 }
1252 if !engine.dispatch_prefill_chunk(
1253 idx,
1254 &mut prefill_chunk[..batch_elems],
1255 emb_dim,
1256 n as u32,
1257 pos as u32,
1258 &mut scratch_a,
1259 &mut scratch_b,
1260 TEST_TRANSFORMER_LAYER_CAP,
1261 ) {
1262 crate::gguf_bridge::wlog(&format!(
1263 "[llm] PREFILL chunk FAILED pos={pos} n={n}"
1264 ));
1265 }
1266 pos += n;
1267 }
1268 }
1269 }
1270
1271 let mut out_ids: Vec<u32> = Vec::new();
1272 let mut streamed_len = 0usize;
1273 let mut sieve = if use_sieve {
1274 build_sieve(&tok, sieve_spec.as_ref(), sieve_lex_path.as_deref())
1275 } else {
1276 None
1277 };
1278 let mut semantic_quin: Option<NQuin> = None;
1279 let mut sieve_failed = false;
1280 let gen_budget = if sieve.is_some() {
1281 3usize
1282 } else {
1283 DECODE_TOKEN_BUDGET as usize
1284 };
1285
1286 crate::compute_universe::start_tensor_search_producer();
1287 crate::compute_universe::publish_query_tensor(
1288 crate::tensor::Tensor10D::default(),
1289 0,
1290 );
1291 crate::qualia_hybrid::prepare_hybrid_decode(&prompt_owned);
1292
1293 for step in 0..gen_budget {
1294 crate::gpu_context::record_llm_decode_step();
1295
1296 let on_token_sink = on_token.as_mut().map(|cb| cb as &mut dyn FnMut(String));
1297 let draft_step = try_accept_topology_draft(
1298 &mut engine,
1299 tensor_idx.as_ref(),
1300 &draft_mapper,
1301 &mut ctx,
1302 emb_dim,
1303 &mut emb_buf,
1304 &mut scratch_a,
1305 &mut scratch_b,
1306 &mut out_ids,
1307 &mut sieve,
1308 prov_hash,
1309 eos,
1310 &tok,
1311 &mut streamed_len,
1312 None,
1313 on_token_sink,
1314 );
1315 match draft_step {
1316 TopologyDraftStep::AcceptedFull => continue,
1317 TopologyDraftStep::Stop {
1318 sieve_failed: sf,
1319 semantic_quin: sq,
1320 } => {
1321 sieve_failed = sf;
1322 semantic_quin = sq;
1323 break;
1324 }
1325 _ => {}
1326 }
1327
1328 drain_tensor_context_inject();
1329 let _attention_mask = crate::compute_universe::attention_route_mask();
1330
1331 let rollback_val = rollback;
1332 rollback = false;
1333 let mut rollback = rollback_val;
1334 if matches!(draft_step, TopologyDraftStep::Denied) {
1335 rollback = true;
1336 }
1337
1338 let cur = *ctx.last().unwrap_or(&tok.bos_token_id);
1339 crate::compute_universe::publish_decode_hint(cur, step as u32);
1340
1341 // 1) Embedding lookup → hidden state (stack dequant).
1342 let hidden_ok = tensor_idx
1343 .as_ref()
1344 .and_then(|idx| {
1345 engine.gguf_mmap.as_deref().map(|m| {
1346 idx.dequantize_token_embedding_into(m, cur, &mut emb_buf[..emb_dim])
1347 })
1348 })
1349 .unwrap_or(0);
1350
1351 // 1b) LoRA delta — additive correction to the embedding vector.
1352 // Applied after dequantize so the base model is unmodified.
1353 // Silently skipped if dimensions don't match (wrong adapter for model).
1354 if hidden_ok > 0 {
1355 if let Some(ref adapter) = lora_adapter {
1356 if adapter.meta.n_in == hidden_ok && adapter.meta.n_out == hidden_ok {
1357 let snap: Vec<f32> = emb_buf[..hidden_ok].to_vec();
1358 let _ = adapter.apply_cpu(&snap, &mut emb_buf[..hidden_ok]);
1359 }
1360 }
1361 }
1362
1363 let (top_i, top_v) = if hidden_ok > 0 {
1364 if let Some(idx) = tensor_idx.as_ref() {
1365 let token_idx = ctx.len().saturating_sub(1) as u32;
1366 // CUDA mega-pass: attempt single-fence all-layer forward.
1367 // Returns Some(u32::MAX) sentinel when forward is done but logits
1368 // still needed (hidden state read back into emb_buf).
1369 #[cfg(all(not(target_arch = "wasm32"), feature = "cuda"))]
1370 let mega_token: Option<u32> = engine.try_cuda_mega_pass_decode(
1371 idx,
1372 &mut emb_buf[..emb_dim],
1373 emb_dim,
1374 token_idx,
1375 );
1376 #[cfg(any(target_arch = "wasm32", not(feature = "cuda")))]
1377 let mega_token: Option<u32> = None;
1378
1379 if let Some(mt) = mega_token.filter(|&t| t != u32::MAX) {
1380 (mt as usize, 0.0f32)
1381 } else if mega_token.is_some() {
1382 // Sentinel: forward done, logits needed.
1383 let sieve_mask = sieve.as_ref().map(|s| s.current_mask());
1384 if let Some(argmax) = engine.dispatch_output_argmax_chunked(
1385 idx,
1386 &emb_buf[..emb_dim],
1387 emb_dim,
1388 &mut scratch_a[..],
1389 TEST_VOCAB_CHUNK_CAP,
1390 sieve_mask,
1391 ) {
1392 if argmax.max_logit > f32::NEG_INFINITY {
1393 (argmax.best_token_id as usize, argmax.max_logit)
1394 } else {
1395 sieve_failed = true;
1396 (0usize, f32::NEG_INFINITY)
1397 }
1398 } else {
1399 emb_buf[..emb_dim].iter().enumerate().fold(
1400 (0usize, f32::NEG_INFINITY),
1401 |(bi, bv), (i, &v)| {
1402 if v > bv {
1403 (i, v)
1404 } else {
1405 (bi, bv)
1406 }
1407 },
1408 )
1409 }
1410 } else {
1411 let _layers = engine.dispatch_transformer_forward(
1412 idx,
1413 &mut emb_buf[..emb_dim],
1414 emb_dim,
1415 &mut scratch_a,
1416 &mut scratch_b,
1417 token_idx,
1418 TEST_TRANSFORMER_LAYER_CAP,
1419 );
1420 let _ = engine.apply_output_norm_inplace(
1421 idx,
1422 &mut emb_buf[..emb_dim],
1423 emb_dim,
1424 );
1425 let sieve_mask = sieve.as_ref().map(|s| s.current_mask());
1426 if let Some(argmax) = engine.dispatch_output_argmax_chunked(
1427 idx,
1428 &emb_buf[..emb_dim],
1429 emb_dim,
1430 &mut scratch_a[..],
1431 TEST_VOCAB_CHUNK_CAP,
1432 sieve_mask,
1433 ) {
1434 if argmax.max_logit > f32::NEG_INFINITY {
1435 (argmax.best_token_id as usize, argmax.max_logit)
1436 } else {
1437 sieve_failed = true;
1438 (0usize, f32::NEG_INFINITY)
1439 }
1440 } else {
1441 emb_buf[..emb_dim].iter().enumerate().fold(
1442 (0usize, f32::NEG_INFINITY),
1443 |(bi, bv), (i, &v)| {
1444 if v > bv {
1445 (i, v)
1446 } else {
1447 (bi, bv)
1448 }
1449 },
1450 )
1451 }
1452 }
1453 } else {
1454 (0usize, 0.0)
1455 }
1456 } else {
1457 let wt = QTensor::new(vec![emb_dim, emb_dim], 0, true);
1458 let logits = embedding_fallback_logits(
1459 &engine,
1460 tensor_idx.as_ref(),
1461 lora_adapter.as_ref(),
1462 cur,
1463 emb_dim,
1464 &mut emb_buf[..],
1465 &wt,
1466 );
1467 logits.iter().enumerate().fold(
1468 (0usize, f32::NEG_INFINITY),
1469 |(bi, bv), (i, &v)| {
1470 if v > bv {
1471 (i, v)
1472 } else {
1473 (bi, bv)
1474 }
1475 },
1476 )
1477 };
1478
1479 // anomaly 0x01 = normal. Removed IEEE mantissa 0x99 check (random ~1/256 fire).
1480 let anomaly = 0x01u8;
1481 let _ = anomaly; // reserved for real governance signals
1482
1483 if sieve_failed {
1484 break;
1485 }
1486
1487 // DenyRollback must never inject sequential garbage (cur+1).
1488 if rollback {
1489 log::warn!(
1490 "LLM_DECODE|sentinel-deny-rollback|keeping argmax token {} (no cur+1)",
1491 top_i
1492 );
1493 }
1494 let next = (top_i as u32) % vlen;
1495
1496 if let Some(ref mut s) = sieve {
1497 match s.apply_token(next) {
1498 Ok(()) => {
1499 out_ids.push(next);
1500 ctx.push(next);
1501 if s.is_complete() {
1502 semantic_quin = Some(s.assemble_quin(prov_hash));
1503 break;
1504 }
1505 }
1506 Err(_) => {
1507 sieve_failed = true;
1508 break;
1509 }
1510 }
1511 } else {
1512 out_ids.push(next);
1513 ctx.push(next);
1514 if let Some(ref mut cb) = on_token {
1515 let full = tok.decode(&out_ids);
1516 if full.len() > streamed_len {
1517 let delta = full[streamed_len..].to_string();
1518 streamed_len = full.len();
1519 cb(delta);
1520 }
1521 }
1522 let fixed = crate::llm_bench::decode_budget_fixed_tokens();
1523 if out_ids.len() >= gen_budget || (!fixed && tok.is_stop_token(next)) {
1524 break;
1525 }
1526 }
1527 }
1528
1529 let text = if semantic_quin.is_some() {
1530 String::new()
1531 } else if sieve_failed {
1532 String::from("[sieve-misaligned]")
1533 } else {
1534 let raw = tok.decode(&out_ids);
1535 if crate::inference_modes::post_turn_verify_enabled() {
1536 let v = crate::post_turn_verify::verify_and_heal_turn(&prompt_owned, &raw);
1537 if crate::post_turn_verify::return_html_as_text() {
1538 v.display_html
1539 } else {
1540 v.final_text
1541 }
1542 } else {
1543 crate::quant_graph_grounding::maybe_ground_generation(&prompt_owned, &raw)
1544 .text
1545 }
1546 };
1547
1548 // Return engine to global instance
1549 {
1550 crate::gguf_bridge::WASM_ENGINE_INSTANCE.with(|g| {
1551 *g.borrow_mut() = Some(engine);
1552 });
1553 }
1554
1555 (text, out_ids.len() as u32, semantic_quin, sieve_failed)
1556 };
1557 let mut prov = vec![prov_hash];
1558 if prov_hash == 0 {
1559 prov.push(q_hash("qualia:grounded"));
1560 }
1561 if let Some(q) = semantic_quin {
1562 prov.push(q.subject);
1563 prov.push(q.predicate);
1564 prov.push(q.object);
1565 }
1566 if sieve_failed && semantic_quin.is_none() {
1567 return (text, prov, tokens, None);
1568 }
1569 return (text, prov, tokens, semantic_quin);
1570 }
1571 }
1572}