Skip to main content

qualia_core_db/inference/
qualia_hybrid.rs

1//! Qualia-unique hybrid inference: graph + manifold + modes + deontic.
2//!
3//! Most engines only optimise GEMV. QualiaDB can also:
4//! 1. **Graph-grounded speculative draft** — encode fact-table repairs as draft
5//!    tokens and verify with the same batch path as prompt-lookup (bit-exact when
6//!    accepted; force-emit when `QUALIA_GRAPH_FORCE=1`).
7//! 2. **Graph route mask** — hash prompt tokens into the U1 `AttentionRouteMask`
8//!    so sparse attention can bias toward graph-linked KV provenance slots.
9//! 3. **10D query publish** — fold prompt word hashes into a `Tensor10D` query
10//!    anchor for the continuous U1→U0 context-inject path.
11//! 4. **Graph logit bias** — boost tokenizer ids for known answer strings mid-decode
12//!    (neuro-symbolic sampling, not post-hoc string replace only).
13//! 5. **Deontic obligation** — when a high-stakes fact matches the prompt, compile
14//!    an `OP_OBLIGATE` norm Quin so the Rights/Webizen layer can audit the duty
15//!    to ground (unique to Qualia’s logic stack).
16//!
17//! These are first-class companions to portable/cuda/quant-graph modes — not a
18//! second engine.
19
20use crate::compute_universe::{
21    publish_attention_route_mask, publish_query_tensor, AttentionRouteMask,
22};
23use crate::prompt_lookup::{Draft, MAX_DRAFT};
24use crate::q_hash;
25use crate::tensor::Tensor10D;
26use crate::NQuin;
27
28/// Logit boost applied to graph answer-token ids (nats; soft guidance).
29pub const GRAPH_LOGIT_BIAS: f32 = 2.5;
30
31/// Prepare hybrid hints before a decode turn (safe no-ops when inactive).
32/// Call **after** any default `publish_query_tensor` so route/query are not wiped.
33/// FastVerify skips mid-decode hybrid work (post-turn verify owns quality).
34///
35/// **Do not** enable route masks under `mode=cuda` alone. That used to OR in
36/// `prefer_tensor_core_gemm()`, which published random prompt-hash KV masks and
37/// destroyed attention (measured garbage decode 2026-07-24: repeated `Ċ` tokens).
38/// Hybrid routing is for quant-graph / explicit `QUALIA_HYBRID_ROUTE=1` only.
39pub fn prepare_hybrid_decode(prompt: &str) {
40    if matches!(
41        crate::inference_modes::active_inference_mode(),
42        crate::inference_modes::InferenceMode::FastVerify
43    ) {
44        return;
45    }
46    let hybrid_route = crate::inference_modes::quant_graph_grounding_enabled()
47        || matches!(
48            std::env::var("QUALIA_HYBRID_ROUTE").ok().as_deref(),
49            Some("1") | Some("true")
50        );
51    if hybrid_route {
52        publish_graph_route_from_prompt(prompt);
53        publish_prompt_query_tensor(prompt);
54    }
55    if crate::inference_modes::quant_graph_grounding_enabled() {
56        let _ = publish_grounding_obligation(prompt);
57    }
58}
59
60/// Map prompt words → attention route bits (tensor/KV provenance indices).
61/// Novel: uses the same U1 mask path as 10D kNN routing, fed from language not GPU.
62/// When quant-graph matches a fact, also route bits from subject/object quin hashes
63/// so attention can prefer KV slots co-located with grounding provenance.
64pub fn publish_graph_route_from_prompt(prompt: &str) {
65    let mut mask = AttentionRouteMask::default();
66    for word in prompt.split(|c: char| !c.is_alphanumeric()) {
67        if word.len() < 3 {
68            continue;
69        }
70        let h = q_hash(&word.to_ascii_lowercase());
71        // 1024 KV slots covered by route mask mapping in attention_kv_mask_u32.
72        mask.set_index((h % 1024) as u32);
73        // Also set a few nearby slots for soft neighbourhood.
74        mask.set_index(((h.wrapping_add(1)) % 1024) as u32);
75    }
76    // Fact-quin routing: subject/object hashes → deterministic KV indices.
77    if crate::inference_modes::quant_graph_grounding_enabled() {
78        let g = crate::quant_graph_grounding::ground_generation(prompt, "");
79        if let Some(obj) = g.object_hash {
80            mask.set_index((obj % 1024) as u32);
81            mask.set_index(((obj >> 10) % 1024) as u32);
82            // Export quin into a one-slot scratch for future SPARQL/WAL consumers.
83            let mut buf = [NQuin {
84                subject: 0,
85                predicate: 0,
86                object: 0,
87                context: 0,
88                metadata: 0,
89                parity: 0,
90            }; 8];
91            let n = crate::quant_graph_grounding::export_fact_quins(&mut buf);
92            for q in buf.iter().take(n) {
93                mask.set_index((q.subject % 1024) as u32);
94                mask.set_index((q.object % 1024) as u32);
95            }
96        }
97    }
98    if mask.active_bits > 0 {
99        publish_attention_route_mask(mask);
100        log::debug!("qualia_hybrid|route_mask|bits={}", mask.active_bits);
101    }
102}
103
104/// Fold prompt into a 10D query for continuous graph–tensor inject.
105pub fn publish_prompt_query_tensor(prompt: &str) {
106    let mut t = Tensor10D {
107        q: 0.0,
108        v: 0.0,
109        w: 0.0,
110        x: 0.0,
111        y: 0.0,
112        z: 0.0,
113        t: 0.0,
114        alpha: 0.0,
115        mu: 0.0,
116        sigma: 0.0,
117    };
118    let mut subject = 0u64;
119    let mut i = 0usize;
120    for word in prompt.split_whitespace().take(32) {
121        let h = q_hash(word);
122        subject ^= h.rotate_left((i as u32) * 3);
123        let f = ((h & 0xFFFF) as f32) / 65535.0;
124        match i % 10 {
125            0 => t.q += f,
126            1 => t.v += f,
127            2 => t.w += f,
128            3 => t.x += f,
129            4 => t.y += f,
130            5 => t.z += f,
131            6 => t.t += f,
132            7 => t.alpha += f,
133            8 => t.mu += f,
134            _ => t.sigma += f,
135        }
136        i += 1;
137    }
138    if i > 0 {
139        let n = i as f32;
140        t.q /= n;
141        t.v /= n;
142        t.w /= n;
143        t.x /= n;
144        t.y /= n;
145        t.z /= n;
146        t.t /= n;
147        t.alpha /= n;
148        t.mu /= n;
149        t.sigma /= n;
150        publish_query_tensor(t, subject);
151    }
152}
153
154/// Draft tokens from the quant-graph fact table (repair text encoded by caller).
155///
156/// `encode` should be the model tokenizer (`encode` or chat-aware). Returns empty
157/// when no fact matches the prompt. Uses empty-answer probe: if needles match and
158/// the (empty) answer is not yet grounded, draft the repair string for verify.
159pub fn propose_fact_draft(prompt: &str, encode: &dyn Fn(&str) -> Vec<u32>) -> Draft {
160    if !crate::inference_modes::quant_graph_grounding_enabled() {
161        return Draft::empty();
162    }
163    // Empty answer never contains answer_ok → repaired=true iff needles match.
164    let g = crate::quant_graph_grounding::ground_generation(prompt, "");
165    // Accept either repaired text or a known reason with object (fact hit).
166    let repair = if g.repaired {
167        g.text.as_str()
168    } else if g.reason.is_some() && g.object_hash.is_some() {
169        // Prompt matches a fact but empty answer was treated as already-ok (shouldn't
170        // happen); still no draft — model is free.
171        return Draft::empty();
172    } else {
173        return Draft::empty();
174    };
175    let ids = encode(repair);
176    if ids.is_empty() {
177        return Draft::empty();
178    }
179    let mut d = Draft::empty();
180    let take = ids.len().min(MAX_DRAFT);
181    d.tokens[..take].copy_from_slice(&ids[..take]);
182    d.len = take;
183    log::info!("qualia_hybrid|fact_draft|reason={:?}|len={take}", g.reason);
184    d
185}
186
187/// Whether to force-emit graph repair without model verify (high-stakes capitals, etc.).
188#[inline]
189pub fn graph_force_enabled() -> bool {
190    matches!(
191        std::env::var("QUALIA_GRAPH_FORCE").ok().as_deref(),
192        Some("1") | Some("true")
193    )
194}
195
196/// If quant-graph + force, return full repair token sequence for immediate emit.
197pub fn force_fact_tokens(prompt: &str, encode: &dyn Fn(&str) -> Vec<u32>) -> Option<Vec<u32>> {
198    if !crate::inference_modes::quant_graph_grounding_enabled() || !graph_force_enabled() {
199        return None;
200    }
201    let g = crate::quant_graph_grounding::ground_generation(prompt, "");
202    if !g.repaired {
203        return None;
204    }
205    let ids = encode(&g.text);
206    if ids.is_empty() {
207        None
208    } else {
209        log::info!("qualia_hybrid|fact_force|reason={:?}", g.reason);
210        Some(ids)
211    }
212}
213
214/// Soft-boost logits for graph answer strings (first matching token id per string).
215///
216/// `lookup` maps a short answer string → preferred token id (e.g. tokenizer encode
217/// of "Paris" / "paris"). Call after full logits are on host, before sample/argmax.
218/// Returns how many vocab entries were boosted.
219pub fn apply_graph_logit_bias(
220    prompt: &str,
221    logits: &mut [f32],
222    lookup: &dyn Fn(&str) -> Option<u32>,
223) -> usize {
224    if !crate::inference_modes::quant_graph_grounding_enabled() || logits.is_empty() {
225        return 0;
226    }
227    let g = crate::quant_graph_grounding::ground_generation(prompt, "");
228    if !g.repaired {
229        // Already grounded or no match — nothing to bias.
230        return 0;
231    }
232    // Prefer the repair text tokens and the reason's capital name.
233    let mut boosted = 0usize;
234    let candidates: [&str; 4] = [
235        g.reason.as_deref().unwrap_or(""),
236        "Paris",
237        "paris",
238        g.text.as_str(),
239    ];
240    // Extract last word of repair as primary answer (… is Paris.)
241    let last_word = g
242        .text
243        .split(|c: char| !c.is_alphanumeric())
244        .filter(|w| w.len() > 2)
245        .last()
246        .unwrap_or("");
247    for s in [last_word, candidates[1], candidates[2]] {
248        if s.is_empty() {
249            continue;
250        }
251        if let Some(tid) = lookup(s) {
252            let i = tid as usize;
253            if i < logits.len() {
254                logits[i] += GRAPH_LOGIT_BIAS;
255                boosted += 1;
256            }
257        }
258        // Case variants
259        let lower = s.to_ascii_lowercase();
260        if lower != s {
261            if let Some(tid) = lookup(&lower) {
262                let i = tid as usize;
263                if i < logits.len() {
264                    logits[i] += GRAPH_LOGIT_BIAS * 0.5;
265                    boosted += 1;
266                }
267            }
268        }
269    }
270    if boosted > 0 {
271        log::debug!("qualia_hybrid|logit_bias|n={boosted}|reason={:?}", g.reason);
272    }
273    boosted
274}
275
276/// Compile a deontic **obligation** Quin for a matched grounding fact (audit trail).
277///
278/// Uses real `compile_norm_quin` — party = process principal hash, property =
279/// capitalOf path, contract = grounding context. Returns None when no fact matches.
280pub fn publish_grounding_obligation(prompt: &str) -> Option<NQuin> {
281    if !crate::inference_modes::quant_graph_grounding_enabled() {
282        return None;
283    }
284    let g = crate::quant_graph_grounding::ground_generation(prompt, "");
285    if g.reason.is_none() && !g.repaired {
286        return None;
287    }
288    let object = g.object_hash.unwrap_or(0);
289    if object == 0 {
290        return None;
291    }
292    // Party: synthetic "decode principal"; property: capitalOf; action: object city.
293    let party = q_hash("q42:inference-principal");
294    let property = q_hash(crate::quant_graph_grounding::P_CAPITAL_OF);
295    let contract = q_hash(crate::quant_graph_grounding::CTX_GROUNDING);
296    // Portal-only WASM lacks `crate::modalities`; obligation Quins need native/full stack.
297    #[cfg(any(
298        not(target_arch = "wasm32"),
299        feature = "wasm-ontology",
300        feature = "wasm-logic",
301        feature = "wasm-scientific",
302        feature = "wasm-full"
303    ))]
304    {
305        let quin = crate::modalities::logic::deontic::compile_norm_quin(
306            party,
307            crate::modalities::logic::deontic::OP_OBLIGATE,
308            property,
309            object,
310            contract,
311            0, // no expiry
312            false,
313        );
314        log::info!(
315            "qualia_hybrid|deontic_obligate|reason={:?}|object={object:#x}",
316            g.reason
317        );
318        Some(quin)
319    }
320    #[cfg(all(
321        target_arch = "wasm32",
322        not(any(
323            feature = "wasm-ontology",
324            feature = "wasm-logic",
325            feature = "wasm-scientific",
326            feature = "wasm-full"
327        ))
328    ))]
329    {
330        let _ = (party, property, object, contract);
331        log::info!(
332            "qualia_hybrid|deontic_obligate_skipped|portal_wasm|reason={:?}|object={object:#x}",
333            g.reason
334        );
335        None
336    }
337}
338
339/// Prefer fact draft when quant-graph; else prompt-lookup n-gram draft.
340pub fn propose_best_draft(prompt: &str, ctx: &[u32], encode: &dyn Fn(&str) -> Vec<u32>) -> Draft {
341    let fact = propose_fact_draft(prompt, encode);
342    if fact.len > 0 {
343        return fact;
344    }
345    crate::prompt_lookup::propose(ctx, MAX_DRAFT)
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use crate::inference_modes::{set_inference_mode, InferenceMode};
352    use crate::quant_graph_grounding::reset_fact_store_to_defaults;
353    use std::sync::Mutex;
354
355    /// Serialise mode mutations — InferenceMode is process-global.
356    fn mode_lock() -> std::sync::MutexGuard<'static, ()> {
357        static LOCK: Mutex<()> = Mutex::new(());
358        LOCK.lock().unwrap_or_else(|e| e.into_inner())
359    }
360
361    #[test]
362    fn fact_draft_on_capital_prompt() {
363        if std::env::var("QUALIA_INFERENCE_MODE").is_ok() {
364            return;
365        }
366        let _g = mode_lock();
367        reset_fact_store_to_defaults();
368        set_inference_mode(InferenceMode::QuantGraph);
369        // Sanity: empty answer must repair when needles match.
370        let g =
371            crate::quant_graph_grounding::ground_generation("What is the capital of France?", "");
372        assert!(
373            g.repaired,
374            "expected repair on empty answer; reason={:?} text={}",
375            g.reason, g.text
376        );
377        let encode = |s: &str| {
378            // Fake tokenizer: one id per byte
379            s.bytes().map(|b| b as u32).collect()
380        };
381        let d = propose_fact_draft("What is the capital of France?", &encode);
382        assert!(
383            d.len > 0,
384            "draft len 0; repaired={} text={}",
385            g.repaired,
386            g.text
387        );
388        set_inference_mode(InferenceMode::Portable);
389    }
390
391    #[test]
392    fn route_mask_sets_bits() {
393        publish_graph_route_from_prompt("capital of France Paris knowledge graph");
394        let m = crate::compute_universe::attention_route_mask();
395        assert!(m.active_bits > 0);
396    }
397
398    #[test]
399    fn logit_bias_boosts_vocab_slot() {
400        if std::env::var("QUALIA_INFERENCE_MODE").is_ok() {
401            return;
402        }
403        let _g = mode_lock();
404        reset_fact_store_to_defaults();
405        set_inference_mode(InferenceMode::QuantGraph);
406        let mut logits = vec![0.0f32; 128];
407        // Map any answer string containing 'P'/'p' style to fixed ids.
408        let lookup = |s: &str| -> Option<u32> {
409            if s.eq_ignore_ascii_case("paris") {
410                Some(42)
411            } else {
412                None
413            }
414        };
415        let n = apply_graph_logit_bias("What is the capital of France?", &mut logits, &lookup);
416        assert!(n >= 1);
417        assert!(logits[42] >= GRAPH_LOGIT_BIAS);
418        set_inference_mode(InferenceMode::Portable);
419    }
420
421    #[test]
422    fn deontic_obligation_on_match() {
423        if std::env::var("QUALIA_INFERENCE_MODE").is_ok() {
424            return;
425        }
426        let _g = mode_lock();
427        reset_fact_store_to_defaults();
428        set_inference_mode(InferenceMode::QuantGraph);
429        let q = publish_grounding_obligation("What is the capital of France?");
430        assert!(q.is_some());
431        let q = q.unwrap();
432        assert_eq!(
433            (q.predicate & 0xFF) as u8,
434            crate::modalities::logic::deontic::OP_OBLIGATE
435        );
436        set_inference_mode(InferenceMode::Portable);
437    }
438}